├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── binding.gyp ├── dist ├── better-clipboard.d.ts ├── better-clipboard.js ├── index.d.ts ├── index.js ├── tools.d.ts └── tools.js ├── lib ├── async.ts ├── better-clipboard.ts ├── index.ts └── tools.ts ├── package.json ├── src ├── clip_osx.h ├── clip_osx.mm ├── clip_win.cc ├── clip_win.h ├── index.ts └── main.cc ├── test └── index.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | npm-debug.log 3 | yarn-error.log 4 | package-lock.json 5 | yarn.lock 6 | .DS_Store 7 | .idea/ 8 | node_modules 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules 3 | /npm-debug.log 4 | yarn.lock 5 | npm-debug.log 6 | /bin 7 | /lib 8 | /.idea 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 SIMU 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 | A better clipboard for Electron (NodeJS). 2 | 3 | #### Install 4 | 5 | ``` 6 | npm i better-clipboard 7 | 8 | electron-rebuild -f -w better-clipboard 9 | ``` 10 | 11 | More info of installation: https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/troubleshooting.md 12 | 13 | #### Usage 14 | 15 | ```js 16 | import { betterClipboard } from 'better-clipboard'; 17 | 18 | betterClipboard.readFilePathList(); // get the path of file which in clipboard 19 | betterClipboard.readBufferList(); 20 | betterClipboard.readFileList(); 21 | 22 | 23 | betterClipboard.writeFileList([]); // write file into clipboard via file path 24 | ``` 25 | 26 | #### Webpack 27 | 28 | If you are using webpack, simply add better-clipboard into externals: 29 | ```js 30 | config.externals = { 31 | 'better-clipboard': 'commonjs better-clipboard' 32 | } 33 | ``` 34 | 35 | Hope you have a better experience when using clipboard in electron. 36 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [{ 3 | "target_name": "better_clipboard", 4 | "sources": [ "src/main.cc" ], 5 | "include_dirs": [" 2 | declare class BetterClipboard { 3 | private nativeClipboard; 4 | constructor(); 5 | readFilePathList(): string[]; 6 | readBufferList(): Promise; 7 | readFileList(): Promise; 8 | writeFileList(filePathList: string[]): void; 9 | } 10 | export default BetterClipboard; 11 | -------------------------------------------------------------------------------- /dist/better-clipboard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var tools_1 = require("./tools"); 4 | var path = require("path"); 5 | var better_clipboard = require('bindings')('better_clipboard.node'); 6 | var BetterClipboard = /** @class */ (function () { 7 | function BetterClipboard() { 8 | this.nativeClipboard = better_clipboard; 9 | } 10 | BetterClipboard.prototype.readFilePathList = function () { 11 | return this.nativeClipboard.readFiles(); 12 | }; 13 | BetterClipboard.prototype.readBufferList = function () { 14 | var filePathList = this.readFilePathList(); 15 | var bufferList = []; 16 | filePathList.forEach(function (filePath) { 17 | if (!path.isAbsolute(filePath)) 18 | return; 19 | bufferList.push(tools_1.readBuffer(filePath)); 20 | }); 21 | return Promise.all(bufferList); 22 | }; 23 | BetterClipboard.prototype.readFileList = function () { 24 | var filePathList = this.readFilePathList(); 25 | var fileList = []; 26 | filePathList.forEach(function (filePath) { 27 | if (!path.isAbsolute(filePath)) 28 | return; 29 | fileList.push(tools_1.readFile(filePath)); 30 | }); 31 | return Promise.all(fileList); 32 | }; 33 | BetterClipboard.prototype.writeFileList = function (filePathList) { 34 | console.log(this.nativeClipboard); 35 | return this.nativeClipboard.writeFiles(filePathList); 36 | }; 37 | return BetterClipboard; 38 | }()); 39 | exports.default = BetterClipboard; 40 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import BetterClipboard from './better-clipboard'; 2 | declare const betterClipboard: BetterClipboard; 3 | export { betterClipboard, BetterClipboard }; 4 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.BetterClipboard = exports.betterClipboard = void 0; 4 | var better_clipboard_1 = require("./better-clipboard"); 5 | exports.BetterClipboard = better_clipboard_1.default; 6 | var betterClipboard = new better_clipboard_1.default(); 7 | exports.betterClipboard = betterClipboard; 8 | -------------------------------------------------------------------------------- /dist/tools.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export declare const readBuffer: (filePath: string) => Promise; 3 | export declare const readFile: (filePath: string) => Promise; 4 | -------------------------------------------------------------------------------- /dist/tools.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.readFile = exports.readBuffer = void 0; 4 | var fs = require("fs"); 5 | var path = require("path"); 6 | var readBuffer = function (filePath) { 7 | return new Promise(function (resolve, reject) { 8 | fs.readFile(filePath, function (err, fileBuffer) { 9 | resolve(err ? null : fileBuffer); 10 | }); 11 | }); 12 | }; 13 | exports.readBuffer = readBuffer; 14 | var readFile = function (filePath) { 15 | return new Promise(function (resolve, reject) { 16 | exports.readBuffer(filePath).then(function (buffer) { 17 | if (!buffer) 18 | resolve(null); 19 | var file = new File([buffer], "" + path.basename(filePath)); 20 | resolve(file); 21 | }); 22 | }); 23 | }; 24 | exports.readFile = readFile; 25 | -------------------------------------------------------------------------------- /lib/async.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simo-an/better-clipboard/0e949a5096593752e5592b78b426822c05e80c4e/lib/async.ts -------------------------------------------------------------------------------- /lib/better-clipboard.ts: -------------------------------------------------------------------------------- 1 | import {readBuffer, readFile} from "./tools"; 2 | import * as path from 'path'; 3 | const better_clipboard = require('bindings')('better_clipboard.node'); 4 | 5 | interface NativeClipboard { 6 | readFiles(): string[], 7 | writeFiles(filePathList: string[]): void, 8 | } 9 | 10 | class BetterClipboard { 11 | private nativeClipboard: NativeClipboard = better_clipboard; 12 | constructor() { 13 | } 14 | 15 | public readFilePathList(): string[] { 16 | return this.nativeClipboard.readFiles(); 17 | } 18 | 19 | public readBufferList(): Promise { 20 | const filePathList = this.readFilePathList(); 21 | const bufferList = []; 22 | 23 | filePathList.forEach((filePath) => { 24 | if (!path.isAbsolute(filePath)) return ; 25 | 26 | bufferList.push(readBuffer(filePath)) 27 | }); 28 | 29 | return Promise.all(bufferList); 30 | } 31 | 32 | public readFileList(): Promise { 33 | const filePathList = this.readFilePathList(); 34 | const fileList = []; 35 | 36 | filePathList.forEach((filePath) => { 37 | if (!path.isAbsolute(filePath)) return ; 38 | 39 | fileList.push(readFile(filePath)) 40 | }); 41 | 42 | return Promise.all(fileList); 43 | } 44 | 45 | public writeFileList(filePathList: string[]): void { 46 | return this.nativeClipboard.writeFiles(filePathList); 47 | } 48 | } 49 | 50 | export default BetterClipboard; 51 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import BetterClipboard from './better-clipboard'; 2 | 3 | const betterClipboard = new BetterClipboard(); 4 | 5 | export { 6 | betterClipboard, BetterClipboard 7 | }; 8 | -------------------------------------------------------------------------------- /lib/tools.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | export const readBuffer = (filePath: string): Promise => { 5 | return new Promise((resolve, reject) => { 6 | fs.readFile(filePath, (err, fileBuffer) => { 7 | resolve(err ? null : fileBuffer); 8 | }); 9 | }) 10 | } 11 | 12 | export const readFile = (filePath: string): Promise => { 13 | return new Promise((resolve, reject) => { 14 | readBuffer(filePath).then(buffer => { 15 | if (!buffer) resolve(null); 16 | 17 | const file = new File([buffer], `${path.basename(filePath)}`); 18 | 19 | resolve(file); 20 | }); 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "better-clipboard", 3 | "version": "1.0.0", 4 | "description": "A better clipboard for Electron.", 5 | "main": "./dist/index.js", 6 | "homepage": "https://github.com/simo-an/better-clipboard", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/simo-an/better-clipboard.git" 10 | }, 11 | "keywords": [ 12 | "better-clipboard", 13 | "clipboard", 14 | "electron" 15 | ], 16 | "scripts": { 17 | "test": "node ./test", 18 | "install": "node-gyp rebuild --release" 19 | }, 20 | "author": "simu", 21 | "license": "MIT", 22 | "gypfile": true, 23 | "dependencies": { 24 | "bindings": "^1.5.0", 25 | "nan": "^2.13.2", 26 | "prebuild-install": "^6.1.3" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^16.0.2", 30 | "typescript": "^4.3.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/clip_osx.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | using namespace std; 6 | using v8::Context; 7 | using v8::Array; 8 | using v8::FunctionCallbackInfo; 9 | using v8::Isolate; 10 | using v8::Local; 11 | using v8::MaybeLocal; 12 | using v8::Object; 13 | using v8::String; 14 | using v8::NewStringType; 15 | using v8::Value; 16 | 17 | Local get_file_names(Isolate *isolate); 18 | 19 | void write_file_names(Isolate *isolate, Local fileNames); -------------------------------------------------------------------------------- /src/clip_osx.mm: -------------------------------------------------------------------------------- 1 | #include "clip_osx.h" 2 | 3 | Local get_file_names(Isolate *isolate){ 4 | Local fileNames = Array::New(isolate); 5 | Local context = isolate->GetCurrentContext(); 6 | 7 | NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; 8 | NSArray* tempArray = [pasteboard pasteboardItems]; 9 | int count = 0; 10 | for(NSPasteboardItem *tmpItem in tempArray){ 11 | NSString *pathString = [tmpItem stringForType:@"public.file-url"]; 12 | const char* str = [pathString UTF8String]; 13 | if(str){ 14 | fileNames->Set(context, count, String::NewFromUtf8(isolate, str, NewStringType::kNormal).ToLocalChecked()); 15 | count++; 16 | } 17 | } 18 | return fileNames; 19 | } 20 | 21 | void write_file_names(Isolate *isolate, Local fileNames) 22 | { 23 | Local context = isolate->GetCurrentContext(); 24 | 25 | std::vector files; 26 | for (size_t i = 0; i < fileNames->Length(); i++) { 27 | MaybeLocal maybeIndex = fileNames->Get(context, i); 28 | Local index = maybeIndex.ToLocalChecked(); 29 | String::Utf8Value path(isolate, index); 30 | std::string pathStr(*path); 31 | files.push_back(pathStr); 32 | } 33 | 34 | NSMutableArray* fileList = [NSMutableArray arrayWithCapacity:files.size()]; 35 | NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; 36 | for (unsigned int i = 0; i < files.size(); i++) 37 | [fileList addObject:[NSString stringWithUTF8String:files[i].c_str()]]; 38 | [pasteboard declareTypes:[NSArray arrayWithObject:NSFilenamesPboardType] owner:nil]; 39 | [pasteboard setPropertyList:fileList forType:NSFilenamesPboardType]; 40 | } 41 | -------------------------------------------------------------------------------- /src/clip_win.cc: -------------------------------------------------------------------------------- 1 | #include "clip_win.h" 2 | 3 | char *GBK2Utf8(const char *strGBK) 4 | { 5 | WCHAR *str1; 6 | int n = MultiByteToWideChar(CP_ACP, 0, strGBK, -1, NULL, 0); 7 | str1 = new WCHAR[n]; 8 | MultiByteToWideChar(CP_ACP, 0, strGBK, -1, str1, n); 9 | n = WideCharToMultiByte(CP_UTF8, 0, str1, -1, NULL, 0, NULL, NULL); 10 | char *str2 = new char[n]; 11 | WideCharToMultiByte(CP_UTF8, 0, str1, -1, str2, n, NULL, NULL); 12 | delete[] str1; 13 | str1 = NULL; 14 | return str2; 15 | } 16 | 17 | Local get_file_names(Isolate *isolate) 18 | { 19 | Local fileNames = Array::New(isolate, 0); 20 | Local context = isolate->GetCurrentContext(); 21 | if (OpenClipboard(NULL)) // open clipboard 22 | { 23 | HDROP hDrop = HDROP(::GetClipboardData(CF_HDROP)); // get the file path hwnd of clipboard 24 | if (hDrop != NULL) 25 | { 26 | char szFilePathName[MAX_PATH + 1] = { 0 }; 27 | UINT nNumOfFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0); // get the count of files 28 | fileNames = Array::New(isolate, nNumOfFiles); 29 | for (UINT nIndex = 0; nIndex < nNumOfFiles; ++nIndex) 30 | { 31 | memset(szFilePathName, 0, MAX_PATH + 1); 32 | DragQueryFile(hDrop, nIndex, szFilePathName, MAX_PATH); // get file name 33 | fileNames->Set(context, nIndex, String::NewFromUtf8(isolate, GBK2Utf8(szFilePathName), NewStringType::kNormal).ToLocalChecked()); 34 | } 35 | } 36 | CloseClipboard(); // close clipboard 37 | } 38 | return fileNames; 39 | } 40 | 41 | std::wstring stringToWstring(const std::string& str) 42 | { 43 | int nLen = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, NULL, 0); 44 | if (nLen == 0) 45 | return std::wstring(L""); 46 | 47 | wchar_t* wide = new wchar_t[nLen]; 48 | if (!wide) 49 | return std::wstring(L""); 50 | 51 | MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, wide, nLen); 52 | std::wstring wstr(wide); 53 | delete[] wide; 54 | wide = NULL; 55 | return wstr; 56 | } 57 | 58 | void write_file_names(Isolate *isolate, Local fileNames) 59 | { 60 | Local context = isolate->GetCurrentContext(); 61 | 62 | std::vector files; 63 | for (size_t i = 0; i < fileNames->Length(); i++) { 64 | MaybeLocal maybeIndex = fileNames->Get(context, i); 65 | Local index = maybeIndex.ToLocalChecked(); 66 | String::Utf8Value path(isolate, index); 67 | std::string pathStr(*path); 68 | files.push_back(stringToWstring(pathStr)); 69 | } 70 | 71 | size_t bytes = sizeof(DROPFILES); 72 | for (size_t i = 0; i < files.size(); ++i) 73 | bytes += (files[i].length() + 1) * sizeof(wchar_t); 74 | bytes += sizeof(wchar_t); 75 | HANDLE hdata = ::GlobalAlloc(GMEM_MOVEABLE, bytes); 76 | if (!hdata) 77 | return; 78 | DROPFILES* drop_files = static_cast(::GlobalLock(hdata)); 79 | drop_files->pFiles = sizeof(DROPFILES); 80 | drop_files->fWide = TRUE; 81 | BYTE* data = reinterpret_cast(drop_files) + sizeof(DROPFILES); 82 | // Copy the strings stored in 'files' with proper NULL separation. 83 | wchar_t* data_pos = reinterpret_cast(data); 84 | for (size_t i = 0; i < files.size(); ++i) { 85 | size_t offset = files[i].length() + 1; 86 | memcpy(data_pos, files[i].c_str(), offset * sizeof(wchar_t)); 87 | data_pos += offset; 88 | } 89 | data_pos[0] = L'\0'; // Double NULL termination after the last string. 90 | ::GlobalUnlock(hdata); 91 | if (OpenClipboard(NULL)) { 92 | EmptyClipboard(); 93 | SetClipboardData(CF_HDROP, hdata); 94 | CloseClipboard(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/clip_win.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | using namespace std; 9 | using v8::Context; 10 | using v8::Array; 11 | using v8::FunctionCallbackInfo; 12 | using v8::Isolate; 13 | using v8::Local; 14 | using v8::MaybeLocal; 15 | using v8::Object; 16 | using v8::String; 17 | using v8::NewStringType; 18 | using v8::Value; 19 | 20 | Local get_file_names(Isolate *isolate); 21 | 22 | void write_file_names(Isolate *isolate, Local fileNames); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simo-an/better-clipboard/0e949a5096593752e5592b78b426822c05e80c4e/src/index.ts -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #ifdef __WIN32__ 3 | #include "clip_win.h" 4 | #elif __APPLE__ 5 | #include "clip_osx.h" 6 | #endif 7 | 8 | namespace clipboard 9 | { 10 | using v8::Context; 11 | using v8::Array; 12 | using v8::FunctionCallbackInfo; 13 | using v8::Isolate; 14 | using v8::Local; 15 | using v8::MaybeLocal; 16 | using v8::Object; 17 | using v8::String; 18 | using v8::NewStringType; 19 | using v8::Value; 20 | 21 | void readFiles(const FunctionCallbackInfo &args) 22 | { 23 | Isolate *isolate = args.GetIsolate(); 24 | Local fileNames = get_file_names(isolate); 25 | args.GetReturnValue().Set(fileNames); 26 | } 27 | 28 | void writeFiles(const FunctionCallbackInfo &args) 29 | { 30 | if (args[0]->IsArray()) { 31 | Isolate *isolate = args.GetIsolate(); 32 | Local array = Local::Cast(args[0]); 33 | write_file_names(isolate, array); 34 | } 35 | } 36 | 37 | void Init(Local exports) 38 | { 39 | NODE_SET_METHOD(exports, "readFiles", readFiles); 40 | NODE_SET_METHOD(exports, "writeFiles", writeFiles); 41 | } 42 | 43 | NODE_MODULE(NODE_GYP_MODULE_NAME, Init) 44 | } -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const { betterClipboard } = require("../dist"); 2 | 3 | console.log("files:", betterClipboard.readBufferList()); 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "./dist", 7 | "target": "es5", 8 | "module": "commonjs", 9 | "strict": false, 10 | "declaration": true, 11 | "moduleResolution": "node", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": ["es2020", "es2019", "es2018", "dom"], 16 | }, 17 | "include": ["./lib/index.ts"], 18 | "exclude": [ "node_modules" ] 19 | } 20 | --------------------------------------------------------------------------------