Skip to content

Commit

Permalink
Merge pull request #6 from pioneers/persistent-config
Browse files Browse the repository at this point in the history
Persistent config
  • Loading branch information
snowNnik authored Jul 28, 2024
2 parents 68f6842 + 0e8229c commit 2966b42
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 70 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ logs
pids
*.pid
*.seed
dawn-config.json

# Coverage directory used by tools like istanbul
coverage
Expand Down
117 changes: 117 additions & 0 deletions src/common/IpcEventTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type AppConsoleMessage from './AppConsoleMessage';

/**
* Data for the renderer-init event, sent when the renderer process has finished initializing and is
* 'ready-to-show'.
*/
export interface RendererInitData {
dawnVersion: string;
robotIPAddress: string;
robotSSHAddress: string;
fieldIPAddress: string;
fieldStationNumber: string;
}
/**
* Data for a specialization of the renderer-file-control event, sent when the main process wants to
* try saving the code and needs to ask the renderer process for the content of the editor.
*/
interface RendererFcPrmtSaveData {
type: 'promptSave';
forceDialog: boolean;
}
/**
* Data for a specialization of the renderer-file-control event, sent when the main process wants to
* try loading a file into the editor and needs to ask the renderer process if this is ok (if there
* are no unsaved changes).
*/
interface RendererFcPrmtLoadData {
type: 'promptLoad';
}
/**
* Data for a specialization of the renderer-file-control event, sent when the main process has
* successfully saved the code and the renderer should clear the dirty editor indicator.
*/
interface RendererFcSaveData {
type: 'didSave';
}
/**
* Data for a specialization of the renderer-file-control event, sent when the main process has
* successfully loaded code and the renderer should store the retrieved content in the editor.
*/
interface RendererFcOpenData {
type: 'didOpen';
content: string;
}
/**
* Data for a specialization of the renderer-file-control event, sent when the main process has
* successfully saved or loaded code and the renderer should update the path shown in the editor.
*/
interface RendererFcPathData {
type: 'didChangePath';
path: string;
}
/**
* Data for a specialization of the renderer-file-control event, sent when the main process detects
* external changes to the currently open file and the renderer should set the dirty editor
* indicator.
*/
interface RendererFcExtChangeData {
type: 'didExternalChange';
}
/**
* Data for the renderer-file-control event sent by the main process to request or submit
* information related to the code file and editor.
*/
export type RendererFileControlData =
| RendererFcPrmtSaveData
| RendererFcPrmtLoadData
| RendererFcSaveData
| RendererFcOpenData
| RendererFcPathData
| RendererFcExtChangeData;
/**
* Data for the renderer-post-console event sent by the main process to add a console message to the
* AppConsole.
*/
export type RendererPostConsoleData = AppConsoleMessage;
/**
* Data for the renderer-robot-update event when some info related to the robot or its connection
* changes.
*/
export interface RendererRobotUpdateData {
newRuntimeVersion?: string;
newRobotBatteryVoltage?: number;
newRobotLatencyMs?: number;
}

/**
* Data for a specialization of the main-file-control event, sent by the renderer to
* initiate/respond to a request to save the code.
*/
interface MainFcSaveData {
type: 'save';
forceDialog: boolean;
content: string;
}
/**
* Data for a specialization of the main-file-control event, sent by the renderer to
* initiate/authorize a request to load code into the editor.
*/
interface MainFcLoadData {
type: 'load';
}
/**
* Data for the main-file-control event sent by the renderer process to submit information related
* to the code file and editor.
*/
export type MainFileControlData = MainFcSaveData | MainFcLoadData;
/**
* Data for the main-quit event sent by the renderer both to authorize a request to quit and to send
* updated configuration data that should be saved before the program closes.
*/
export interface MainQuitData {
robotIPAddress: string;
robotSSHAddress: string;
fieldIPAddress: string;
fieldStationNumber: string;
}
52 changes: 52 additions & 0 deletions src/main/Config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Describes persistent configuration for Dawn.
*/
export default interface Config {
robotIPAddress: string;
robotSSHAddress: string;
fieldIPAddress: string;
fieldStationNumber: string;
}

