├── .editorconfig ├── .gitignore ├── .release.json ├── .travis.yml ├── LICENSE ├── README.md ├── example_async.js ├── example_sync.js ├── main ├── errors │ ├── CreateFileError.d.ts │ ├── CreateFileError.js │ ├── LaunchEditorError.d.ts │ ├── LaunchEditorError.js │ ├── ReadFileError.d.ts │ ├── ReadFileError.js │ ├── RemoveFileError.d.ts │ └── RemoveFileError.js ├── index.d.ts └── index.js ├── package-lock.json ├── package.json ├── src ├── errors │ ├── CreateFileError.ts │ ├── LaunchEditorError.ts │ ├── ReadFileError.ts │ └── RemoveFileError.ts └── index.ts ├── test └── spec │ └── main.spec.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .idea -------------------------------------------------------------------------------- /.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "files_to_commit": ["main/**/*"], 3 | "pre_commit_commands": [ 4 | "npm run lint", 5 | "npm run compile", 6 | "npm test" 7 | ], 8 | "post_complete_commands": [ 9 | "npm publish" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | arch: 3 | - amd64 4 | - ppc64le 5 | node_js: 6 | - 12 7 | - 11 8 | - 10 9 | - 9 10 | - 8 11 | - 7 12 | - 6 13 | - 5 14 | - 4 15 | language: node_js 16 | script: npm test 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kevin Gravier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # External Editor 2 | 3 | [![ExternalEditor on Travis CI](https://img.shields.io/travis/mrkmg/node-external-editor.svg?style=flat-square)](https://travis-ci.org/mrkmg/node-external-editor/branches) 4 | [![ExternalEditor on NPM](https://img.shields.io/npm/v/external-editor.svg?style=flat-square)](https://www.npmjs.com/package/external-editor) 5 | [![ExternalEditor uses the MIT](https://img.shields.io/npm/l/external-editor.svg?style=flat-square)](https://opensource.org/licenses/MIT) 6 | 7 | 8 | A node module to edit a string with a users preferred text editor using $VISUAL or $ENVIRONMENT. 9 | 10 | Version: 3.1.0 11 | 12 | As of version 3.0.0, the minimum version of node supported is 4. 13 | 14 | ## Install 15 | 16 | `npm install external-editor --save` 17 | 18 | ## Usage 19 | 20 | A simple example using the `.edit` convenience method 21 | 22 | import {edit} from "external-editor"; 23 | const data = edit('\n\n# Please write your text above'); 24 | console.log(data); 25 | 26 | A full featured example 27 | 28 | import {ExternalEditor, CreateFileError, ReadFileError, RemoveFileError} from "external-editor" 29 | 30 | try { 31 | const editor = new ExternalEditor(); 32 | const text = editor.run() // the text is also available in editor.text 33 | 34 | if (editor.last_exit_status !== 0) { 35 | console.log("The editor exited with a non-zero code"); 36 | } 37 | } catch (err) { 38 | if (err instanceOf CreateFileError) { 39 | console.log('Failed to create the temporary file'); 40 | } else if (err instanceOf ReadFileError) { 41 | console.log('Failed to read the temporary file'); 42 | } else if (err instanceOf LaunchEditorError) { 43 | console.log('Failed to launch your editor'); 44 | } else { 45 | throw err; 46 | } 47 | } 48 | 49 | // Do things with the text 50 | 51 | // Eventually call the cleanup to remove the temporary file 52 | try { 53 | editor.cleanup(); 54 | } catch (err) { 55 | if (err instanceOf RemoveFileError) { 56 | console.log('Failed to remove the temporary file'); 57 | } else { 58 | throw err 59 | } 60 | } 61 | 62 | 63 | #### API 64 | **Convenience Methods** 65 | 66 | - `edit(text, config)` 67 | - `text` (string) *Optional* Defaults to empty string 68 | - `config` (Config) *Optional* Options for temporary file creation 69 | - **Returns** (string) The contents of the file 70 | - Could throw `CreateFileError`, `ReadFileError`, or `LaunchEditorError`, or `RemoveFileError` 71 | - `editAsync(text, callback, config)` 72 | - `text` (string) *Optional* Defaults to empty string 73 | - `callback` (function (error, text)) 74 | - `error` could be of type `CreateFileError`, `ReadFileError`, or `LaunchEditorError`, or `RemoveFileError` 75 | - `text`(string) The contents of the file 76 | - `config` (Config) *Optional* Options for temporary file creation 77 | 78 | 79 | **Errors** 80 | 81 | - `CreateFileError` Error thrown if the temporary file could not be created. 82 | - `ReadFileError` Error thrown if the temporary file could not be read. 83 | - `RemoveFileError` Error thrown if the temporary file could not be removed during cleanup. 84 | - `LaunchEditorError` Error thrown if the editor could not be launched. 85 | 86 | **External Editor Public Methods** 87 | 88 | - `new ExternalEditor(text, config)` 89 | - `text` (string) *Optional* Defaults to empty string 90 | - `config` (Config) *Optional* Options for temporary file creation 91 | - Could throw `CreateFileError` 92 | - `run()` Launches the editor. 93 | - **Returns** (string) The contents of the file 94 | - Could throw `LaunchEditorError` or `ReadFileError` 95 | - `runAsync(callback)` Launches the editor in an async way 96 | - `callback` (function (error, text)) 97 | - `error` could be of type `ReadFileError` or `LaunchEditorError` 98 | - `text`(string) The contents of the file 99 | - `cleanup()` Removes the temporary file. 100 | - Could throw `RemoveFileError` 101 | 102 | **External Editor Public Properties** 103 | 104 | - `text` (string) *readonly* The text in the temporary file. 105 | - `editor.bin` (string) The editor determined from the environment. 106 | - `editor.args` (array) Default arguments for the bin 107 | - `tempFile` (string) Path to temporary file. Can be changed, but be careful as the temporary file probably already 108 | exists and would need be removed manually. 109 | - `lastExitStatus` (number) The last exit code emitted from the editor. 110 | 111 | **Config Options** 112 | 113 | - `prefix` (string) *Optional* A prefix for the file name. 114 | - `postfix` (string; *Optional* A postfix for the file name. Useful if you want to provide an extension. 115 | - `mode` (number) *Optional* Which mode to create the file with. e.g. 644 116 | - `template` (string) *Optional* A template for the filename. See [tmp](https://www.npmjs.com/package/tmp). 117 | - `dir` (string) *Optional* Which path to store the file. 118 | 119 | ## Errors 120 | 121 | All errors have a simple message explaining what went wrong. They all also have an `originalError` property containing 122 | the original error thrown for debugging purposes. 123 | 124 | ## Why Synchronous? 125 | 126 | Everything is synchronous to make sure the editor has complete control of the stdin and stdout. Testing has shown 127 | async launching of the editor can lead to issues when using readline or other packages which try to read from stdin or 128 | write to stdout. Seeing as this will be used in an interactive CLI environment, I made the decision to force the package 129 | to be synchronous. If you know a reliable way to force all stdin and stdout to be limited only to the child_process, 130 | please submit a PR. 131 | 132 | If async is really needed, you can use `editAsync` or `runAsync`. If you are using readline or have anything else 133 | listening to the stdin or you write to stdout, you will most likely have problem, so make sure to remove any other 134 | listeners on stdin, stdout, or stderr. 135 | 136 | ## Demo 137 | 138 | [![asciicast](https://asciinema.org/a/a1qh9lypbe65mj0ivfuoslz2s.png)](https://asciinema.org/a/a1qh9lypbe65mj0ivfuoslz2s) 139 | 140 | ## Breaking Changes from v2 to v3 141 | 142 | - NodeJS 0.12 support dropped. 143 | - Switched to named imports. 144 | - All "snake_cased" variables and properties are now "camelCased". 145 | - `ExternalEditor.temp_file` is now `ExternalEditor.tempFile`. 146 | - `ExternalEditor.last_exit_status` is now `ExternalEditor.lastExitStatus`. 147 | - `Error.original_error` is now `Error.originalError`. 148 | 149 | ## License 150 | 151 | The MIT License (MIT) 152 | 153 | Copyright (c) 2016-2018 Kevin Gravier 154 | 155 | Permission is hereby granted, free of charge, to any person obtaining a copy 156 | of this software and associated documentation files (the "Software"), to deal 157 | in the Software without restriction, including without limitation the rights 158 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 159 | copies of the Software, and to permit persons to whom the Software is 160 | furnished to do so, subject to the following conditions: 161 | 162 | The above copyright notice and this permission notice shall be included in all 163 | copies or substantial portions of the Software. 164 | 165 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 166 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 167 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 168 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 169 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 170 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 171 | SOFTWARE. 172 | -------------------------------------------------------------------------------- /example_async.js: -------------------------------------------------------------------------------- 1 | var ExternalEditor = require('./main').ExternalEditor; 2 | var readline = require('readline'); 3 | 4 | var rl = readline.createInterface({ 5 | input: process.stdin, 6 | output: null 7 | }); 8 | 9 | var message = '\n\n# Please Write a message\n# Any line starting with # is ignored'; 10 | 11 | process.stdout.write('Please write a message. (press enter to launch your preferred editor)'); 12 | 13 | editor = new ExternalEditor(message); 14 | 15 | rl.on('line', function () { 16 | try { 17 | rl.pause(); 18 | editor.runAsync(function (error, response) 19 | { 20 | if (error) { 21 | process.stdout.write(error.message); 22 | process.exit(1); 23 | } 24 | if (response.length === 0) { 25 | readline.moveCursor(process.stdout, 0, -1); 26 | process.stdout.write('Your message was empty, please try again. (press enter to launch your preferred editor)'); 27 | rl.resume(); 28 | } else { 29 | process.stdout.write('Your Message:\n'); 30 | process.stdout.write(response); 31 | process.stdout.write('\n'); 32 | rl.close(); 33 | } 34 | }); 35 | } catch (err) { 36 | process.stderr.write(err.message); 37 | process.stdout.write('\n'); 38 | rl.close(); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /example_sync.js: -------------------------------------------------------------------------------- 1 | var ExternalEditor = require('./main').ExternalEditor; 2 | var readline = require('readline'); 3 | 4 | var rl = readline.createInterface({ 5 | input: process.stdin, 6 | output: null 7 | }); 8 | 9 | var message = '\n\n# Please Write a message\n# Any line starting with # is ignored'; 10 | 11 | process.stdout.write('Please write a message. (press enter to launch your preferred editor)'); 12 | 13 | editor = new ExternalEditor(message); 14 | 15 | rl.on('line', function () { 16 | try { 17 | // Get response, remove all lines starting with #, remove any trailing newlines. 18 | var response = editor.run().replace(/^#.*\n?/gm, '').replace(/\n+$/g, '').trim(); 19 | 20 | if (editor.lastExitStatus !== 0) { 21 | process.stderr.write("WARN: The editor exited with a non-zero status\n\n") 22 | } 23 | 24 | if (response.length === 0) { 25 | readline.moveCursor(process.stdout, 0, -1); 26 | process.stdout.write('Your message was empty, please try again. (press enter to launch your preferred editor)'); 27 | } else { 28 | process.stdout.write('Your Message:\n'); 29 | process.stdout.write(response); 30 | process.stdout.write('\n'); 31 | rl.close(); 32 | } 33 | } catch (err) { 34 | process.stderr.write(err.message); 35 | process.stdout.write('\n'); 36 | rl.close(); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /main/errors/CreateFileError.d.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node External Editor 3 | * 4 | * Kevin Gravier 5 | * MIT 2018 6 | */ 7 | export declare class CreateFileError extends Error { 8 | originalError: Error; 9 | constructor(originalError: Error); 10 | } 11 | -------------------------------------------------------------------------------- /main/errors/CreateFileError.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*** 3 | * Node External Editor 4 | * 5 | * Kevin Gravier 6 | * MIT 2018 7 | */ 8 | var __extends = (this && this.__extends) || (function () { 9 | var extendStatics = function (d, b) { 10 | extendStatics = Object.setPrototypeOf || 11 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 12 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 13 | return extendStatics(d, b); 14 | }; 15 | return function (d, b) { 16 | extendStatics(d, b); 17 | function __() { this.constructor = d; } 18 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 19 | }; 20 | })(); 21 | Object.defineProperty(exports, "__esModule", { value: true }); 22 | var CreateFileError = /** @class */ (function (_super) { 23 | __extends(CreateFileError, _super); 24 | function CreateFileError(originalError) { 25 | var _newTarget = this.constructor; 26 | var _this = _super.call(this, "Failed to create temporary file for editor") || this; 27 | _this.originalError = originalError; 28 | var proto = _newTarget.prototype; 29 | if (Object.setPrototypeOf) { 30 | Object.setPrototypeOf(_this, proto); 31 | } 32 | else { 33 | _this.__proto__ = _newTarget.prototype; 34 | } 35 | return _this; 36 | } 37 | return CreateFileError; 38 | }(Error)); 39 | exports.CreateFileError = CreateFileError; 40 | -------------------------------------------------------------------------------- /main/errors/LaunchEditorError.d.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node External Editor 3 | * 4 | * Kevin Gravier 5 | * MIT 2018 6 | */ 7 | export declare class LaunchEditorError extends Error { 8 | originalError: Error; 9 | constructor(originalError: Error); 10 | } 11 | -------------------------------------------------------------------------------- /main/errors/LaunchEditorError.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*** 3 | * Node External Editor 4 | * 5 | * Kevin Gravier 6 | * MIT 2018 7 | */ 8 | var __extends = (this && this.__extends) || (function () { 9 | var extendStatics = function (d, b) { 10 | extendStatics = Object.setPrototypeOf || 11 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 12 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 13 | return extendStatics(d, b); 14 | }; 15 | return function (d, b) { 16 | extendStatics(d, b); 17 | function __() { this.constructor = d; } 18 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 19 | }; 20 | })(); 21 | Object.defineProperty(exports, "__esModule", { value: true }); 22 | var LaunchEditorError = /** @class */ (function (_super) { 23 | __extends(LaunchEditorError, _super); 24 | function LaunchEditorError(originalError) { 25 | var _newTarget = this.constructor; 26 | var _this = _super.call(this, "Failed launch editor") || this; 27 | _this.originalError = originalError; 28 | var proto = _newTarget.prototype; 29 | if (Object.setPrototypeOf) { 30 | Object.setPrototypeOf(_this, proto); 31 | } 32 | else { 33 | _this.__proto__ = _newTarget.prototype; 34 | } 35 | return _this; 36 | } 37 | return LaunchEditorError; 38 | }(Error)); 39 | exports.LaunchEditorError = LaunchEditorError; 40 | -------------------------------------------------------------------------------- /main/errors/ReadFileError.d.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node External Editor 3 | * 4 | * Kevin Gravier 5 | * MIT 2018 6 | */ 7 | export declare class ReadFileError extends Error { 8 | originalError: Error; 9 | constructor(originalError: Error); 10 | } 11 | -------------------------------------------------------------------------------- /main/errors/ReadFileError.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*** 3 | * Node External Editor 4 | * 5 | * Kevin Gravier 6 | * MIT 2018 7 | */ 8 | var __extends = (this && this.__extends) || (function () { 9 | var extendStatics = function (d, b) { 10 | extendStatics = Object.setPrototypeOf || 11 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 12 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 13 | return extendStatics(d, b); 14 | }; 15 | return function (d, b) { 16 | extendStatics(d, b); 17 | function __() { this.constructor = d; } 18 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 19 | }; 20 | })(); 21 | Object.defineProperty(exports, "__esModule", { value: true }); 22 | var ReadFileError = /** @class */ (function (_super) { 23 | __extends(ReadFileError, _super); 24 | function ReadFileError(originalError) { 25 | var _newTarget = this.constructor; 26 | var _this = _super.call(this, "Failed to read temporary file") || this; 27 | _this.originalError = originalError; 28 | var proto = _newTarget.prototype; 29 | if (Object.setPrototypeOf) { 30 | Object.setPrototypeOf(_this, proto); 31 | } 32 | else { 33 | _this.__proto__ = _newTarget.prototype; 34 | } 35 | return _this; 36 | } 37 | return ReadFileError; 38 | }(Error)); 39 | exports.ReadFileError = ReadFileError; 40 | -------------------------------------------------------------------------------- /main/errors/RemoveFileError.d.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node External Editor 3 | * 4 | * Kevin Gravier 5 | * MIT 2018 6 | */ 7 | export declare class RemoveFileError extends Error { 8 | originalError: Error; 9 | constructor(originalError: Error); 10 | } 11 | -------------------------------------------------------------------------------- /main/errors/RemoveFileError.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*** 3 | * Node External Editor 4 | * 5 | * Kevin Gravier 6 | * MIT 2018 7 | */ 8 | var __extends = (this && this.__extends) || (function () { 9 | var extendStatics = function (d, b) { 10 | extendStatics = Object.setPrototypeOf || 11 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 12 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 13 | return extendStatics(d, b); 14 | }; 15 | return function (d, b) { 16 | extendStatics(d, b); 17 | function __() { this.constructor = d; } 18 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 19 | }; 20 | })(); 21 | Object.defineProperty(exports, "__esModule", { value: true }); 22 | var RemoveFileError = /** @class */ (function (_super) { 23 | __extends(RemoveFileError, _super); 24 | function RemoveFileError(originalError) { 25 | var _newTarget = this.constructor; 26 | var _this = _super.call(this, "Failed to cleanup temporary file") || this; 27 | _this.originalError = originalError; 28 | var proto = _newTarget.prototype; 29 | if (Object.setPrototypeOf) { 30 | Object.setPrototypeOf(_this, proto); 31 | } 32 | else { 33 | _this.__proto__ = _newTarget.prototype; 34 | } 35 | return _this; 36 | } 37 | return RemoveFileError; 38 | }(Error)); 39 | exports.RemoveFileError = RemoveFileError; 40 | -------------------------------------------------------------------------------- /main/index.d.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node External Editor 3 | * 4 | * Kevin Gravier 5 | * MIT 2019 6 | */ 7 | import { CreateFileError } from "./errors/CreateFileError"; 8 | import { LaunchEditorError } from "./errors/LaunchEditorError"; 9 | import { ReadFileError } from "./errors/ReadFileError"; 10 | import { RemoveFileError } from "./errors/RemoveFileError"; 11 | export interface IEditorParams { 12 | args: string[]; 13 | bin: string; 14 | } 15 | export interface IFileOptions { 16 | prefix?: string; 17 | postfix?: string; 18 | mode?: number; 19 | template?: string; 20 | dir?: string; 21 | } 22 | export declare type StringCallback = (err: Error, result: string) => void; 23 | export declare type VoidCallback = () => void; 24 | export { CreateFileError, LaunchEditorError, ReadFileError, RemoveFileError }; 25 | export declare function edit(text?: string, fileOptions?: IFileOptions): string; 26 | export declare function editAsync(text: string, callback: StringCallback, fileOptions?: IFileOptions): void; 27 | export declare class ExternalEditor { 28 | private static splitStringBySpace; 29 | text: string; 30 | tempFile: string; 31 | editor: IEditorParams; 32 | lastExitStatus: number; 33 | private fileOptions; 34 | readonly temp_file: string; 35 | readonly last_exit_status: number; 36 | constructor(text?: string, fileOptions?: IFileOptions); 37 | run(): string; 38 | runAsync(callback: StringCallback): void; 39 | cleanup(): void; 40 | private determineEditor; 41 | private createTemporaryFile; 42 | private readTemporaryFile; 43 | private removeTemporaryFile; 44 | private launchEditor; 45 | private launchEditorAsync; 46 | } 47 | -------------------------------------------------------------------------------- /main/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*** 3 | * Node External Editor 4 | * 5 | * Kevin Gravier 6 | * MIT 2019 7 | */ 8 | Object.defineProperty(exports, "__esModule", { value: true }); 9 | var chardet_1 = require("chardet"); 10 | var child_process_1 = require("child_process"); 11 | var fs_1 = require("fs"); 12 | var iconv_lite_1 = require("iconv-lite"); 13 | var tmp_1 = require("tmp"); 14 | var CreateFileError_1 = require("./errors/CreateFileError"); 15 | exports.CreateFileError = CreateFileError_1.CreateFileError; 16 | var LaunchEditorError_1 = require("./errors/LaunchEditorError"); 17 | exports.LaunchEditorError = LaunchEditorError_1.LaunchEditorError; 18 | var ReadFileError_1 = require("./errors/ReadFileError"); 19 | exports.ReadFileError = ReadFileError_1.ReadFileError; 20 | var RemoveFileError_1 = require("./errors/RemoveFileError"); 21 | exports.RemoveFileError = RemoveFileError_1.RemoveFileError; 22 | function edit(text, fileOptions) { 23 | if (text === void 0) { text = ""; } 24 | var editor = new ExternalEditor(text, fileOptions); 25 | editor.run(); 26 | editor.cleanup(); 27 | return editor.text; 28 | } 29 | exports.edit = edit; 30 | function editAsync(text, callback, fileOptions) { 31 | if (text === void 0) { text = ""; } 32 | var editor = new ExternalEditor(text, fileOptions); 33 | editor.runAsync(function (err, result) { 34 | if (err) { 35 | setImmediate(callback, err, null); 36 | } 37 | else { 38 | try { 39 | editor.cleanup(); 40 | setImmediate(callback, null, result); 41 | } 42 | catch (cleanupError) { 43 | setImmediate(callback, cleanupError, null); 44 | } 45 | } 46 | }); 47 | } 48 | exports.editAsync = editAsync; 49 | var ExternalEditor = /** @class */ (function () { 50 | function ExternalEditor(text, fileOptions) { 51 | if (text === void 0) { text = ""; } 52 | this.text = ""; 53 | this.fileOptions = {}; 54 | this.text = text; 55 | if (fileOptions) { 56 | this.fileOptions = fileOptions; 57 | } 58 | this.determineEditor(); 59 | this.createTemporaryFile(); 60 | } 61 | ExternalEditor.splitStringBySpace = function (str) { 62 | var pieces = []; 63 | var currentString = ""; 64 | for (var strIndex = 0; strIndex < str.length; strIndex++) { 65 | var currentLetter = str[strIndex]; 66 | if (strIndex > 0 && currentLetter === " " && str[strIndex - 1] !== "\\" && currentString.length > 0) { 67 | pieces.push(currentString); 68 | currentString = ""; 69 | } 70 | else { 71 | currentString += currentLetter; 72 | } 73 | } 74 | if (currentString.length > 0) { 75 | pieces.push(currentString); 76 | } 77 | return pieces; 78 | }; 79 | Object.defineProperty(ExternalEditor.prototype, "temp_file", { 80 | get: function () { 81 | console.log("DEPRECATED: temp_file. Use tempFile moving forward."); 82 | return this.tempFile; 83 | }, 84 | enumerable: true, 85 | configurable: true 86 | }); 87 | Object.defineProperty(ExternalEditor.prototype, "last_exit_status", { 88 | get: function () { 89 | console.log("DEPRECATED: last_exit_status. Use lastExitStatus moving forward."); 90 | return this.lastExitStatus; 91 | }, 92 | enumerable: true, 93 | configurable: true 94 | }); 95 | ExternalEditor.prototype.run = function () { 96 | this.launchEditor(); 97 | this.readTemporaryFile(); 98 | return this.text; 99 | }; 100 | ExternalEditor.prototype.runAsync = function (callback) { 101 | var _this = this; 102 | try { 103 | this.launchEditorAsync(function () { 104 | try { 105 | _this.readTemporaryFile(); 106 | setImmediate(callback, null, _this.text); 107 | } 108 | catch (readError) { 109 | setImmediate(callback, readError, null); 110 | } 111 | }); 112 | } 113 | catch (launchError) { 114 | setImmediate(callback, launchError, null); 115 | } 116 | }; 117 | ExternalEditor.prototype.cleanup = function () { 118 | this.removeTemporaryFile(); 119 | }; 120 | ExternalEditor.prototype.determineEditor = function () { 121 | var editor = process.env.VISUAL ? process.env.VISUAL : 122 | process.env.EDITOR ? process.env.EDITOR : 123 | /^win/.test(process.platform) ? "notepad" : 124 | "vim"; 125 | var editorOpts = ExternalEditor.splitStringBySpace(editor).map(function (piece) { return piece.replace("\\ ", " "); }); 126 | var bin = editorOpts.shift(); 127 | this.editor = { args: editorOpts, bin: bin }; 128 | }; 129 | ExternalEditor.prototype.createTemporaryFile = function () { 130 | try { 131 | this.tempFile = tmp_1.tmpNameSync(this.fileOptions); 132 | var opt = { encoding: "utf8" }; 133 | if (this.fileOptions.hasOwnProperty("mode")) { 134 | opt.mode = this.fileOptions.mode; 135 | } 136 | fs_1.writeFileSync(this.tempFile, this.text, opt); 137 | } 138 | catch (createFileError) { 139 | throw new CreateFileError_1.CreateFileError(createFileError); 140 | } 141 | }; 142 | ExternalEditor.prototype.readTemporaryFile = function () { 143 | try { 144 | var tempFileBuffer = fs_1.readFileSync(this.tempFile); 145 | if (tempFileBuffer.length === 0) { 146 | this.text = ""; 147 | } 148 | else { 149 | var encoding = chardet_1.detect(tempFileBuffer).toString(); 150 | if (!iconv_lite_1.encodingExists(encoding)) { 151 | // Probably a bad idea, but will at least prevent crashing 152 | encoding = "utf8"; 153 | } 154 | this.text = iconv_lite_1.decode(tempFileBuffer, encoding); 155 | } 156 | } 157 | catch (readFileError) { 158 | throw new ReadFileError_1.ReadFileError(readFileError); 159 | } 160 | }; 161 | ExternalEditor.prototype.removeTemporaryFile = function () { 162 | try { 163 | fs_1.unlinkSync(this.tempFile); 164 | } 165 | catch (removeFileError) { 166 | throw new RemoveFileError_1.RemoveFileError(removeFileError); 167 | } 168 | }; 169 | ExternalEditor.prototype.launchEditor = function () { 170 | try { 171 | var editorProcess = child_process_1.spawnSync(this.editor.bin, this.editor.args.concat([this.tempFile]), { stdio: "inherit" }); 172 | this.lastExitStatus = editorProcess.status; 173 | } 174 | catch (launchError) { 175 | throw new LaunchEditorError_1.LaunchEditorError(launchError); 176 | } 177 | }; 178 | ExternalEditor.prototype.launchEditorAsync = function (callback) { 179 | var _this = this; 180 | try { 181 | var editorProcess = child_process_1.spawn(this.editor.bin, this.editor.args.concat([this.tempFile]), { stdio: "inherit" }); 182 | editorProcess.on("exit", function (code) { 183 | _this.lastExitStatus = code; 184 | setImmediate(callback); 185 | }); 186 | } 187 | catch (launchError) { 188 | throw new LaunchEditorError_1.LaunchEditorError(launchError); 189 | } 190 | }; 191 | return ExternalEditor; 192 | }()); 193 | exports.ExternalEditor = ExternalEditor; 194 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "external-editor", 3 | "version": "3.0.3", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.0.0", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", 10 | "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.0.0" 14 | } 15 | }, 16 | "@babel/highlight": { 17 | "version": "7.5.0", 18 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", 19 | "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", 20 | "dev": true, 21 | "requires": { 22 | "chalk": "^2.0.0", 23 | "esutils": "^2.0.2", 24 | "js-tokens": "^4.0.0" 25 | } 26 | }, 27 | "@types/chai": { 28 | "version": "4.1.7", 29 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", 30 | "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", 31 | "dev": true 32 | }, 33 | "@types/chardet": { 34 | "version": "0.5.0", 35 | "resolved": "https://registry.npmjs.org/@types/chardet/-/chardet-0.5.0.tgz", 36 | "integrity": "sha512-n5dB+Qtllpj36wOgWho8QtB09z0ad8KAihC+WJ6Jd+uQdmoLaBtacp5PlzB6eACwp+BioYI3R91W7FrQGIWMMA==", 37 | "dev": true, 38 | "requires": { 39 | "@types/node": "*" 40 | } 41 | }, 42 | "@types/mocha": { 43 | "version": "5.2.7", 44 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", 45 | "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", 46 | "dev": true 47 | }, 48 | "@types/node": { 49 | "version": "10.14.12", 50 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.12.tgz", 51 | "integrity": "sha512-QcAKpaO6nhHLlxWBvpc4WeLrTvPqlHOvaj0s5GriKkA1zq+bsFBPpfYCvQhLqLgYlIko8A9YrPdaMHCo5mBcpg==", 52 | "dev": true 53 | }, 54 | "@types/tmp": { 55 | "version": "0.0.33", 56 | "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", 57 | "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", 58 | "dev": true 59 | }, 60 | "ansi-styles": { 61 | "version": "3.2.1", 62 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 63 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 64 | "dev": true, 65 | "requires": { 66 | "color-convert": "^1.9.0" 67 | } 68 | }, 69 | "argparse": { 70 | "version": "1.0.10", 71 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 72 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 73 | "dev": true, 74 | "requires": { 75 | "sprintf-js": "~1.0.2" 76 | } 77 | }, 78 | "arrify": { 79 | "version": "1.0.1", 80 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", 81 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", 82 | "dev": true 83 | }, 84 | "assertion-error": { 85 | "version": "1.1.0", 86 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 87 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 88 | "dev": true 89 | }, 90 | "balanced-match": { 91 | "version": "1.0.0", 92 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 93 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 94 | "dev": true 95 | }, 96 | "brace-expansion": { 97 | "version": "1.1.11", 98 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 99 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 100 | "dev": true, 101 | "requires": { 102 | "balanced-match": "^1.0.0", 103 | "concat-map": "0.0.1" 104 | } 105 | }, 106 | "browser-stdout": { 107 | "version": "1.3.1", 108 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 109 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 110 | "dev": true 111 | }, 112 | "buffer-from": { 113 | "version": "1.1.1", 114 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 115 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 116 | "dev": true 117 | }, 118 | "builtin-modules": { 119 | "version": "1.1.1", 120 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 121 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 122 | "dev": true 123 | }, 124 | "chai": { 125 | "version": "4.2.0", 126 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", 127 | "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", 128 | "dev": true, 129 | "requires": { 130 | "assertion-error": "^1.1.0", 131 | "check-error": "^1.0.2", 132 | "deep-eql": "^3.0.1", 133 | "get-func-name": "^2.0.0", 134 | "pathval": "^1.1.0", 135 | "type-detect": "^4.0.5" 136 | } 137 | }, 138 | "chalk": { 139 | "version": "2.4.2", 140 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 141 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 142 | "dev": true, 143 | "requires": { 144 | "ansi-styles": "^3.2.1", 145 | "escape-string-regexp": "^1.0.5", 146 | "supports-color": "^5.3.0" 147 | } 148 | }, 149 | "chardet": { 150 | "version": "0.7.0", 151 | "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", 152 | "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" 153 | }, 154 | "check-error": { 155 | "version": "1.0.2", 156 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 157 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 158 | "dev": true 159 | }, 160 | "color-convert": { 161 | "version": "1.9.3", 162 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 163 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 164 | "dev": true, 165 | "requires": { 166 | "color-name": "1.1.3" 167 | } 168 | }, 169 | "color-name": { 170 | "version": "1.1.3", 171 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 172 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 173 | "dev": true 174 | }, 175 | "commander": { 176 | "version": "2.15.1", 177 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 178 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 179 | "dev": true 180 | }, 181 | "concat-map": { 182 | "version": "0.0.1", 183 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 184 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 185 | "dev": true 186 | }, 187 | "debug": { 188 | "version": "3.1.0", 189 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 190 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 191 | "dev": true, 192 | "requires": { 193 | "ms": "2.0.0" 194 | } 195 | }, 196 | "deep-eql": { 197 | "version": "3.0.1", 198 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 199 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 200 | "dev": true, 201 | "requires": { 202 | "type-detect": "^4.0.0" 203 | } 204 | }, 205 | "diff": { 206 | "version": "3.5.0", 207 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 208 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 209 | "dev": true 210 | }, 211 | "es6-shim": { 212 | "version": "0.35.5", 213 | "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.5.tgz", 214 | "integrity": "sha512-E9kK/bjtCQRpN1K28Xh4BlmP8egvZBGJJ+9GtnzOwt7mdqtrjHFuVGr7QJfdjBIKqrlU5duPf3pCBoDrkjVYFg==", 215 | "dev": true 216 | }, 217 | "escape-string-regexp": { 218 | "version": "1.0.5", 219 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 220 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 221 | "dev": true 222 | }, 223 | "esprima": { 224 | "version": "4.0.1", 225 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 226 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 227 | "dev": true 228 | }, 229 | "esutils": { 230 | "version": "2.0.2", 231 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 232 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 233 | "dev": true 234 | }, 235 | "fs.realpath": { 236 | "version": "1.0.0", 237 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 238 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 239 | "dev": true 240 | }, 241 | "get-func-name": { 242 | "version": "2.0.0", 243 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 244 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 245 | "dev": true 246 | }, 247 | "glob": { 248 | "version": "7.1.2", 249 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 250 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 251 | "dev": true, 252 | "requires": { 253 | "fs.realpath": "^1.0.0", 254 | "inflight": "^1.0.4", 255 | "inherits": "2", 256 | "minimatch": "^3.0.4", 257 | "once": "^1.3.0", 258 | "path-is-absolute": "^1.0.0" 259 | } 260 | }, 261 | "growl": { 262 | "version": "1.10.5", 263 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 264 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 265 | "dev": true 266 | }, 267 | "has-flag": { 268 | "version": "3.0.0", 269 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 270 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 271 | "dev": true 272 | }, 273 | "he": { 274 | "version": "1.1.1", 275 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 276 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 277 | "dev": true 278 | }, 279 | "iconv-lite": { 280 | "version": "0.4.24", 281 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 282 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 283 | "requires": { 284 | "safer-buffer": ">= 2.1.2 < 3" 285 | } 286 | }, 287 | "inflight": { 288 | "version": "1.0.6", 289 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 290 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 291 | "dev": true, 292 | "requires": { 293 | "once": "^1.3.0", 294 | "wrappy": "1" 295 | } 296 | }, 297 | "inherits": { 298 | "version": "2.0.3", 299 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 300 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 301 | "dev": true 302 | }, 303 | "js-tokens": { 304 | "version": "4.0.0", 305 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 306 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 307 | "dev": true 308 | }, 309 | "js-yaml": { 310 | "version": "3.13.1", 311 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", 312 | "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", 313 | "dev": true, 314 | "requires": { 315 | "argparse": "^1.0.7", 316 | "esprima": "^4.0.0" 317 | } 318 | }, 319 | "make-error": { 320 | "version": "1.3.5", 321 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", 322 | "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", 323 | "dev": true 324 | }, 325 | "minimatch": { 326 | "version": "3.0.4", 327 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 328 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 329 | "dev": true, 330 | "requires": { 331 | "brace-expansion": "^1.1.7" 332 | } 333 | }, 334 | "minimist": { 335 | "version": "0.0.8", 336 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 337 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 338 | "dev": true 339 | }, 340 | "mkdirp": { 341 | "version": "0.5.1", 342 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 343 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 344 | "dev": true, 345 | "requires": { 346 | "minimist": "0.0.8" 347 | } 348 | }, 349 | "mocha": { 350 | "version": "5.2.0", 351 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 352 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 353 | "dev": true, 354 | "requires": { 355 | "browser-stdout": "1.3.1", 356 | "commander": "2.15.1", 357 | "debug": "3.1.0", 358 | "diff": "3.5.0", 359 | "escape-string-regexp": "1.0.5", 360 | "glob": "7.1.2", 361 | "growl": "1.10.5", 362 | "he": "1.1.1", 363 | "minimatch": "3.0.4", 364 | "mkdirp": "0.5.1", 365 | "supports-color": "5.4.0" 366 | } 367 | }, 368 | "ms": { 369 | "version": "2.0.0", 370 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 371 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 372 | "dev": true 373 | }, 374 | "once": { 375 | "version": "1.4.0", 376 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 377 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 378 | "dev": true, 379 | "requires": { 380 | "wrappy": "1" 381 | } 382 | }, 383 | "os-tmpdir": { 384 | "version": "1.0.2", 385 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 386 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 387 | }, 388 | "path-is-absolute": { 389 | "version": "1.0.1", 390 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 391 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 392 | "dev": true 393 | }, 394 | "path-parse": { 395 | "version": "1.0.6", 396 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 397 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 398 | "dev": true 399 | }, 400 | "pathval": { 401 | "version": "1.1.0", 402 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 403 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 404 | "dev": true 405 | }, 406 | "resolve": { 407 | "version": "1.11.1", 408 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", 409 | "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", 410 | "dev": true, 411 | "requires": { 412 | "path-parse": "^1.0.6" 413 | } 414 | }, 415 | "safer-buffer": { 416 | "version": "2.1.2", 417 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 418 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 419 | }, 420 | "semver": { 421 | "version": "5.7.0", 422 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", 423 | "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", 424 | "dev": true 425 | }, 426 | "source-map": { 427 | "version": "0.6.1", 428 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 429 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 430 | "dev": true 431 | }, 432 | "source-map-support": { 433 | "version": "0.5.12", 434 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", 435 | "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", 436 | "dev": true, 437 | "requires": { 438 | "buffer-from": "^1.0.0", 439 | "source-map": "^0.6.0" 440 | } 441 | }, 442 | "sprintf-js": { 443 | "version": "1.0.3", 444 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 445 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 446 | "dev": true 447 | }, 448 | "supports-color": { 449 | "version": "5.4.0", 450 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 451 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 452 | "dev": true, 453 | "requires": { 454 | "has-flag": "^3.0.0" 455 | } 456 | }, 457 | "tmp": { 458 | "version": "0.0.33", 459 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 460 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 461 | "requires": { 462 | "os-tmpdir": "~1.0.2" 463 | } 464 | }, 465 | "ts-node": { 466 | "version": "7.0.1", 467 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", 468 | "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", 469 | "dev": true, 470 | "requires": { 471 | "arrify": "^1.0.0", 472 | "buffer-from": "^1.1.0", 473 | "diff": "^3.1.0", 474 | "make-error": "^1.1.1", 475 | "minimist": "^1.2.0", 476 | "mkdirp": "^0.5.1", 477 | "source-map-support": "^0.5.6", 478 | "yn": "^2.0.0" 479 | }, 480 | "dependencies": { 481 | "minimist": { 482 | "version": "1.2.0", 483 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 484 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 485 | "dev": true 486 | } 487 | } 488 | }, 489 | "tslib": { 490 | "version": "1.10.0", 491 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 492 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", 493 | "dev": true 494 | }, 495 | "tslint": { 496 | "version": "5.18.0", 497 | "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.18.0.tgz", 498 | "integrity": "sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w==", 499 | "dev": true, 500 | "requires": { 501 | "@babel/code-frame": "^7.0.0", 502 | "builtin-modules": "^1.1.1", 503 | "chalk": "^2.3.0", 504 | "commander": "^2.12.1", 505 | "diff": "^3.2.0", 506 | "glob": "^7.1.1", 507 | "js-yaml": "^3.13.1", 508 | "minimatch": "^3.0.4", 509 | "mkdirp": "^0.5.1", 510 | "resolve": "^1.3.2", 511 | "semver": "^5.3.0", 512 | "tslib": "^1.8.0", 513 | "tsutils": "^2.29.0" 514 | } 515 | }, 516 | "tsutils": { 517 | "version": "2.29.0", 518 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", 519 | "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", 520 | "dev": true, 521 | "requires": { 522 | "tslib": "^1.8.1" 523 | } 524 | }, 525 | "type-detect": { 526 | "version": "4.0.8", 527 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 528 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 529 | "dev": true 530 | }, 531 | "typescript": { 532 | "version": "3.5.2", 533 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", 534 | "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", 535 | "dev": true 536 | }, 537 | "wrappy": { 538 | "version": "1.0.2", 539 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 540 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 541 | "dev": true 542 | }, 543 | "yn": { 544 | "version": "2.0.0", 545 | "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", 546 | "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", 547 | "dev": true 548 | } 549 | } 550 | } 551 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "external-editor", 3 | "version": "3.1.0", 4 | "description": "Edit a string with the users preferred text editor using $VISUAL or $ENVIRONMENT", 5 | "main": "main/index.js", 6 | "types": "main/index.d.ts", 7 | "scripts": { 8 | "test": "mocha --recursive --require ts-node/register --timeout 10000 ./test/spec 'test/spec/**/*.ts'", 9 | "compile": "tsc -p tsconfig.json", 10 | "lint": "tslint './src/**/*.ts' './test/**/*.ts'" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/mrkmg/node-external-editor.git" 15 | }, 16 | "keywords": [ 17 | "editor", 18 | "external", 19 | "user", 20 | "visual" 21 | ], 22 | "author": "Kevin Gravier (https://mrkmg.com)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/mrkmg/node-external-editor/issues" 26 | }, 27 | "homepage": "https://github.com/mrkmg/node-external-editor#readme", 28 | "dependencies": { 29 | "chardet": "^0.7.0", 30 | "iconv-lite": "^0.4.24", 31 | "tmp": "^0.0.33" 32 | }, 33 | "engines": { 34 | "node": ">=4" 35 | }, 36 | "devDependencies": { 37 | "@types/chai": "^4.1.4", 38 | "@types/chardet": "^0.5.0", 39 | "@types/mocha": "^5.2.5", 40 | "@types/node": "^10.14.12", 41 | "@types/tmp": "0.0.33", 42 | "chai": "^4.0.0", 43 | "es6-shim": "^0.35.3", 44 | "mocha": "^5.2.0", 45 | "ts-node": "^7.0.1", 46 | "tslint": "^5.18.0", 47 | "typescript": "^3.5.2" 48 | }, 49 | "files": [ 50 | "main", 51 | "example_sync.js", 52 | "example_async.js" 53 | ], 54 | "config": { 55 | "ndt": { 56 | "versions": [ 57 | "major" 58 | ] 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/errors/CreateFileError.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node External Editor 3 | * 4 | * Kevin Gravier 5 | * MIT 2018 6 | */ 7 | 8 | export class CreateFileError extends Error { 9 | constructor(public originalError: Error) { 10 | super("Failed to create temporary file for editor"); 11 | 12 | const proto = new.target.prototype; 13 | if ((Object as any).setPrototypeOf) { 14 | (Object as any).setPrototypeOf(this, proto); 15 | } else { 16 | (this as any).__proto__ = new.target.prototype; 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/errors/LaunchEditorError.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node External Editor 3 | * 4 | * Kevin Gravier 5 | * MIT 2018 6 | */ 7 | 8 | export class LaunchEditorError extends Error { 9 | constructor(public originalError: Error) { 10 | super("Failed launch editor"); 11 | 12 | const proto = new.target.prototype; 13 | if ((Object as any).setPrototypeOf) { 14 | (Object as any).setPrototypeOf(this, proto); 15 | } else { 16 | (this as any).__proto__ = new.target.prototype; 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/errors/ReadFileError.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node External Editor 3 | * 4 | * Kevin Gravier 5 | * MIT 2018 6 | */ 7 | 8 | export class ReadFileError extends Error { 9 | constructor(public originalError: Error) { 10 | super("Failed to read temporary file"); 11 | 12 | const proto = new.target.prototype; 13 | if ((Object as any).setPrototypeOf) { 14 | (Object as any).setPrototypeOf(this, proto); 15 | } else { 16 | (this as any).__proto__ = new.target.prototype; 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/errors/RemoveFileError.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node External Editor 3 | * 4 | * Kevin Gravier 5 | * MIT 2018 6 | */ 7 | 8 | export class RemoveFileError extends Error { 9 | constructor(public originalError: Error) { 10 | super("Failed to cleanup temporary file"); 11 | 12 | const proto = new.target.prototype; 13 | if ((Object as any).setPrototypeOf) { 14 | (Object as any).setPrototypeOf(this, proto); 15 | } else { 16 | (this as any).__proto__ = new.target.prototype; 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node External Editor 3 | * 4 | * Kevin Gravier 5 | * MIT 2019 6 | */ 7 | 8 | import {detect} from "chardet"; 9 | import {spawn, spawnSync} from "child_process"; 10 | import {readFileSync, unlinkSync, WriteFileOptions, writeFileSync} from "fs"; 11 | import {decode, encodingExists} from "iconv-lite"; 12 | import {tmpNameSync} from "tmp"; 13 | import {CreateFileError} from "./errors/CreateFileError"; 14 | import {LaunchEditorError} from "./errors/LaunchEditorError"; 15 | import {ReadFileError} from "./errors/ReadFileError"; 16 | import {RemoveFileError} from "./errors/RemoveFileError"; 17 | 18 | export interface IEditorParams { 19 | args: string[]; 20 | bin: string; 21 | } 22 | 23 | export interface IFileOptions { 24 | prefix?: string; 25 | postfix?: string; 26 | mode?: number; 27 | template?: string; 28 | dir?: string; 29 | } 30 | 31 | export type StringCallback = (err: Error, result: string) => void; 32 | export type VoidCallback = () => void; 33 | export {CreateFileError, LaunchEditorError, ReadFileError, RemoveFileError}; 34 | 35 | export function edit(text: string = "", fileOptions?: IFileOptions) { 36 | const editor = new ExternalEditor(text, fileOptions); 37 | editor.run(); 38 | editor.cleanup(); 39 | return editor.text; 40 | } 41 | 42 | export function editAsync(text: string = "", callback: StringCallback, fileOptions?: IFileOptions) { 43 | const editor = new ExternalEditor(text, fileOptions); 44 | editor.runAsync((err: Error, result: string) => { 45 | if (err) { 46 | setImmediate(callback, err, null); 47 | } else { 48 | try { 49 | editor.cleanup(); 50 | setImmediate(callback, null, result); 51 | } catch (cleanupError) { 52 | setImmediate(callback, cleanupError, null); 53 | } 54 | } 55 | }); 56 | 57 | } 58 | 59 | export class ExternalEditor { 60 | private static splitStringBySpace(str: string) { 61 | const pieces: string[] = []; 62 | 63 | let currentString = ""; 64 | for (let strIndex = 0; strIndex < str.length; strIndex++) { 65 | const currentLetter = str[strIndex]; 66 | 67 | if (strIndex > 0 && currentLetter === " " && str[strIndex - 1] !== "\\" && currentString.length > 0) { 68 | pieces.push(currentString); 69 | currentString = ""; 70 | } else { 71 | currentString += currentLetter; 72 | } 73 | } 74 | 75 | if (currentString.length > 0) { 76 | pieces.push(currentString); 77 | } 78 | 79 | return pieces; 80 | } 81 | 82 | public text: string = ""; 83 | public tempFile: string; 84 | public editor: IEditorParams; 85 | public lastExitStatus: number; 86 | private fileOptions: IFileOptions = {}; 87 | 88 | public get temp_file() { 89 | console.log("DEPRECATED: temp_file. Use tempFile moving forward."); 90 | return this.tempFile; 91 | } 92 | 93 | public get last_exit_status() { 94 | console.log("DEPRECATED: last_exit_status. Use lastExitStatus moving forward."); 95 | return this.lastExitStatus; 96 | } 97 | 98 | constructor(text: string = "", fileOptions?: IFileOptions) { 99 | this.text = text; 100 | 101 | if (fileOptions) { 102 | this.fileOptions = fileOptions; 103 | } 104 | 105 | this.determineEditor(); 106 | this.createTemporaryFile(); 107 | } 108 | 109 | public run() { 110 | this.launchEditor(); 111 | this.readTemporaryFile(); 112 | return this.text; 113 | } 114 | 115 | public runAsync(callback: StringCallback) { 116 | try { 117 | this.launchEditorAsync(() => { 118 | try { 119 | this.readTemporaryFile(); 120 | setImmediate(callback, null, this.text); 121 | } catch (readError) { 122 | setImmediate(callback, readError, null); 123 | } 124 | }); 125 | } catch (launchError) { 126 | setImmediate(callback, launchError, null); 127 | } 128 | } 129 | 130 | public cleanup() { 131 | this.removeTemporaryFile(); 132 | } 133 | 134 | private determineEditor() { 135 | const editor = 136 | process.env.VISUAL ? process.env.VISUAL : 137 | process.env.EDITOR ? process.env.EDITOR : 138 | /^win/.test(process.platform) ? "notepad" : 139 | "vim"; 140 | 141 | const editorOpts = ExternalEditor.splitStringBySpace(editor).map((piece: string) => piece.replace("\\ ", " ")); 142 | const bin = editorOpts.shift(); 143 | 144 | this.editor = {args: editorOpts, bin}; 145 | } 146 | 147 | private createTemporaryFile() { 148 | try { 149 | this.tempFile = tmpNameSync(this.fileOptions); 150 | const opt: WriteFileOptions = {encoding: "utf8"}; 151 | if (this.fileOptions.hasOwnProperty("mode")) { 152 | opt.mode = this.fileOptions.mode; 153 | } 154 | writeFileSync(this.tempFile, this.text, opt); 155 | } catch (createFileError) { 156 | throw new CreateFileError(createFileError); 157 | } 158 | } 159 | 160 | private readTemporaryFile() { 161 | try { 162 | const tempFileBuffer = readFileSync(this.tempFile); 163 | if (tempFileBuffer.length === 0) { 164 | this.text = ""; 165 | } else { 166 | let encoding = detect(tempFileBuffer).toString(); 167 | 168 | if (!encodingExists(encoding)) { 169 | // Probably a bad idea, but will at least prevent crashing 170 | encoding = "utf8"; 171 | } 172 | 173 | this.text = decode(tempFileBuffer, encoding); 174 | } 175 | } catch (readFileError) { 176 | throw new ReadFileError(readFileError); 177 | } 178 | 179 | } 180 | 181 | private removeTemporaryFile() { 182 | try { 183 | unlinkSync(this.tempFile); 184 | } catch (removeFileError) { 185 | throw new RemoveFileError(removeFileError); 186 | } 187 | } 188 | 189 | private launchEditor() { 190 | try { 191 | const editorProcess = spawnSync( 192 | this.editor.bin, 193 | this.editor.args.concat([this.tempFile]), 194 | {stdio: "inherit"}); 195 | this.lastExitStatus = editorProcess.status; 196 | } catch (launchError) { 197 | throw new LaunchEditorError(launchError); 198 | } 199 | } 200 | 201 | private launchEditorAsync(callback: VoidCallback) { 202 | try { 203 | const editorProcess = spawn( 204 | this.editor.bin, 205 | this.editor.args.concat([this.tempFile]), 206 | {stdio: "inherit"}); 207 | editorProcess.on("exit", (code: number) => { 208 | this.lastExitStatus = code; 209 | setImmediate(callback); 210 | }); 211 | } catch (launchError) { 212 | throw new LaunchEditorError(launchError); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /test/spec/main.spec.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Node External Editor 3 | * 4 | * Kevin Gravier 5 | * MIT 2018 6 | */ 7 | 8 | // tslint:disable-next-line:no-var-requires 9 | require("es6-shim"); 10 | 11 | import Chai = require("chai"); 12 | import {readFileSync, statSync, writeFileSync} from "fs"; 13 | import {encode} from "iconv-lite"; 14 | import {dirname} from "path"; 15 | import {edit, editAsync, ExternalEditor} from "../../src"; 16 | const assert = Chai.assert; 17 | 18 | const testingInput = "aAbBcCdDeEfFgG"; 19 | const expectedResult = "aAbBcCdDeE"; 20 | 21 | describe("main", () => { 22 | let previousVisual: string; 23 | let editor: ExternalEditor; 24 | 25 | before(() => { 26 | previousVisual = process.env.VISUAL; 27 | process.env.VISUAL = "truncate --size 10"; 28 | }); 29 | 30 | beforeEach(() => { 31 | editor = new ExternalEditor(testingInput); 32 | }); 33 | 34 | afterEach(() => { 35 | editor.cleanup(); 36 | }); 37 | 38 | after(() => { 39 | process.env.VISUAL = previousVisual; 40 | }); 41 | 42 | it("convenience method \".edit\"", () => { 43 | const text = edit(testingInput); 44 | assert.equal(text, expectedResult); 45 | }); 46 | 47 | it("convenience method \".editAsync\"", (cb) => { 48 | editAsync(testingInput, (e, text) => { 49 | assert.equal(text, expectedResult); 50 | cb(); 51 | }); 52 | }); 53 | 54 | it("writes original text to file", () => { 55 | const contents = readFileSync(editor.tempFile).toString(); 56 | assert.equal(contents, testingInput); 57 | }); 58 | 59 | it("run() returns correctly", () => { 60 | const text = editor.run(); 61 | assert.equal(text, expectedResult); 62 | assert.equal(editor.lastExitStatus, 0); 63 | }); 64 | 65 | it("runAsync() callbacks correctly", (cb) => { 66 | editor.runAsync((e, text) => { 67 | assert.equal(text, expectedResult); 68 | assert.equal(editor.lastExitStatus, 0); 69 | cb(); 70 | }); 71 | }); 72 | 73 | it("run() returns text same as editor.text", () => { 74 | const text = editor.run(); 75 | assert.equal(text, editor.text); 76 | }); 77 | 78 | it("runAsync() callback text same as editor.text", (cb) => { 79 | editor.runAsync((e, text) => { 80 | assert.equal(text, editor.text); 81 | cb(); 82 | }); 83 | }); 84 | }); 85 | 86 | describe("invalid exit code", () => { 87 | let editor: ExternalEditor; 88 | 89 | beforeEach(() => { 90 | editor = new ExternalEditor(testingInput); 91 | editor.editor.bin = "bash"; 92 | editor.editor.args = ["-c", "exit 1"]; 93 | }); 94 | 95 | afterEach(() => { 96 | editor.cleanup(); 97 | }); 98 | 99 | it("run()", () => { 100 | editor.run(); 101 | assert.equal(editor.lastExitStatus, 1); 102 | }); 103 | 104 | it("runAsync()", (cb) => { 105 | editor.runAsync((e, text) => { 106 | assert.equal(editor.lastExitStatus, 1); 107 | cb(); 108 | }); 109 | }); 110 | }); 111 | 112 | describe("custom options", () => { 113 | let editor: ExternalEditor = null; 114 | 115 | afterEach(() => { 116 | if (editor) { 117 | editor.cleanup(); 118 | } 119 | editor = null; 120 | }); 121 | 122 | it("prefix", () => { 123 | editor = new ExternalEditor("testing", { 124 | prefix: "pre", 125 | }); 126 | 127 | assert.match(editor.tempFile, /.+\/pre.+$/); 128 | }); 129 | 130 | it("postfix", () => { 131 | editor = new ExternalEditor("testing", { 132 | postfix: "end.post", 133 | }); 134 | 135 | assert.match(editor.tempFile, /.+end\.post$/); 136 | }); 137 | 138 | it("dir", () => { 139 | editor = new ExternalEditor("testing", { 140 | dir: __dirname, 141 | }); 142 | 143 | assert.equal(dirname(editor.tempFile), __dirname); 144 | }); 145 | 146 | it("mode", () => { 147 | editor = new ExternalEditor("testing", { 148 | mode: 0o755, 149 | }); 150 | 151 | const stat = statSync(editor.tempFile); 152 | const int = parseInt(stat.mode.toString(8), 10); 153 | 154 | assert.equal(int, 100755); 155 | }); 156 | }); 157 | 158 | describe("charsets", () => { 159 | let previousVisual: string; 160 | let editor: ExternalEditor; 161 | 162 | before(() => { 163 | previousVisual = process.env.VISUAL; 164 | process.env.VISUAL = "true"; 165 | }); 166 | 167 | beforeEach(() => { 168 | editor = new ExternalEditor("XXX"); 169 | }); 170 | 171 | afterEach(() => { 172 | editor.cleanup(); 173 | }); 174 | 175 | after(() => { 176 | process.env.VISUAL = previousVisual; 177 | }); 178 | 179 | it("empty", () => { 180 | writeFileSync(editor.tempFile, ""); 181 | const text = editor.run(); 182 | assert.equal(text, ""); 183 | }); 184 | 185 | it("utf8", () => { 186 | const testData = "काचं शक्नोम्यत्तुम् । नोपहिनस्ति माम् ॥"; 187 | const textEncoding = "utf8"; 188 | writeFileSync(editor.tempFile, encode(testData, textEncoding), {encoding: "binary"}); 189 | const result = editor.run(); 190 | assert.equal(testData, result); 191 | }); 192 | 193 | it("utf16", () => { 194 | const testData = "काचं शक्नोम्यत्तुम् । नोपहिनस्ति माम् ॥"; 195 | const textEncoding = "utf16"; 196 | writeFileSync(editor.tempFile, encode(testData, textEncoding), {encoding: "binary"}); 197 | const result = editor.run(); 198 | assert.equal(testData, result); 199 | }); 200 | 201 | it("win1252", () => { 202 | const testData = "Testing 1 2 3 ! @ #"; 203 | const textEncoding = "win1252"; 204 | writeFileSync(editor.tempFile, encode(testData, textEncoding), {encoding: "binary"}); 205 | const result = editor.run(); 206 | assert.equal(testData, result); 207 | }); 208 | 209 | it("Big5", () => { 210 | const testData = "能 脊 胼 胯 臭 臬 舀 舐 航 舫 舨 般 芻 茫 荒 荔"; 211 | const textEncoding = "Big5"; 212 | writeFileSync(editor.tempFile, encode(testData, textEncoding), {encoding: "binary"}); 213 | const result = editor.run(); 214 | assert.equal(testData, result); 215 | }); 216 | 217 | }); 218 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "noImplicitAny": true, 7 | "removeComments": false, 8 | "preserveConstEnums": true, 9 | "outDir": "main/", 10 | "sourceMap": false, 11 | "declaration": true 12 | }, 13 | "include": [ 14 | "src/**/*", 15 | "types/**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules", 19 | "**/*.spec.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "no-console": false 9 | }, 10 | "rulesDirectory": [] 11 | } 12 | --------------------------------------------------------------------------------