diff --git a/CHANGELOG.md b/CHANGELOG.md index b86fa87a..b26ca72d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased +## [4.4.2] - 2023-10-09 + +### Added + +- Added more info to README. +- Escape codes that are too long now push the parser back into IDLE state, closes #270. Added setting to select max. escape code length. + +### Fixed + +- Tab key now gets captured by the Terminal panes and HT char code sent, closes #263. +- Removed unused imports from Typescript files. +- RX terminals no longer behave like they can capture keystrokes, closes #269. +- Improved handling of a FramingError on read(), closes #259. + +### Changed + +- Rearranged folder structure of view components. +- Tooltips now follow the cursor around, improving usability in settings menu, closes #261. + ## [4.4.1] - 2023-10-04 ### Fixed @@ -409,7 +428,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added auto-scroll to TX pane, closes #89. - Added special delete behaviour for backspace button when in "send on enter" mode, closes #90. -[unreleased]: https://github.com/gbmhunter/NinjaTerm/compare/v4.4.1...HEAD +[unreleased]: https://github.com/gbmhunter/NinjaTerm/compare/v4.4.2...HEAD +[4.4.2]: https://github.com/gbmhunter/NinjaTerm/compare/v4.4.1...v4.4.2 [4.4.1]: https://github.com/gbmhunter/NinjaTerm/compare/v4.4.0...v4.4.1 [4.4.0]: https://github.com/gbmhunter/NinjaTerm/compare/v4.3.1...v4.4.0 [4.3.1]: https://github.com/gbmhunter/NinjaTerm/compare/v4.3.0...v4.3.1 diff --git a/README.md b/README.md index e5da684c..6982fd08 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ Arduino sketches in `arduino-serial` allow you to program different applications 1. Update the version number in `package.json`. 1. Update the CHANGELOG (don't forget the links right at the bottom of the page). -1. Create pull request merging `develop` into `main`. +1. Commit changes and push to `develop`. +1. Create pull request on GitHub merging `develop` into `main`. 1. Once the build on `develop` has been successfully run, merge the `develop` branch into `main` via the merge request. 1. Tag the branch on main with the version number, e.g. `v4.1.0`. 1. Create a release on GitHub pointing to the tag. @@ -50,7 +51,7 @@ Arduino sketches in `arduino-serial` allow you to program different applications ## Deployment -Netlify is used to deploy and host the static NinjaTerm HTML/JS. +Netlify is used to deploy and host the static NinjaTerm HTML/JS. Netlify automatically deploys when the `main` branch is updated. Netlify also creates preview deploys on pull requests (link will be automatically posted into the PR comments). ## Code Architecture @@ -60,15 +61,14 @@ Create React App (CRA) with the typescript PWA template [docs here](https://crea npx create-react-app my-app --template cra-template-pwa-typescript ``` -MobX is used to store the application state. The application model is under `src/model/`. +The React based user interface code is under `src/view`. -## GitHub Pages - -The `docs/` folder contains the source code for the NinjaTerm homepage, hosted by GitHub Pages. This is automatically build and deployed with new commits pushed to `main`. +MobX is used to store the application state (model). The React component redraw themselves based on the state of the model. The application model is under `src/model/`. ## Theme Colors -* DC3545 (red): Primary colour, used for logo. +* `#DC3545` (red): Primary colour, used for logo. +* `#E47F37` (orange): Secondary colour, used for buttons on homepage. ## Extensions diff --git a/package.json b/package.json index 95f5e097..f9cc4993 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ninjaterm", - "version": "4.4.1", + "version": "4.4.2", "private": true, "dependencies": { "@emotion/react": "^11.11.1", diff --git a/src/index.tsx b/src/index.tsx index c5386464..37c611e9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,8 +10,8 @@ import { import ReactGA from "react-ga4"; import { App } from './model/App'; -import AppView from './AppView'; -import HomepageView from './HomepageView'; +import AppView from './view/AppView'; +import HomepageView from './view/Homepage/HomepageView'; // Google Analytics ReactGA.initialize("G-SDMMGN71FN"); diff --git a/src/model/App.tsx b/src/model/App.tsx index 594cd38c..7a29c804 100644 --- a/src/model/App.tsx +++ b/src/model/App.tsx @@ -4,19 +4,12 @@ import { makeAutoObservable, runInAction } from 'mobx'; import StopIcon from '@mui/icons-material/Stop'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; -import { VariantType, enqueueSnackbar } from 'notistack'; import packageDotJson from '../../package.json' // eslint-disable-next-line import/no-cycle import { Settings } from './Settings/Settings'; import Terminal from './Terminal/Terminal'; -import * as serviceWorkerRegistration from '../serviceWorkerRegistration'; - -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://cra.link/PWA -// serviceWorkerRegistration.unregister(); -// serviceWorkerRegistration.register(); +import Snackbar from './Snackbar'; declare global { interface String { @@ -101,11 +94,11 @@ export class App { closedPromise: Promise | null; - snackBarOpen: boolean; - // Version of the NinjaTerm app. Read from package.json version: string; + snackbar: Snackbar; + constructor( testing = false ) { @@ -115,11 +108,11 @@ export class App { this.settings = new Settings(this); - // Need to create terminals before settings, as the settings - // will configure the terminals - this.txRxTerminal = new Terminal(this.settings); - this.rxTerminal = new Terminal(this.settings); - this.txTerminal = new Terminal(this.settings); + this.snackbar = new Snackbar(); + + this.txRxTerminal = new Terminal(this.settings, this.snackbar, true); + this.rxTerminal = new Terminal(this.settings, this.snackbar, false); // Not focusable + this.txTerminal = new Terminal(this.settings, this.snackbar, true); this.numBytesReceived = 0; this.numBytesTransmitted = 0; @@ -129,11 +122,9 @@ export class App { this.reader = null; this.closedPromise = null; - this.snackBarOpen = false; - console.log('Started NinjaTerm.') - // this.runTestMode(); + // this.runTestModeBytes0To255(); // This is fired whenever a serial port that has been allowed access // dissappears (i.e. USB serial), even if we are not connected to it. // navigator.serial.addEventListener("disconnect", (event) => { @@ -166,8 +157,22 @@ export class App { }, 200); } - setSnackBarOpen(trueFalse: boolean) { - this.snackBarOpen = trueFalse; + /** Function used for testing when you don't have an Arduino handy. + * Sets up a interval timer to add fake RX data. + * Change as needed for testing! + */ + runTestModeBytes0To255() { + console.log('runTestMode2() called.'); + this.settings.dataProcessing.visibleData.fields.ansiEscapeCodeParsingEnabled.value = false; + this.settings.dataProcessing.applyChanges(); + let testCharIdx = 0; + setInterval(() => { + this.parseRxData(Uint8Array.from([ testCharIdx ])); + testCharIdx += 1; + if (testCharIdx === 256) { + testCharIdx = 0; + } + }, 200); } setSettingsDialogOpen(trueFalse: boolean) { @@ -195,7 +200,7 @@ export class App { localPort = await navigator.serial.requestPort(); } catch (error) { console.log('Error occurred. error=', error); - this.sendToSnackbar('User cancelled port selection.', 'error'); + this.snackbar.sendToSnackbar('User cancelled port selection.', 'error'); return; } console.log('Got local port, now setting state...'); @@ -223,13 +228,13 @@ export class App { const msg = 'Serial port is already in use by another program.\n' + 'Reported error from port.open():\n' + `${error}` - this.sendToSnackbar(msg, 'error'); + this.snackbar.sendToSnackbar(msg, 'error'); console.log(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.sendToSnackbar(msg, 'error'); + this.snackbar.sendToSnackbar(msg, 'error'); console.log(msg); } } else { @@ -237,7 +242,7 @@ export class App { const msg = `Unrecognized error occurred when trying to open serial port.\n` + 'Reported error from port.open():\n' + `${error}` - this.sendToSnackbar(msg, 'error'); + this.snackbar.sendToSnackbar(msg, 'error'); console.log(msg); } @@ -246,7 +251,7 @@ export class App { return; } console.log('Serial port opened.'); - this.sendToSnackbar('Serial port opened.', 'success'); + this.snackbar.sendToSnackbar('Serial port opened.', 'success'); this.setPortState(PortState.OPENED); // This will automatically close the settings window if the user is currently in it, // clicks "Open" and the port opens successfully. @@ -281,6 +286,7 @@ export class App { // This is called if the USB serial device is removed whilst // reading console.log('reader.read() threw an error. error=', error, 'port.readable="', this.port?.readable, '" (null indicates fatal error)'); + // These error are described at https://wicg.github.io/serial/ // @ts-ignore: if (error instanceof DOMException) { console.log('Exception was DOMException. error.name=', error.name); @@ -288,24 +294,34 @@ export class App { // potentially make buffer size to port.open() larger or speed up processing/rendering // if this occurs often. This is non-fatal, readable will not be null if (error.name === 'BufferOverrunError') { - this.sendToSnackbar('RX buffer overrun occurred. Too much data is coming in for the app to handle.\n' + this.snackbar.sendToSnackbar('RX buffer overrun occurred. Too much data is coming in for the app to handle.\n' + 'Returned error from reader.read():\n' + `${error}`, 'warning'); } else if (error.name === 'BreakError') { - this.sendToSnackbar('Encountered break signal.\n' + this.snackbar.sendToSnackbar('Encountered break signal.\n' + 'Returned error from reader.read():\n' + `${error}`, 'warning'); - } else { + } else if (error.name === 'FramingError') { + this.snackbar.sendToSnackbar('Encountered framing error.\n' + + 'Returned error from reader.read():\n' + + `${error}`, + 'warning'); + } else if (error.name === 'ParityError') { + this.snackbar.sendToSnackbar('Encountered parity error.\n' + + 'Returned error from reader.read():\n' + + `${error}`, + 'warning'); + } else { const msg = `Unrecognized DOMException error with name=${error.name} occurred when trying to read from serial port.\n` + 'Reported error from port.read():\n' + `${error}`; - this.sendToSnackbar(msg, 'error'); + this.snackbar.sendToSnackbar(msg, 'error'); console.log(msg); } } else { - this.sendToSnackbar(`Serial port was removed unexpectedly.\nReturned error from reader.read():\n${error}`, 'error'); + this.snackbar.sendToSnackbar(`Serial port was removed unexpectedly.\nReturned error from reader.read():\n${error}`, 'error'); } // this.setPortState(PortState.CLOSED); @@ -327,21 +343,8 @@ export class App { } /** - * Enqueues a message to the snackbar used for temporary status updates to the user. + * In normal operation this is called from the readUntilClose() function above. * - * @param msg The message you want to display. Use "\n" to insert new lines. - * @param variant The variant (e.g. error, warning) of snackbar you want to display. - */ - sendToSnackbar(msg: string, variant: VariantType) { - enqueueSnackbar( - msg, - { - variant: variant, - style: { whiteSpace: 'pre-line' } // This allows the new lines in the string above to also be carried through to the displayed message - }); - } - - /** * Unit tests call this instead of mocking out the serial port read() function * as setting up the deferred promise was too tricky. * @@ -369,7 +372,7 @@ export class App { await this.closedPromise; this.setPortState(PortState.CLOSED); - this.sendToSnackbar('Serial port closed.', 'success'); + this.snackbar.sendToSnackbar('Serial port closed.', 'success'); this.reader = null; this.closedPromise = null; } @@ -387,6 +390,13 @@ export class App { */ async handleKeyDown(event: React.KeyboardEvent) { // console.log('handleKeyDown() called. event=', event, this); + + // Prevent Tab press from moving focus to another element on screen + // Do this even if port is not opened + if (event.key === 'Tab') { + event.preventDefault(); + } + if (this.portState === PortState.OPENED) { // Serial port is open, let's send it to the serial // port @@ -430,8 +440,11 @@ export class App { } else if (event.key === 'ArrowDown') { // Send "ESC[B" (go down 1) bytesToWrite.push(0x1B, '['.charCodeAt(0), 'B'.charCodeAt(0)); - // If we get here, we don't know what to do with the key press + } else if (event.key === 'Tab') { + // Send horizontal tab, HT, 0x09 + bytesToWrite.push(0x09); } else { + // If we get here, we don't know what to do with the key press console.log('Unsupported char! event=', event); return; } diff --git a/src/model/Settings/DataProcessingSettings.tsx b/src/model/Settings/DataProcessingSettings.tsx index 69d1118f..ea715626 100644 --- a/src/model/Settings/DataProcessingSettings.tsx +++ b/src/model/Settings/DataProcessingSettings.tsx @@ -31,6 +31,12 @@ class Data { errorMsg: '', rule: 'required', }, + maxEscapeCodeLengthChars: { + value: 10, + hasError: false, + errorMsg: '', + rule: 'required|integer|min:2', // Min. is two, one for the escape byte and then a single char. + }, terminalWidthChars: { value: 120, // 80 is standard hasError: false, diff --git a/src/model/Snackbar.tsx b/src/model/Snackbar.tsx new file mode 100644 index 00000000..9e7e4e63 --- /dev/null +++ b/src/model/Snackbar.tsx @@ -0,0 +1,32 @@ +import { makeAutoObservable } from 'mobx'; +import { VariantType, enqueueSnackbar } from 'notistack'; + +export default class Snackbar { + + snackBarOpen: boolean; + + constructor() { + this.snackBarOpen = false; + makeAutoObservable(this); // Make sure this near the end + }; + + setSnackBarOpen(trueFalse: boolean) { + this.snackBarOpen = trueFalse; + } + + /** + * Enqueues a message to the snackbar used for temporary status updates to the user. + * + * @param msg The message you want to display. Use "\n" to insert new lines. + * @param variant The variant (e.g. error, warning) of snackbar you want to display. + */ + sendToSnackbar(msg: string, variant: VariantType) { + enqueueSnackbar( + msg, + { + variant: variant, + style: { whiteSpace: 'pre-line' } // This allows the new lines in the string above to also be carried through to the displayed message + }); + } + +} diff --git a/src/model/Terminal/Terminal.tsx b/src/model/Terminal/Terminal.tsx index 8f12e1c8..f70b8848 100644 --- a/src/model/Terminal/Terminal.tsx +++ b/src/model/Terminal/Terminal.tsx @@ -1,24 +1,15 @@ /* eslint-disable no-continue */ -import { autorun, makeAutoObservable, observe, reaction } from 'mobx'; -import { ReactElement } from 'react'; -// import { TextEncoder, TextDecoder } from 'util'; -// import { assert } from 'console'; +import { autorun, makeAutoObservable } from 'mobx'; +import { Settings } from 'model/Settings/Settings'; +import Snackbar from 'model/Snackbar'; import TerminalRow from './TerminalRow'; import TerminalChar from './TerminalChar'; -import { App } from '../App'; -import { Settings } from '../Settings/Settings'; - -// Polyfill because TextDecoder is not bundled with jsdom 16 and breaks Jest, see -// https://stackoverflow.com/questions/68468203/why-am-i-getting-textencoder-is-not-defined-in-jest -// Object.assign(global, { TextDecoder, TextEncoder }); /** * Represents a single terminal-style user interface. */ export default class Terminal { - outputHtml: ReactElement[]; - // This represents the current style active on the terminal currentStyle: {}; @@ -67,14 +58,24 @@ export default class Terminal { // Used to know when to capture key strokes for the Terminal isFocused: boolean; - constructor(settings: Settings) { + // If this is set to false, the Terminal is not focusable. It will not have a background + // glow on hover or click, and the cursor will always outlined, never filled in. + isFocusable: boolean; + + snackbar: Snackbar; + + constructor(settings: Settings, snackbar: Snackbar, isFocusable: boolean) { this.settings = settings; + this.snackbar = snackbar; + this.isFocusable = isFocusable; autorun(() => { if (!this.settings.dataProcessing.appliedData.fields.ansiEscapeCodeParsingEnabled.value) { // ANSI escape code parsing has been disabled // Flush any partial ANSI escape code - this.addVisibleChars(this.partialEscapeCode); + for (let idx = 0; idx < this.partialEscapeCode.length; idx += 1) { + this.addVisibleChar(this.partialEscapeCode[idx].charCodeAt(0)); + } this.partialEscapeCode = ''; this.inAnsiEscapeCode = false; this.inCSISequence = false; @@ -89,7 +90,6 @@ export default class Terminal { // } // ) - this.outputHtml = []; this.cursorPosition = [0, 0]; this.scrollLock = true; @@ -145,15 +145,32 @@ export default class Terminal { // Parse each character // console.log('parseData() called. data=', data); // const dataAsStr = new TextDecoder().decode(data); - const dataAsStr = String.fromCharCode.apply(null, Array.from(data)); + + // This variable can get modified during the loop, for example if a partial escape code + // reaches it's length limit, the ESC char is stripped and the remainder of the partial is + // prepending onto dataAsStr for further processing + // let dataAsStr = String.fromCharCode.apply(null, Array.from(data)); + + let remainingData: number[] = [] for (let idx = 0; idx < data.length; idx += 1) { - const char = dataAsStr[idx]; + remainingData.push(data[idx]); + } + + while (true) { + + // Remove char from start of remaining data + let rxByte = remainingData.shift(); + if (rxByte === undefined) { + break; + } + + // const char = dataAsStr[idx]; // This console print is very useful when debugging // console.log(`char: "${char}", 0x${char.charCodeAt(0).toString(16)}`); // Don't want to interpret new lines if we are half-way // through processing an ANSI escape code - if (this.inIdleState && char === '\n') { + if (this.inIdleState && rxByte === '\n'.charCodeAt(0)) { this.moveToNewLine(); // this.limitNumRows(); // eslint-disable-next-line no-continue @@ -162,12 +179,12 @@ export default class Terminal { // Check if ANSI escape code parsing is disabled, and if so, skip parsing if (!this.settings.dataProcessing.appliedData.fields.ansiEscapeCodeParsingEnabled.value) { - this.addVisibleChar(char); + this.addVisibleChar(rxByte); // this.limitNumRows(); continue; } - if (char === '\x1B') { + if (rxByte === 0x1B) { // console.log('Start of escape sequence found!'); this.resetEscapeCodeParserState(); this.inAnsiEscapeCode = true; @@ -178,7 +195,7 @@ export default class Terminal { // character is to be displayed if (this.inAnsiEscapeCode) { // Add received char to partial escape code - this.partialEscapeCode += char; + this.partialEscapeCode += String.fromCharCode(rxByte); // console.log('partialEscapeCode=', this.partialEscapeCode); if (this.partialEscapeCode === '\x1B[') { this.inCSISequence = true; @@ -187,7 +204,8 @@ export default class Terminal { if (this.inCSISequence) { // console.log('In CSI sequence'); // Wait for alphabetic character to end CSI sequence - if (char.toUpperCase() !== char.toLowerCase()) { + const charStr = String.fromCharCode(rxByte); + if (charStr.toUpperCase() !== charStr.toLowerCase()) { // console.log( // 'Received terminating letter of CSI sequence! Escape code = ', // this.partialEscapeCode @@ -200,7 +218,25 @@ export default class Terminal { } else { // Not currently receiving ANSI escape code, // so send character to terminal(s) - this.addVisibleChar(char); + this.addVisibleChar(rxByte); + } + + // 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.settings.dataProcessing.appliedData.fields.maxEscapeCodeLengthChars.value; + if (this.inAnsiEscapeCode && this.partialEscapeCode.length === maxEscapeCodeLengthChars) { + console.log(`Reached max. length (${maxEscapeCodeLengthChars}) for partial escape code.`); + this.snackbar.sendToSnackbar( + `Reached max. length (${maxEscapeCodeLengthChars}) for partial escape code.`, + 'warning'); + // Remove the ESC byte, and then prepend the rest onto the data to be processed + // Got to shift them in backwards + for (let partialIdx = this.partialEscapeCode.length - 1; partialIdx >= 1; partialIdx -= 1) { + remainingData.unshift(this.partialEscapeCode[partialIdx].charCodeAt(0)); + } + this.resetEscapeCodeParserState(); + this.inIdleState = true; } } @@ -432,9 +468,9 @@ export default class Terminal { this.cursorPosition[1] -= numColsToLeftAdjusted; } - addVisibleChars(chars: string) { - for (let idx = 0; idx < chars.length; idx += 1) { - this.addVisibleChar(chars[idx]); + addVisibleChars(rxBytes: number[]) { + for (let idx = 0; idx < rxBytes.length; idx += 1) { + this.addVisibleChar(rxBytes[idx]); } } @@ -443,10 +479,15 @@ export default class Terminal { * Cursor is also incremented to next suitable position. * @param char Must be a single printable character only. */ - addVisibleChar(char: string) { + addVisibleChar(rxByte: number) { // assert(char.length === 1); const terminalChar = new TerminalChar(); - terminalChar.char = char; + + if (rxByte === 0x00) { + terminalChar.char = String.fromCharCode(0x2400); + } else { + terminalChar.char = String.fromCharCode(rxByte); + } // Calculate the foreground color CSS let foregroundColorCss = ''; @@ -512,8 +553,6 @@ export default class Terminal { clearData() { this.currentStyle = {}; - this.outputHtml = []; - this.outputHtml.push( ); this.cursorPosition = [0, 0]; this.terminalRows = []; @@ -622,7 +661,17 @@ export default class Terminal { } } + /** + * Use this to set whether the terminal is considered focused or not. If focused, the + * terminal will be given a glow border and the cursor will go solid. + * + * @param trueFalse True to set as focused. + */ setIsFocused(trueFalse: boolean) { + // Only let this be set if terminal is focusable + if (!this.isFocusable) { + return; + } this.isFocused = trueFalse; } } diff --git a/src/App.css b/src/view/App.css similarity index 100% rename from src/App.css rename to src/view/App.css diff --git a/src/AppView.module.css b/src/view/AppView.module.css similarity index 100% rename from src/AppView.module.css rename to src/view/AppView.module.css diff --git a/src/AppView.test.tsx b/src/view/AppView.test.tsx similarity index 88% rename from src/AppView.test.tsx rename to src/view/AppView.test.tsx index 9f0a39da..b3de7849 100644 --- a/src/AppView.test.tsx +++ b/src/view/AppView.test.tsx @@ -17,7 +17,7 @@ import { act, } from '@testing-library/react'; -import { App } from './model/App'; +import { App } from 'model/App'; import AppView from './AppView'; import { TextEncoder, TextDecoder } from 'util'; @@ -169,6 +169,11 @@ function checkExpectedAgainstActualDisplay( expectedDisplay: ExpectedTerminalChar[][], actualDisplay: Element ) { + + // Enable these two lines for easy debugging! + // console.log('expectedDisplay=', expectedDisplay); + // screen.debug(actualDisplay); + // Make sure there are the same number of actual rows as expected rows expect(actualDisplay.children.length).toBe(expectedDisplay.length); @@ -196,6 +201,10 @@ function checkExpectedAgainstActualDisplay( describe('App', () => { + //========================================================================== + // RX TESTS + //========================================================================== + it('should handle single RX char', async () => { let {app, writtenData} = await createAppWithMockSerialPort(); @@ -592,8 +601,62 @@ describe('App', () => { }); }); + it('escape code over max size should not lock up parser', async () => { + const app = new App(true); + render(); + + // ESC byte then 0-7, this is 9 bytes in all + let textToSend = '\x1B01234567'; + + const utf8EncodeText = new TextEncoder(); + await act(async () => { + app.parseRxData(utf8EncodeText.encode(`${textToSend}`)); + }); + + const terminalRows = screen.getByTestId('tx-rx-terminal-view').children[0] + .children[0]; + + // We haven't sent 10 bytes in the escape code yet, so nothing should + // be displayed on screen + let expectedDisplay: ExpectedTerminalChar[][] = [ + [ + new ExpectedTerminalChar({ char: ' ', classNames: 'cursorUnfocused' }), + ], + ]; + await waitFor(() => { + checkExpectedAgainstActualDisplay(expectedDisplay, terminalRows); + }); + + // Now send 10th byte! This should cause the parser to emit all the chars + // after the ESC byte to the screen + textToSend = '8'; + await act(async () => { + app.parseRxData(utf8EncodeText.encode(`${textToSend}`)); + }); + + // Check that all data is displayed correctly in terminal + expectedDisplay = [ + [ + new ExpectedTerminalChar({ char: '0' }), + new ExpectedTerminalChar({ char: '1' }), + new ExpectedTerminalChar({ char: '2' }), + new ExpectedTerminalChar({ char: '3' }), + new ExpectedTerminalChar({ char: '4' }), + new ExpectedTerminalChar({ char: '5' }), + new ExpectedTerminalChar({ char: '6' }), + new ExpectedTerminalChar({ char: '7' }), + new ExpectedTerminalChar({ char: '8' }), + new ExpectedTerminalChar({ char: ' ', classNames: 'cursorUnfocused' }), + ], + ]; + await waitFor(() => { + checkExpectedAgainstActualDisplay(expectedDisplay, terminalRows); + }); + }); + + //========================================================================== // TX TESTS - //========================================================== + //========================================================================== it('app should send basic A char', async () => { let {app, writtenData} = await createAppWithMockSerialPort(); @@ -621,4 +684,16 @@ describe('App', () => { expect(writtenData).toEqual(expectedData); }); }); + + it('app should send Horizontal Tab, HT (0x09) when Tab key is pressed', async () => { + let {app, writtenData} = await createAppWithMockSerialPort(); + + const terminal = screen.getByTestId('tx-rx-terminal-view'); + // Simulate a key press + fireEvent.keyDown(terminal, {key: 'Tab'}) + const expectedData = [ 0x09 ]; + await waitFor(() => { + expect(writtenData).toEqual(expectedData); + }); + }); }); diff --git a/src/AppView.tsx b/src/view/AppView.tsx similarity index 97% rename from src/AppView.tsx rename to src/view/AppView.tsx index 296478d5..3937df4d 100644 --- a/src/AppView.tsx +++ b/src/view/AppView.tsx @@ -1,5 +1,4 @@ -// import { MemoryRouter as Router, Routes, Route } from 'react-router-dom'; -import React, { useEffect, useRef } from 'react'; +import React from 'react'; import { observer } from 'mobx-react-lite'; import { @@ -22,12 +21,12 @@ import SettingsIcon from '@mui/icons-material/Settings'; import CssBaseline from '@mui/material/CssBaseline'; import { SnackbarProvider } from 'notistack'; -import { App, PortState, portStateToButtonProps } from './model/App'; +import { App, PortState, portStateToButtonProps } from '../model/App'; import './App.css'; import { DataViewConfiguration, dataViewConfigEnumToDisplayName, -} from './model/Settings/DataProcessingSettings'; +} from '../model/Settings/DataProcessingSettings'; import SettingsDialog from './Settings/SettingsView'; import TerminalView from './TerminalView'; import LogoImage from './logo192.png'; diff --git a/src/HomepageView.css b/src/view/Homepage/HomepageView.css similarity index 100% rename from src/HomepageView.css rename to src/view/Homepage/HomepageView.css diff --git a/src/HomepageView.tsx b/src/view/Homepage/HomepageView.tsx similarity index 96% rename from src/HomepageView.tsx rename to src/view/Homepage/HomepageView.tsx index 9ec74627..7faf6cc2 100644 --- a/src/HomepageView.tsx +++ b/src/view/Homepage/HomepageView.tsx @@ -4,9 +4,6 @@ import CssBaseline from "@mui/material/CssBaseline"; import { Box, Button, - Card, - CardActions, - CardContent, IconButton, Typography, } from "@mui/material"; @@ -149,7 +146,7 @@ export default observer((props: Props) => { Rich support for ANSI CSI colour codes, giving you ability to express information however you see fit! (e.g. colour errors red, warnings yellow).
- + Demonstration of ANSI escape codes in NinjaTerm.
@@ -167,7 +164,7 @@ export default observer((props: Props) => { Most of the time you want to see the most recent information printed to the screen. NinjaTerm has a "scroll lock" feature to allow for that. However, scrolling up allows you to break the "scroll lock" and focus on previous info (e.g. an error that occurred). NinjaTerm will adjust the scroll point to keep that information in view even if the scrollback buffer is full.
- + Demonstration of smart scrolling in NinjaTerm.
diff --git a/src/ansi-escape-code-colours.gif b/src/view/Homepage/ansi-escape-code-colours.gif similarity index 100% rename from src/ansi-escape-code-colours.gif rename to src/view/Homepage/ansi-escape-code-colours.gif diff --git a/src/github-readme-logo.png b/src/view/Homepage/github-readme-logo.png similarity index 100% rename from src/github-readme-logo.png rename to src/view/Homepage/github-readme-logo.png diff --git a/src/smart-scroll.gif b/src/view/Homepage/smart-scroll.gif similarity index 100% rename from src/smart-scroll.gif rename to src/view/Homepage/smart-scroll.gif diff --git a/src/Settings/DataProcessingView.tsx b/src/view/Settings/DataProcessingView.tsx similarity index 61% rename from src/Settings/DataProcessingView.tsx rename to src/view/Settings/DataProcessingView.tsx index 42d71536..1a43899b 100644 --- a/src/Settings/DataProcessingView.tsx +++ b/src/view/Settings/DataProcessingView.tsx @@ -14,11 +14,11 @@ import { } from '@mui/material'; import { observer } from 'mobx-react-lite'; -import { App } from '../model/App'; +import { App } from 'model/App'; import { DataViewConfiguration, dataViewConfigEnumToDisplayName, -} from '../model/Settings/DataProcessingSettings'; +} from 'model/Settings/DataProcessingSettings'; interface Props { appStore: App; @@ -29,11 +29,15 @@ function DataProcessingView(props: Props) { return ( - {/* ============================ ANSI ESCAPE CODE PARSING ENABLED =========================== */} + {/* =============================================================================== */} + {/* ANSI ESCAPE CODE PARSING ENABLED */} + {/* =============================================================================== */} + placement="top" + arrow + > - {/* ============================ DATA WIDTH =========================== */} - + {/* =============================================================================== */} + {/* MAX. ESCAPE CODE LENGTH */} + {/* =============================================================================== */} + + chars + ), + }} + value={ + appStore.settings.dataProcessing.visibleData.fields + .maxEscapeCodeLengthChars.value + } + onChange={(event: React.ChangeEvent) => { + appStore.settings.dataProcessing.onFieldChange( + event.target.name, + event.target.value + ); + }} + error={ + appStore.settings.dataProcessing.visibleData.fields + .maxEscapeCodeLengthChars.hasError + } + helperText={ + appStore.settings.dataProcessing.visibleData.fields + .maxEscapeCodeLengthChars.errorMsg + } + sx={{ marginBottom: '20px' }} + /> + + {/* =============================================================================== */} + {/* DATA WIDTH */} + {/* =============================================================================== */} + - {/* ============================ SCROLLBACK SIZE =========================== */} + {/* =============================================================================== */} + {/* SCROLLBACK BUFFER SIZE */} + {/* =============================================================================== */} + Increasing this will give you more history but decrease performance and increase memory usage. Must be a positive non-zero integer." + followCursor + arrow + > - {/* ============================ DATA VIEW CONFIGURATION =========================== */} + {/* =============================================================================== */} + {/* DATA VIEW CONFIGURATION */} + {/* =============================================================================== */} Data View Configuration @@ -155,12 +211,16 @@ function DataProcessingView(props: Props) { - {/* ============================ LOCAL TX ECHO =========================== */} + {/* =============================================================================== */} + {/* LOCAL TX ECHO */} + {/* =============================================================================== */} - {/* ============================ APPLY BUTTON =========================== */} + {/* =============================================================================== */} + {/* APPLY BUTTON */} + {/* =============================================================================== */}