diff --git a/browser/src/Platform.ts b/browser/src/Platform.ts index b46bd126ba..5e079b50dd 100644 --- a/browser/src/Platform.ts +++ b/browser/src/Platform.ts @@ -8,11 +8,18 @@ export const isLinux = () => os.platform() === "linux" export const getUserHome = () => (isWindows() ? process.env["APPDATA"] : process.env["HOME"]) // tslint:disable-line no-string-literal -export const getLinkPath = () => (isMac() ? "/usr/local/bin/oni" : "") // TODO: windows + linux +export const getLinkPath = () => (isMac() ? "/usr/local/bin/oni" : "") // TODO: Linux export const isAddedToPath = () => { if (isMac()) { try { fs.lstatSync(getLinkPath()) + + const currentLinkPath = fs.readlinkSync(getLinkPath()) + + // Temporary guard to check if the old script has been linked to. + if (currentLinkPath.indexOf("cli/mac/oni.sh") === -1) { + return false + } } catch (_) { return false } @@ -21,14 +28,14 @@ export const isAddedToPath = () => { return false } -export const removeFromPath = () => (isMac() ? fs.unlinkSync(getLinkPath()) : false) // TODO: windows + other +export const removeFromPath = () => (isMac() ? fs.unlinkSync(getLinkPath()) : false) // TODO: Linux export const addToPath = async () => { if (isMac()) { const appDirectory = path.join(path.dirname(process.mainModule.filename), "..", "..") const options = { name: "Oni", icns: path.join(appDirectory, "Resources", "Oni.icns") } - const linkPath = path.join(appDirectory, "Resources", "app", "oni.sh") - await _runSudoCommand(`ln -s ${linkPath} ${getLinkPath()}`, options) + const linkPath = path.join(appDirectory, "Resources", "app", "cli", "mac", "oni.sh") + await _runSudoCommand(`ln -fs ${linkPath} ${getLinkPath()}`, options) } } diff --git a/build/BuildSetupTemplate.js b/build/BuildSetupTemplate.js index 845c5ff211..ee7f7d0c82 100644 --- a/build/BuildSetupTemplate.js +++ b/build/BuildSetupTemplate.js @@ -20,6 +20,7 @@ const packageJsonContents = fs.readFileSync(path.join(__dirname, "..", "package. const packageMeta = JSON.parse(packageJsonContents) const { version, name } = packageMeta const prodName = packageMeta.build.productName +const pathVariable = "{app}\\resources\\app\\cli\\windows\\" let buildFolderPrefix = os.arch() === "x32" ? "ia32-" : "" @@ -37,10 +38,11 @@ const valuesToReplace = { SourcePath: path.join(__dirname, "..", "dist", `win-${buildFolderPrefix}unpacked`, "*"), WizardImageFilePath: path.join(__dirname, "setup", "Oni_128.bmp"), WizardSmallImageFilePath: path.join(__dirname, "setup", "Oni_54.bmp"), + cliPath: pathVariable, } const addToEnv = ` -Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; Tasks: addtopath; Check: NeedsAddPath(ExpandConstant('{app}')) +Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};${pathVariable}"; Tasks: addtopath; Check: NeedsAddPath(ExpandConstant('${pathVariable}')) Root: HKCU; Subkey: "SOFTWARE\\Classes\\*\\shell\\${prodName}"; ValueType: expandsz; ValueName: ""; ValueData: "Open with ${prodName}"; Tasks: addToRightClickMenu; Flags: uninsdeletekey Root: HKCU; Subkey: "SOFTWARE\\Classes\\*\\shell\\${prodName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\\resources\\app\\images\\oni.ico"; Tasks: addToRightClickMenu diff --git a/build/setup.template.iss b/build/setup.template.iss index 74cf4d7b9c..71582d5bfa 100644 --- a/build/setup.template.iss +++ b/build/setup.template.iss @@ -48,5 +48,88 @@ begin Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0; end; +// http://stackoverflow.com/a/23838239/261019 +procedure Explode(var Dest: TArrayOfString; Text: String; Separator: String); +var + i, p: Integer; +begin + i := 0; + repeat + SetArrayLength(Dest, i+1); + p := Pos(Separator,Text); + if p > 0 then begin + Dest[i] := Copy(Text, 1, p-1); + Text := Copy(Text, p + Length(Separator), Length(Text)); + i := i + 1; + end else begin + Dest[i] := Text; + Text := ''; + end; + until Length(Text)=0; +end; + +// This update step checks for the old path variable ({app}) and removes it when installing if needed. +// This is a modified version of the UninstallStepChanged, taken from VSCode. +procedure CurStepChanged(CurUninstallStep: TSetupStep); +var + Path: string; + OldOniPath: string; + Parts: TArrayOfString; + NewPath: string; + i: Integer; +begin + if not CurUninstallStep = ssInstall then begin + exit; + end; + if not RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', Path) + then begin + exit; + end; + NewPath := ''; + OldOniPath := ExpandConstant('{app}') + Explode(Parts, Path, ';'); + for i:=0 to GetArrayLength(Parts)-1 do begin + if CompareText(Parts[i], OldOniPath) <> 0 then begin + NewPath := NewPath + Parts[i]; + + if i < GetArrayLength(Parts) - 1 then begin + NewPath := NewPath + ';'; + end; + end; + end; + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', NewPath); +end; + +// This is taken from the VSCode installer file. +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + Path: string; + OniPath: string; + Parts: TArrayOfString; + NewPath: string; + i: Integer; +begin + if not CurUninstallStep = usUninstall then begin + exit; + end; + if not RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', Path) + then begin + exit; + end; + NewPath := ''; + OniPath := ExpandConstant('{{cliPath}}') + Explode(Parts, Path, ';'); + for i:=0 to GetArrayLength(Parts)-1 do begin + if CompareText(Parts[i], OniPath) <> 0 then begin + NewPath := NewPath + Parts[i]; + + if i < GetArrayLength(Parts) - 1 then begin + NewPath := NewPath + ';'; + end; + end; + end; + RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', NewPath); +end; + [Registry] {{RegistryKey}} \ No newline at end of file diff --git a/cli/linux/oni.sh b/cli/linux/oni.sh new file mode 100755 index 0000000000..78a4bdeda1 --- /dev/null +++ b/cli/linux/oni.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Get the path to the currently running script: +self=$0 + +# Test if $self is a symlink +if [ -L "$self" ] ; then + # readlink returns the path to the file the link points to: + target=$(readlink "$self") +else + target=$self +fi + +ONI_PATH=$(dirname "$target") +ONI_EXECUTABLE="${ONI_PATH}/../../../../oni" +CLI_SCRIPT="${ONI_PATH}/../../lib/cli/src/cli.js" + +ELECTRON_RUN_AS_NODE=1 "${ONI_EXECUTABLE}" "$CLI_SCRIPT" "$*" +exit $? \ No newline at end of file diff --git a/cli/mac/oni.sh b/cli/mac/oni.sh new file mode 100755 index 0000000000..e0585d009a --- /dev/null +++ b/cli/mac/oni.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Get the path to the currently running script: +self=$0 + +# Test if $self is a symlink +if [ -L "$self" ] ; then + # readlink returns the path to the file the link points to: + target=$(readlink "$self") +else + target=$self +fi + +ONI_PATH=$(dirname "$target") +ONI_EXECUTABLE="${ONI_PATH}/../../../../MacOS/Oni" +CLI_SCRIPT="${ONI_PATH}/../../lib/cli/src/cli.js" + +ELECTRON_RUN_AS_NODE=1 "${ONI_EXECUTABLE}" "$CLI_SCRIPT" "$*" +exit $? \ No newline at end of file diff --git a/cli/oni b/cli/oni deleted file mode 100755 index 3655cec427..0000000000 --- a/cli/oni +++ /dev/null @@ -1,29 +0,0 @@ -#! /usr/bin/env node - -var path = require("path") -var spawn = require("child_process").spawn -var os = require("os") -var fs = require("fs") - -var processOptions = {} -processOptions.detached = true -processOptions.stdio = ["ignore", null, null] -processOptions.env = Object.assign({}, process.env, { "LOCAL_ONI": 1 }) - -var isWindows = os.platform() === "win32"; - -var args = process.argv.slice(2); -args.unshift(path.resolve(path.join(__dirname, "..", "lib", "main", "src", "main.js"))) - -// In Windows, directly call into the electron executable - otherwise we"ll end up with a floating node console -var nonWindowsElectronPath = path.join(__dirname, "..", "node_modules", ".bin", "electron") -var windowsElectronPath = path.join(__dirname, "..", "node_modules", "electron", "dist", "electron.exe") -var spawnProcess = isWindows ? windowsElectronPath : nonWindowsElectronPath - -var child = spawn(spawnProcess, args, processOptions) -child.unref() - -child.on("error", (err) => console.error(err)) -child.on("exit", (code) => process.exit(code)); - -process.exit(0) diff --git a/cli/src/cli.ts b/cli/src/cli.ts new file mode 100644 index 0000000000..083abd44b3 --- /dev/null +++ b/cli/src/cli.ts @@ -0,0 +1,82 @@ +import * as path from "path" + +import { CLIArgs, parseCLIProcessArgv } from "./cli_args" + +import { spawn } from "child_process" + +export async function main(cli_arguments: string[]): Promise { + // First, try to parse the CLI args and deal with them. + let args: CLIArgs + + try { + args = parseCLIProcessArgv(cli_arguments) + } catch (err) { + process.stdout.write(err.message) + throw err + } + + if (args.help || args.version) { + const version = require(path.join(__dirname, "..", "..", "..", "package.json")).version // tslint:disable-line no-var-requires + process.stdout.write("Oni: Modern Modal Editing - powered by Neovim\n") + process.stdout.write(` version: ${version}\n`) + process.stdout.write("\nUsage:\n oni [FILE]\t\tEdit file\n") + process.stdout.write("\nhttps://github.com/onivim/oni\n") + + return Promise.resolve(true) + } + + // Otherwise, was loaded normally, so launch Oni. + + const env = assign({}, process.env, { + ONI_CLI: "1", // Let Oni know it was started from the CLI + ELECTRON_NO_ATTACH_CONSOLE: "1", + }) + + delete env["ELECTRON_RUN_AS_NODE"] + + const options = { + detached: true, + env, + } + + const child = await spawn(process.execPath, cli_arguments.slice(2), options) + child.unref() + + child.on("close", code => { + if (code !== 0) { + throw Error(`Exit code was ${code}, not 0.`) + } else { + return Promise.resolve(true) + } + }) + + child.on("error", err => { + throw err + }) + + child.on("exit", code => { + if (code && code !== 0) { + throw Error(`Exit code was ${code}, not 0.`) + } else { + return Promise.resolve(true) + } + }) +} + +function assign(destination: any, ...sources: any[]): any { + sources.forEach(source => Object.keys(source).forEach(key => (destination[key] = source[key]))) + return destination +} + +function eventuallyExit(code: number): void { + setTimeout(() => process.exit(code), 0) +} + +main(process.argv) + .then(() => { + eventuallyExit(0) + }) + .then(null, err => { + console.error(err.message || err.stack || err) + eventuallyExit(1) + }) diff --git a/cli/src/cli_args.ts b/cli/src/cli_args.ts new file mode 100644 index 0000000000..2c91d1fe43 --- /dev/null +++ b/cli/src/cli_args.ts @@ -0,0 +1,27 @@ +import * as minimist from "minimist" + +export interface CLIArgs { + help?: boolean + version?: boolean + [arg: string]: any +} + +const options: minimist.Opts = { + boolean: ["help", "version"], + alias: { + help: "h", + version: "v", + }, +} + +export function parseArgs(args: string[]): CLIArgs { + return minimist(args, options) as CLIArgs +} + +// Skip the first 2 arguments as that is the location of the Electron binary, +// and the location of the cli.ts script. +export function parseCLIProcessArgv(processArgv: string[]): CLIArgs { + let [, , ...args] = processArgv + + return parseArgs(args) +} diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 0000000000..7d5dd781f7 --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "lib": ["dom", "es2017"], + "module": "commonjs", + "moduleResolution": "node", + "newLine": "LF", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "outDir": "../lib/cli", + "pretty": true, + "removeComments": true, + "rootDir": ".", + "skipLibCheck": true, + "strictNullChecks": false, + "suppressImplicitAnyIndexErrors": true, + "target": "es2015", + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/cli/windows/oni.cmd b/cli/windows/oni.cmd new file mode 100644 index 0000000000..bc34f0c153 --- /dev/null +++ b/cli/windows/oni.cmd @@ -0,0 +1,5 @@ +@echo off +setlocal +set ELECTRON_RUN_AS_NODE=1 +call "%~dp0..\..\..\..\Oni.exe" "%~dp0..\..\lib\cli\src\cli.js" %* +endlocal \ No newline at end of file diff --git a/main/src/main.ts b/main/src/main.ts index e4f4b86f70..82202ff2c0 100644 --- a/main/src/main.ts +++ b/main/src/main.ts @@ -1,4 +1,3 @@ -import * as minimist from "minimist" import * as path from "path" import { app, BrowserWindow, ipcMain, Menu } from "electron" @@ -23,16 +22,7 @@ if (isAutomation) { Log.info("Verbose flag set since running automation") } -// We want to check for the 'help' flag before initializing electron -const argv = minimist(process.argv.slice(1)) -const version = require(path.join(__dirname, "..", "..", "..", "package.json")).version // tslint:disable-line no-var-requires -if (argv.help || argv.h) { - process.stdout.write("ONI: Modern Modal Editing - powered by Neovim\n") - process.stdout.write(` version: ${version}\n`) - process.stdout.write("\nUsage:\n oni [FILE]\t\tEdit file\n") - process.stdout.write("\nhttps://github.com/onivim/oni\n") - process.exit(0) -} +// const argv = minimist(process.argv.slice(1)) interface IWindowState { bounds?: { diff --git a/oni.sh b/oni.sh deleted file mode 100755 index 69a2df56f4..0000000000 --- a/oni.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -if [ "$(uname)" == 'Darwin' ]; then - OS='Mac' -else - # TODO: Support Linux - echo "Your platform ($(uname -a)) is not supported." - exit 1 -fi - -# get the path to the currently running script: -self=$0 - -OPEN_DIRECTORY="$PWD" - -# test if $self is a symlink -if [ -L "$self" ] ; then - # readlink returns the path to the file the link points to: - target=$(readlink "$self") -else - target=$self -fi - -ONI_PATH=$(dirname "$target") - -FULL_ONI_PATH="$ONI_PATH/../../MacOS/Oni" - -ONI_CWD="$OPEN_DIRECTORY" open --new -a "$FULL_ONI_PATH" --args $* diff --git a/package.json b/package.json index 8f847b5c55..5a4ac12f93 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ ], "main": "./lib/main/src/main.js", "bin": { - "oni": "./cli/oni", - "oni-vim": "./cli/oni" + "oni": "./lib/cli/src/cli.js", + "oni-vim": "./lib/cli/src/cli.js" }, "build": { "asar": false, @@ -782,11 +782,12 @@ "scripts": { "precommit": "pretty-quick --staged", "prepush": "yarn run build && yarn run lint", - "build": "yarn run build:browser && yarn run build:webview_preload && yarn run build:main && yarn run build:plugins", + "build": "yarn run build:browser && yarn run build:webview_preload && yarn run build:main && yarn run build:plugins && yarn run build:cli", "build-debug": "yarn run build:browser-debug && yarn run build:main && yarn run build:plugins", "build:browser": "webpack --config browser/webpack.production.config.js", "build:browser-debug": "webpack --config browser/webpack.debug.config.js", "build:main": "cd main && tsc -p tsconfig.json", + "build:cli": "cd cli && tsc -p tsconfig.json", "build:plugins": "yarn run build:plugin:oni-plugin-typescript && yarn run build:plugin:oni-plugin-markdown-preview", "build:plugin:oni-plugin-typescript": "cd vim/core/oni-plugin-typescript && yarn run build", "build:plugin:oni-plugin-markdown-preview": "cd extensions/oni-plugin-markdown-preview && yarn run build", @@ -815,10 +816,12 @@ "upload:dist": "node build/script/UploadDistributionBuildsToAzure", "fix-lint": "yarn run fix-lint:browser && yarn run fix-lint:main && yarn run fix-lint:test", "fix-lint:browser": "tslint --fix --project browser/tsconfig.json --exclude **/node_modules/**/* --config tslint.json && tslint --fix --project vim/core/oni-plugin-typescript/tsconfig.json --config tslint.json && tslint --fix --project extensions/oni-plugin-markdown-preview/tsconfig.json --config tslint.json", + "fix-lint:cli": "tslint --fix --project cli/tsconfig.json --config tslint.json", "fix-lint:main": "tslint --fix --project main/tsconfig.json --config tslint.json", "fix-lint:test": "tslint --fix --project test/tsconfig.json --config tslint.json", "lint": "yarn run lint:browser && yarn run lint:main && yarn run lint:test", "lint:browser": "tslint --project browser/tsconfig.json --config tslint.json && tslint --project vim/core/oni-plugin-typescript/tsconfig.json --config tslint.json && tslint --project extensions/oni-plugin-markdown-preview/tsconfig.json --config tslint.json", + "lint:cli": "tslint --project cli/tsconfig.json --config tslint.json", "lint:main": "tslint --project main/tsconfig.json --config tslint.json", "lint:test": "tslint --project test/tsconfig.json --config tslint.json && tslint vim/core/oni-plugin-typescript/src/**/*.ts && tslint extensions/oni-plugin-markdown-preview/src/**/*.ts", "pack": "cross-env ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=1 build --publish never",