diff --git a/src/main/cli.ts b/src/main/cli.ts index 9e676f3c..7052ed96 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -7,6 +7,7 @@ import { getBundledEnvInstallerPath, getBundledPythonEnvPath, getBundledPythonPath, + getLogFilePath, installCondaPackEnvironment, isBaseCondaEnv, isEnvInstalledByDesktopApp, @@ -16,11 +17,16 @@ import { import yargs from 'yargs/yargs'; import * as fs from 'fs'; import * as path from 'path'; -import { appData } from './config/appdata'; +import { appData, ApplicationData } from './config/appdata'; import { IEnvironmentType, IPythonEnvironment } from './tokens'; -import { SettingType, userSettings } from './config/settings'; +import { + SettingType, + UserSettings, + userSettings, + WorkspaceSettings +} from './config/settings'; import { Registry } from './registry'; -import { app } from 'electron'; +import { app, shell } from 'electron'; import { condaEnvPathForCondaExePath, getCondaChannels, @@ -188,6 +194,103 @@ export function parseCLIArgs(argv: string[]) { } } ) + .command( + 'config ', + 'Manage JupyterLab Desktop settings', + yargs => { + yargs + .positional('action', { + describe: 'Setting action', + choices: ['list', 'set', 'unset', 'open-file'], + default: 'list' + }) + .option('project', { + describe: 'Set config for project at current working directory', + type: 'boolean', + default: false + }) + .option('project-path', { + describe: 'Set / list config for project at specified path', + type: 'string' + }); + }, + async argv => { + console.log('Note: This is an experimental feature.'); + + const action = argv.action; + switch (action) { + case 'list': + handleConfigListCommand(argv); + break; + case 'set': + handleConfigSetCommand(argv); + break; + case 'unset': + handleConfigUnsetCommand(argv); + break; + case 'open-file': + handleConfigOpenFileCommand(argv); + break; + default: + console.log('Invalid input for "config" command.'); + break; + } + } + ) + .command( + 'appdata ', + 'Manage JupyterLab Desktop app data', + yargs => { + yargs.positional('action', { + describe: 'App data action', + choices: ['list', 'open-file'], + default: 'list' + }); + }, + async argv => { + console.log('Note: This is an experimental feature.'); + + const action = argv.action; + switch (action) { + case 'list': + handleAppDataListCommand(argv); + break; + case 'open-file': + handleAppDataOpenFileCommand(argv); + break; + default: + console.log('Invalid input for "appdata" command.'); + break; + } + } + ) + .command( + 'logs ', + 'Manage JupyterLab Desktop logs', + yargs => { + yargs.positional('action', { + describe: 'Logs action', + choices: ['show', 'open-file'], + default: 'show' + }); + }, + async argv => { + console.log('Note: This is an experimental feature.'); + + const action = argv.action; + switch (action) { + case 'show': + handleLogsShowCommand(argv); + break; + case 'open-file': + handleLogsOpenFileCommand(argv); + break; + default: + console.log('Invalid input for "logs" command.'); + break; + } + } + ) .parseAsync(); } @@ -816,6 +919,266 @@ export async function handleEnvSetSystemPythonPathCommand(argv: any) { userSettings.save(); } +function getProjectPathForConfigCommand(argv: any): string | undefined { + let projectPath = undefined; + if (argv.project || argv.projectPath) { + projectPath = argv.projectPath + ? path.resolve(argv.projectPath) + : process.cwd(); + if ( + argv.projectPath && + !(fs.existsSync(projectPath) && fs.statSync(projectPath).isDirectory()) + ) { + console.error(`Invalid project path! "${projectPath}"`); + process.exit(1); + } + } + + return projectPath; +} + +function handleConfigListCommand(argv: any) { + const listLines: string[] = []; + + const projectPath = argv.projectPath + ? path.resolve(argv.projectPath) + : process.cwd(); + + listLines.push('Project / Workspace settings'); + listLines.push('============================'); + listLines.push(`[Project path: ${projectPath}]`); + listLines.push( + `[Source file: ${WorkspaceSettings.getWorkspaceSettingsPath(projectPath)}]` + ); + listLines.push('\nSettings'); + listLines.push('========'); + + const wsSettings = new WorkspaceSettings(projectPath).settings; + const wsSettingKeys = Object.keys(wsSettings).sort(); + if (wsSettingKeys.length > 0) { + for (let key of wsSettingKeys) { + const value = wsSettings[key].value; + listLines.push(`${key}: ${JSON.stringify(value)}`); + } + } else { + listLines.push('No setting overrides found in project directory.'); + } + listLines.push('\n'); + + listLines.push('Global settings'); + listLines.push('==============='); + listLines.push(`[Source file: ${UserSettings.getUserSettingsPath()}]`); + listLines.push('\nSettings'); + listLines.push('========'); + + const settingKeys = Object.values(SettingType).sort(); + const settings = userSettings.settings; + + for (let key of settingKeys) { + const setting = settings[key]; + listLines.push( + `${key}: ${JSON.stringify(setting.value)} [${ + setting.differentThanDefault ? 'modified' : 'set to default' + }${setting.wsOverridable ? ', project overridable' : ''}]` + ); + } + + console.log(listLines.join('\n')); +} + +function handleConfigSetCommand(argv: any) { + const parseSetting = (): { key: string; value: string } => { + if (argv._.length !== 3) { + console.error(`Invalid setting. Use "set " format.`); + return { key: undefined, value: undefined }; + } + + let value; + + // boolean, arrays, objects + try { + value = JSON.parse(argv._[2]); + } catch (error) { + try { + // string without quotes + value = JSON.parse(`"${argv._[2]}"`); + } catch (error) { + console.error(error.message); + } + } + + return { key: argv._[1], value: value }; + }; + + const projectPath = getProjectPathForConfigCommand(argv); + + let key, value; + try { + const keyVal = parseSetting(); + key = keyVal.key; + value = keyVal.value; + } catch (error) { + console.error('Failed to parse setting!'); + return; + } + + if (key === undefined || value === undefined) { + console.error('Failed to parse key value pair!'); + return; + } + + if (!(key in SettingType)) { + console.error(`Invalid setting key! "${key}"`); + return; + } + + if (projectPath) { + const setting = userSettings.settings[key]; + if (!setting.wsOverridable) { + console.error(`Setting "${key}" is not overridable by project.`); + return; + } + + const wsSettings = new WorkspaceSettings(projectPath); + wsSettings.setValue(key as SettingType, value); + wsSettings.save(); + } else { + userSettings.setValue(key as SettingType, value); + userSettings.save(); + } + + console.log( + `${ + projectPath ? 'Project' : 'Global' + } setting "${key}" set to "${value}" successfully.` + ); +} + +function handleConfigUnsetCommand(argv: any) { + const parseKey = (): string => { + if (argv._.length !== 2) { + console.error(`Invalid setting. Use "unset " format.`); + return undefined; + } + + return argv._[1]; + }; + + const projectPath = getProjectPathForConfigCommand(argv); + + let key = parseKey(); + + if (!key) { + return; + } + + if (!(key in SettingType)) { + console.error(`Invalid setting key! "${key}"`); + return; + } + + if (projectPath) { + const setting = userSettings.settings[key]; + if (!setting.wsOverridable) { + console.error(`Setting "${key}" is not overridable by project.`); + return; + } + + const wsSettings = new WorkspaceSettings(projectPath); + wsSettings.unsetValue(key as SettingType); + wsSettings.save(); + } else { + userSettings.unsetValue(key as SettingType); + userSettings.save(); + } + + console.log( + `${projectPath ? 'Project' : 'Global'} setting "${key}" reset to ${ + projectPath ? 'global ' : '' + }default successfully.` + ); +} + +function handleConfigOpenFileCommand(argv: any) { + const projectPath = getProjectPathForConfigCommand(argv); + const settingsFilePath = projectPath + ? WorkspaceSettings.getWorkspaceSettingsPath(projectPath) + : UserSettings.getUserSettingsPath(); + + console.log(`Settings file path: ${settingsFilePath}`); + + if ( + !(fs.existsSync(settingsFilePath) && fs.statSync(settingsFilePath).isFile()) + ) { + console.log('Settings file does not exist!'); + return; + } + + shell.openPath(settingsFilePath); +} + +function handleAppDataListCommand(argv: any) { + const listLines: string[] = []; + + listLines.push('Application data'); + listLines.push('================'); + listLines.push(`[Source file: ${ApplicationData.getAppDataPath()}]`); + listLines.push('\nData'); + listLines.push('===='); + + const skippedKeys = new Set(['newsList']); + const appDataKeys = Object.keys(appData).sort(); + + for (let key of appDataKeys) { + if (key.startsWith('_') || skippedKeys.has(key)) { + continue; + } + const data = (appData as any)[key]; + listLines.push(`${key}: ${JSON.stringify(data)}`); + } + + console.log(listLines.join('\n')); +} + +function handleAppDataOpenFileCommand(argv: any) { + const appDataFilePath = ApplicationData.getAppDataPath(); + console.log(`App data file path: ${appDataFilePath}`); + + if ( + !(fs.existsSync(appDataFilePath) && fs.statSync(appDataFilePath).isFile()) + ) { + console.log('App data file does not exist!'); + return; + } + + shell.openPath(appDataFilePath); +} + +function handleLogsShowCommand(argv: any) { + const logFilePath = getLogFilePath(); + console.log(`Log file path: ${logFilePath}`); + + if (!(fs.existsSync(logFilePath) && fs.statSync(logFilePath).isFile())) { + console.log('Log file does not exist!'); + return; + } + + const logs = fs.readFileSync(logFilePath); + console.log(logs.toString()); +} + +function handleLogsOpenFileCommand(argv: any) { + const logFilePath = getLogFilePath(); + console.log(`Log file path: ${logFilePath}`); + + if (!(fs.existsSync(logFilePath) && fs.statSync(logFilePath).isFile())) { + console.log('Log file does not exist!'); + return; + } + + shell.openPath(logFilePath); +} + export async function launchCLIinEnvironment( envPath: string ): Promise { diff --git a/src/main/config/appdata.ts b/src/main/config/appdata.ts index 7995225e..6e993db6 100644 --- a/src/main/config/appdata.ts +++ b/src/main/config/appdata.ts @@ -54,7 +54,7 @@ export class ApplicationData { } read() { - const appDataPath = this._getAppDataPath(); + const appDataPath = ApplicationData.getAppDataPath(); if (!fs.existsSync(appDataPath)) { return; } @@ -176,7 +176,7 @@ export class ApplicationData { } save() { - const appDataPath = this._getAppDataPath(); + const appDataPath = ApplicationData.getAppDataPath(); const appDataJSON: { [key: string]: any } = {}; if (this.pythonPath !== '') { @@ -373,7 +373,7 @@ export class ApplicationData { return this._recentSessionsChanged; } - private _getAppDataPath(): string { + static getAppDataPath(): string { const userDataDir = getUserDataDir(); return path.join(userDataDir, 'app-data.json'); } diff --git a/src/main/config/settings.ts b/src/main/config/settings.ts index 1d5a344b..1eb31bd5 100644 --- a/src/main/config/settings.ts +++ b/src/main/config/settings.ts @@ -102,13 +102,17 @@ export class Setting { } get differentThanDefault(): boolean { - return this.value !== this._defaultValue; + return JSON.stringify(this.value) !== JSON.stringify(this._defaultValue); } get wsOverridable(): boolean { return this?._options?.wsOverridable; } + setToDefault() { + this._value = this._defaultValue; + } + private _defaultValue: T; private _value: T; private _valueSet = false; @@ -163,6 +167,15 @@ export class UserSettings { } } + static getUserSettingsPath(): string { + const userDataDir = getUserDataDir(); + return path.join(userDataDir, 'settings.json'); + } + + get settings() { + return this._settings; + } + getValue(setting: SettingType) { return this._settings[setting].value; } @@ -171,8 +184,12 @@ export class UserSettings { this._settings[setting].value = value; } + unsetValue(setting: SettingType) { + this._settings[setting].setToDefault(); + } + read() { - const userSettingsPath = this._getUserSettingsPath(); + const userSettingsPath = UserSettings.getUserSettingsPath(); if (!fs.existsSync(userSettingsPath)) { return; } @@ -188,7 +205,7 @@ export class UserSettings { } save() { - const userSettingsPath = this._getUserSettingsPath(); + const userSettingsPath = UserSettings.getUserSettingsPath(); const userSettings: { [key: string]: any } = {}; for (let key in SettingType) { @@ -207,11 +224,6 @@ export class UserSettings { ); } - private _getUserSettingsPath(): string { - const userDataDir = getUserDataDir(); - return path.join(userDataDir, 'settings.json'); - } - protected _settings: { [key: string]: Setting }; } @@ -223,6 +235,10 @@ export class WorkspaceSettings extends UserSettings { this.read(); } + get settings() { + return this._wsSettings; + } + getValue(setting: SettingType) { if (setting in this._wsSettings) { return this._wsSettings[setting].value; @@ -239,10 +255,16 @@ export class WorkspaceSettings extends UserSettings { this._wsSettings[setting].value = value; } + unsetValue(setting: SettingType) { + delete this._wsSettings[setting]; + } + read() { super.read(); - const wsSettingsPath = this._getWorkspaceSettingsPath(); + const wsSettingsPath = WorkspaceSettings.getWorkspaceSettingsPath( + this._workingDirectory + ); if (!fs.existsSync(wsSettingsPath)) { return; } @@ -261,7 +283,9 @@ export class WorkspaceSettings extends UserSettings { } save() { - const wsSettingsPath = this._getWorkspaceSettingsPath(); + const wsSettingsPath = WorkspaceSettings.getWorkspaceSettingsPath( + this._workingDirectory + ); const wsSettings: { [key: string]: any } = {}; for (let key in SettingType) { @@ -299,12 +323,8 @@ export class WorkspaceSettings extends UserSettings { return false; } - private _getWorkspaceSettingsPath(): string { - return path.join( - this._workingDirectory, - '.jupyter', - 'desktop-settings.json' - ); + static getWorkspaceSettingsPath(workingDirectory: string): string { + return path.join(workingDirectory, '.jupyter', 'desktop-settings.json'); } private _workingDirectory: string; diff --git a/src/main/main.ts b/src/main/main.ts index 2a6866e9..95c73698 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -224,7 +224,9 @@ function processArgs(): Promise { parseCLIArgs(process.argv.slice(isDevMode() ? 2 : 1)).then(value => { argv = value; if ( - ['--help', '--version', 'env'].find(arg => process.argv?.includes(arg)) + ['--help', '--version', 'env', 'config', 'appdata', 'logs'].find(arg => + process.argv?.includes(arg) + ) ) { app.quit(); return;