/**
* Converts a template value to a Config by populating missing properties on a shallow copy of the
* template with defaults. If the template is not an object, returns a default Config. Non-Config
* properties on the template are preserved.
* @param template - the value to use as a template for the returned Config
* @returns A Config first populated by properties in the template, then by defaults for any missing
* properties.
*/
export function coerceToConfig(template: unknown): Config {
let config: Partial<Config>;
if (typeof template !== 'object' || !template) {
config = {};
} else {
// Don't mutate argument
config = { ...template };
}
if (
!('robotIPAddress' in config) ||
typeof config.robotIPAddress !== 'string'
) {
config.robotIPAddress = '192.168.0.100';
}
if (
!('robotSSHAddress' in config) ||
typeof config.robotSSHAddress !== 'string'
) {
config.robotSSHAddress = '192.168.0.100';
}
if (
!('fieldIPAddress' in config) ||
typeof config.fieldIPAddress !== 'string'
) {
config.fieldIPAddress = 'localhost';
}
if (
!('fieldStationNumber' in config) ||
typeof config.fieldStationNumber !== 'string'
) {
config.fieldStationNumber = '4';
}
return config as Config; // By now we're sure all the fields (and maybe some more) are set
}
114 changes: 83 additions & 31 deletions src/main/MainApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import type { BrowserWindow, FileFilter } from 'electron';
import fs from 'fs';
import { version as dawnVersion } from '../../package.json';
import AppConsoleMessage from '../common/AppConsoleMessage';
import type {
RendererInitData,
RendererFileControlData,
RendererPostConsoleData,
MainFileControlData,
MainQuitData,
} from '../common/IpcEventTypes';
import type Config from './Config';
import { coerceToConfig } from './Config';

/**
* Cooldown time in milliseconds to wait between sending didExternalChange messages to the renderer
Expand All @@ -16,6 +25,7 @@ const CODE_FILE_FILTERS: FileFilter[] = [
{ name: 'Python Files', extensions: ['py'] },
{ name: 'All Files', extensions: ['*'] },
];
const CONFIG_RELPATH = 'dawn-config.json';

/**
* Manages state owned by the main electron process.
Expand Down Expand Up @@ -49,6 +59,12 @@ export default class MainApp {
*/
#preventQuit: boolean;

/**
* Persistent configuration loaded when MainApp is constructed and saved when the main window is
* closed.
*/
#config: Config;

/**
* @param mainWindow - the BrowserWindow.
*/
Expand All @@ -65,19 +81,42 @@ export default class MainApp {
}
});
ipcMain.on('main-file-control', (_event, data) => {
if (data.type === 'save') {
this.#saveCodeFile(data.content as string, data.forceDialog as boolean);
} else if (data.type === 'load') {
const typedData = data as MainFileControlData;
if (typedData.type === 'save') {
this.#saveCodeFile(typedData.content, typedData.forceDialog);
} else if (typedData.type === 'load') {
this.#openCodeFile();
} else {
// eslint-disable-next-line no-console
console.error(`Unknown data.type for main-file-control ${data.type}`);
}
});
ipcMain.on('main-quit', () => {
ipcMain.on('main-quit', (_event, data) => {
const typedData = data as MainQuitData;
this.#config.robotIPAddress = typedData.robotIPAddress;
this.#config.robotSSHAddress = typedData.robotSSHAddress;
this.#config.fieldIPAddress = typedData.fieldIPAddress;
this.#config.fieldStationNumber = typedData.fieldStationNumber;
try {
fs.writeFileSync(CONFIG_RELPATH, JSON.stringify(this.#config));
} catch (e) {
// eslint-disable-next-line no-console
console.error(`Failed to write config on quit. ${String(e)}`);
}
this.#preventQuit = false;
this.#mainWindow.close();
});

try {
this.#config = coerceToConfig(
JSON.parse(
fs.readFileSync(CONFIG_RELPATH, {
encoding: 'utf8',
flag: 'r',
}),
),
);
} catch {
// Use all defaults if bad JSON or no config file
this.#config = coerceToConfig({});
}
}

/**
Expand All @@ -88,7 +127,15 @@ export default class MainApp {
onPresent() {
this.#watcher?.close();
this.#savePath = null;
this.#mainWindow.webContents.send('renderer-init', { dawnVersion });
// TODO: add better typing for IPC instead of just adding checks before each call
const data: RendererInitData = {
dawnVersion,
robotIPAddress: this.#config.robotIPAddress,
robotSSHAddress: this.#config.robotSSHAddress,
fieldIPAddress: this.#config.fieldIPAddress,
fieldStationNumber: this.#config.fieldStationNumber,
};
this.#mainWindow.webContents.send('renderer-init', data);
}

/**
Expand All @@ -99,20 +146,20 @@ export default class MainApp {
promptSaveCodeFile(forceDialog: boolean) {
// We need a round trip to the renderer because that's where the code in the editor actually
// lives
this.#mainWindow.webContents.send('renderer-file-control', {
const data: RendererFileControlData = {
type: 'promptSave',
forceDialog,
});
};
this.#mainWindow.webContents.send('renderer-file-control', data);
}

/**
* Requests that the renderer process start to load code from a file into the editor. The renderer
* may delay or ignore this request (e.g. if the file currently beind edited is dirty).
*/
promptLoadCodeFile() {
this.#mainWindow.webContents.send('renderer-file-control', {
type: 'promptLoad',
});
const data: RendererFileControlData = { type: 'promptLoad' };
this.#mainWindow.webContents.send('renderer-file-control', data);
}

/**
Expand All @@ -136,17 +183,14 @@ export default class MainApp {
{ encoding: 'utf8', flag: 'w' },
(err) => {
if (err) {
this.#mainWindow.webContents.send(
'renderer-post-console',
new AppConsoleMessage(
'dawn-err',
`Failed to save code to ${this.#savePath}. ${err}`,
),
const data: RendererPostConsoleData = new AppConsoleMessage(
'dawn-err',
`Failed to save code to ${this.#savePath}. ${err}`,
);
this.#mainWindow.webContents.send('renderer-post-console', data);
} else {
this.#mainWindow.webContents.send('renderer-file-control', {
type: 'didSave',
});
const data: RendererFileControlData = { type: 'didSave' };
this.#mainWindow.webContents.send('renderer-file-control', data);
}
this.#watchCodeFile();
},
Expand All @@ -160,13 +204,19 @@ export default class MainApp {
#openCodeFile() {
const success = this.#showCodePathDialog('load');
if (success) {
this.#mainWindow.webContents.send('renderer-file-control', {
type: 'didOpen',
content: fs.readFileSync(this.#savePath as string, {
try {
const content = fs.readFileSync(this.#savePath as string, {
encoding: 'utf8',
flag: 'r',
}),
});
});
const data: RendererFileControlData = {
type: 'didOpen',
content,
};
this.#mainWindow.webContents.send('renderer-file-control', data);
} catch {
// Don't care
}
}
}

Expand All @@ -191,10 +241,11 @@ export default class MainApp {
}
if (result && result.length) {
this.#savePath = typeof result === 'string' ? result : result[0];
this.#mainWindow.webContents.send('renderer-file-control', {
const data: RendererFileControlData = {
type: 'didChangePath',
path: this.#savePath,
});
};
this.#mainWindow.webContents.send('renderer-file-control', data);
if (mode === 'load') {
this.#watchCodeFile();
}
Expand All @@ -220,9 +271,10 @@ export default class MainApp {
setTimeout(() => {
this.#watchDebounce = true;
}, WATCH_DEBOUNCE_MS);
this.#mainWindow.webContents.send('renderer-file-control', {
const data: RendererFileControlData = {
type: 'didExternalChange',
});
};
this.#mainWindow.webContents.send('renderer-file-control', data);
}
},
);
Expand Down
Loading

0 comments on commit 2966b42

Please sign in to comment.