├── images
├── 16x16.png
└── icon.icns
├── .gitignore
├── auto_updater.json
├── .vscode
└── settings.json
├── .eslintrc.js
├── README.md
├── index.html
├── package.json
├── updater.js
├── config.js
├── settings.js
└── main.js
/images/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaabhilash97/cliptext-clipboard-manager/HEAD/images/16x16.png
--------------------------------------------------------------------------------
/images/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaabhilash97/cliptext-clipboard-manager/HEAD/images/icon.icns
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | signer.sh
2 | **/.gitignore
3 | **/node_modules
4 | **/.DS_Store
5 | dist/github
6 | dist/mac
7 | dist/*.dmg
8 | dist/
9 |
--------------------------------------------------------------------------------
/auto_updater.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.1",
3 | "releaseDate": "2019-11-27T11:29:00.656Z",
4 | "url": "https://github.com/aaabhilash97/cliptext/releases/download/v2.0.1/cliptext-2.0.1-mac.zip"
5 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "USERPROFILE",
4 | "autoload",
5 | "clipboarddb",
6 | "clipboarddbpref",
7 | "submenu",
8 | "upsert"
9 | ]
10 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'env': {
3 | 'browser': true,
4 | 'commonjs': true,
5 | 'es6': true,
6 | },
7 | 'extends': [
8 | 'google',
9 | ],
10 | 'globals': {
11 | 'Atomics': 'readonly',
12 | 'SharedArrayBuffer': 'readonly',
13 | },
14 | 'parserOptions': {
15 | 'ecmaVersion': 2018,
16 | },
17 | 'rules': {
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### ClipText
2 | ClipText is a simple clipboard manager for macOS built with electron.
3 | ##### Features
4 | * Save history of your last copied texts
5 | * Global hotkey for popingup tray context menu with clipboard history.
6 |
7 | Global shortcut for launching Clipboard history is ```Cmd+Alt+h```
8 |
9 | You can find the latest build from [releases](https://github.com/aaabhilash97/cliptext/releases) section
10 |
11 | ### Screenshots:
12 |
13 |
14 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | textsms
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cliptext",
3 | "version": "2.0.1",
4 | "description": "clipboard manager",
5 | "main": "main.js",
6 | "scripts": {
7 | "build": "electron-builder",
8 | "start": "electron ."
9 | },
10 | "build": {
11 | "appId": "com.xxxxxxx.cliptext",
12 | "files": [
13 | "*.js",
14 | "*.html",
15 | "package.json",
16 | "node_modules",
17 | "images"
18 | ],
19 | "mac": {
20 | "icon": "./images/icon.icns"
21 | },
22 | "asar": true
23 | },
24 | "author": "Abhilash Km (aaabhilash97@gmail.com)",
25 | "license": "ISC",
26 | "devDependencies": {
27 | "electron": "^15.5.5",
28 | "electron-builder": "^22.1.0",
29 | "electron-rebuild": "^1.5.11",
30 | "eslint": "^6.6.0",
31 | "eslint-config-google": "^0.14.0"
32 | },
33 | "dependencies": {
34 | "auto-launch": "^5.0.1",
35 | "electron-log": "^2.2.6",
36 | "got": "^11.8.5",
37 | "md5": "^2.2.1",
38 | "nedb": "^1.8.0",
39 | "semver": "^5.3.0"
40 | }
41 | }
--------------------------------------------------------------------------------
/updater.js:
--------------------------------------------------------------------------------
1 | const {logger, packageInfos} = require('./config');
2 |
3 | const {autoUpdater} = require('electron');
4 | const got = require('got');
5 | const semver = require('semver');
6 |
7 |
8 | const appVersion = packageInfos.version;
9 |
10 | const options = {
11 | repo: 'https://raw.githubusercontent.com/aaabhilash97/cliptext/master/auto_updater.json',
12 | };
13 |
14 | /**
15 | * Start update checker
16 | */
17 | function checkForUpdates() {
18 | setInterval(async ()=>{
19 | try {
20 | let data = await got(options.repo);
21 | data = JSON.parse(data.body);
22 | const regex = /-(\d+\.\d+\.\d+)-/;
23 | const version = data.url.match(regex);
24 | if (semver.gt(version[1], appVersion)) {
25 | autoUpdater.setFeedURL(options.repo);
26 | autoUpdater.on('checking-for-update', ()=>{
27 | logger.info('checking for updates');
28 | });
29 | autoUpdater.on('update-available', ()=>{
30 | logger.info('update-available');
31 | });
32 | autoUpdater.on('update-not-available', ()=>{
33 | logger.info('update-not-available');
34 | });
35 | autoUpdater.on('update-downloaded', ()=>{
36 | autoUpdater.quitAndInstall();
37 | });
38 | autoUpdater.checkForUpdates();
39 | }
40 | } catch (error) {
41 | logger.error('Error in checking update', error);
42 | }
43 | }, 1000000);
44 | }
45 |
46 | module.exports = {
47 | checkForUpdates: checkForUpdates,
48 | };
49 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | const packageInfos = require('./package.json');
2 |
3 | const logger = require('electron-log');
4 | const NeDB = require('nedb');
5 | const path = require('path');
6 | const fs = require('fs');
7 |
8 | logger.transports.file.level = 'error';
9 | logger.transports.console.level = 'debug';
10 |
11 |
12 | const DBDir = path.join(getUserHome(), `.${packageInfos.name}`);
13 |
14 | try {
15 | fs.accessSync(DBDir);
16 | } catch (err) {
17 | fs.mkdirSync(DBDir);
18 | }
19 |
20 | /**
21 | * Get Home folder for current user
22 | * @return {string} User home folder path
23 | */
24 | function getUserHome() {
25 | return process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'];
26 | }
27 |
28 | const clipboardDB = new NeDB(
29 | {filename: path.join(DBDir, '/.clipboarddbv2'), autoload: true});
30 | const settingsDB = new NeDB(
31 | {filename: path.join(DBDir, '/.clipboarddbprefv2'), autoload: true});
32 |
33 | settingsDB.ensureIndex({fieldName: 'key', unique: true}, function(err) {
34 | if (err) console.error(err);
35 | });
36 |
37 | clipboardDB.ensureIndex({fieldName: 'updated_at'}, function(err) {
38 | if (err) console.error(err);
39 | });
40 | clipboardDB.ensureIndex({fieldName: 'hash'}, function(err) {
41 | if (err) console.error(err);
42 | });
43 |
44 | module.exports = {
45 | logger: logger,
46 | packageInfos: packageInfos,
47 | DBDir: DBDir,
48 | clipboardDB: clipboardDB,
49 | settingsDB: settingsDB,
50 | appName: packageInfos.name,
51 | isDevelopment: process.env.ENV === 'development',
52 | };
53 |
54 |
--------------------------------------------------------------------------------
/settings.js:
--------------------------------------------------------------------------------
1 | const {logger, appName, isDevelopment, settingsDB} = require('./config');
2 |
3 | const AutoLaunch = require('auto-launch');
4 |
5 | const autoLaunch = new AutoLaunch({
6 | name: appName,
7 | mac: {
8 | useLaunchAgent: true,
9 | },
10 | });
11 |
12 |
13 | let initSync = false;
14 | const settings = {
15 | clipboardLimit: 30,
16 | autoStart: true,
17 | };
18 |
19 | /**
20 | * @return {Promise}
21 | */
22 | function sync() {
23 | return new Promise((resolve, reject)=>{
24 | settingsDB.find({}).exec(async (error, results) =>{
25 | try {
26 | if (error) {
27 | logger.error('Error in fetching settings from DB:', error);
28 | return reject(error);
29 | }
30 | for (const result of results) {
31 | settings[result.key] = result.value;
32 | }
33 | initSync = true;
34 | return resolve(settings);
35 | } catch (error) {
36 | logger.debug('Error in reading or apply settings: ', error);
37 | return reject(error);
38 | }
39 | });
40 | });
41 | }
42 |
43 | /**
44 | * Save settings to database
45 | * @param {String} key
46 | * @param {Any} value
47 | * @return {Promise}
48 | */
49 | function saveToDB(key, value) {
50 | return new Promise((resolve, reject)=>{
51 | settingsDB.update(
52 | {key: key},
53 | {key, key, value: value},
54 | {upsert: true},
55 | (err)=> {
56 | if (err) {
57 | console.error(err);
58 | logger.error('Error in in update settings: ', err);
59 | return reject(err);
60 | }
61 | return resolve({
62 | [key]: value,
63 | });
64 | });
65 | });
66 | }
67 |
68 |
69 | /**
70 | * Get settings from store
71 | * @param {string} key setting key
72 | * @return {Any} setting value
73 | */
74 | async function get(key) {
75 | if (!initSync) {
76 | await sync();
77 | if (!isDevelopment && settings.autoStart) {
78 | const enabled = await autoLaunch.isEnabled();
79 | logger.debug('autostart status: ', enabled);
80 | if (!enabled) await autoLaunch.enable();
81 | } else {
82 | await autoLaunch.disable();
83 | }
84 | }
85 | return settings[key];
86 | }
87 |
88 | /**
89 | *
90 | * @param {String} key
91 | * @param {Any} value
92 | */
93 | async function set(key, value) {
94 | settings[key] = value;
95 | await saveToDB(key, value);
96 | }
97 |
98 | module.exports = {
99 | get: get,
100 | set: set,
101 | };
102 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const { logger, packageInfos, clipboardDB, appName } = require("./config");
2 | const settings = require("./settings");
3 | const updater = require("./updater.js");
4 |
5 | const { app, Menu, Tray, clipboard, globalShortcut } = require("electron");
6 | const path = require("path");
7 | const md5 = require("md5");
8 |
9 | /* Tray elements */
10 | let tray = null;
11 |
12 | const emptyFillerMenu = { label: "clipboard is empty", enabled: false };
13 | const titleMenu1 = {
14 | label: `${appName} v${packageInfos.version} `,
15 | enabled: false
16 | };
17 | const titleMenu2 = {
18 | label: `_____________________Clipboard History________________________`,
19 | enabled: false
20 | };
21 | const menuSeparator = {
22 | label: "______________________________________________________________",
23 | enabled: false
24 | };
25 |
26 | const clearAllMenu = {
27 | label: "Clear All",
28 | click: clearAllHistory,
29 | accelerator: "Command+Alt+c"
30 | };
31 |
32 | const quitMenu = { label: "Quit", click: app.quit, accelerator: "Command+Q" };
33 |
34 | const settingsMenu = {
35 | label: "Settings",
36 | submenu: []
37 | };
38 | const settingsAutoStartEntry = {
39 | label: "Launch on System startup",
40 | click: setAutostart,
41 | type: "radio"
42 | };
43 | settingsMenu.submenu.push(settingsAutoStartEntry);
44 |
45 | const settingsLimitEntry = {
46 | label: "Clipboard Limit",
47 | submenu: []
48 | };
49 | for (const limit of [30, 50, 100, 200, 400]) {
50 | settingsLimitEntry.submenu.push({
51 | label: String(limit),
52 | value: limit,
53 | click: setClipboardLimit,
54 | type: "radio"
55 | });
56 | }
57 | settingsMenu.submenu.push(settingsLimitEntry);
58 |
59 | /* Tray elements end */
60 |
61 | /**
62 | * Clear history from database
63 | */
64 | function clearAllHistory() {
65 | clipboardDB.remove({}, { multi: true }, function(err, numRemoved) {
66 | if (err) {
67 | logger.error("Error in clearing history: ", err);
68 | } else {
69 | updateTray();
70 | }
71 | });
72 | }
73 |
74 | /**
75 | * Set clipboard limit value
76 | * @param {Number} value clipboard limit size
77 | */
78 | async function setClipboardLimit(value) {
79 | try {
80 | await settings.set("clipboardLimit", value.value);
81 | await updateTray();
82 | } catch (error) {
83 | logger.error("setClipboardLimit error: ", error);
84 | }
85 | }
86 |
87 | /**
88 | * Set auto start
89 | */
90 | async function setAutostart() {
91 | try {
92 | const value = !settingsAutoStartEntry.checked;
93 | await settings.set("autoStart", value);
94 | await updateTray();
95 | } catch (error) {
96 | logger.error("setAutostart error: ", error);
97 | }
98 | }
99 |
100 | /**
101 | * get clipboard limit value from db
102 | * @return {Number}
103 | */
104 | async function getClipboardLimit() {
105 | const clipboardLimit = await settings.get("clipboardLimit");
106 | for (const limitEntry of settingsLimitEntry.submenu) {
107 | if (limitEntry.value == clipboardLimit) {
108 | limitEntry.checked = true;
109 | } else {
110 | limitEntry.checked = false;
111 | }
112 | }
113 | return clipboardLimit;
114 | }
115 |
116 | /**
117 | * get autostart from db
118 | */
119 | async function getAutostart() {
120 | const autoStart = await settings.get("autoStart");
121 | settingsAutoStartEntry.checked = autoStart ? true : false;
122 | return autoStart;
123 | }
124 |
125 | /**
126 | * Read clipboard history from database
127 | * @return {Promise}
128 | */
129 | function getClipboardItems() {
130 | return new Promise(async (resolve, reject) => {
131 | const clipboardLimit = await getClipboardLimit();
132 | clipboardDB
133 | .find({})
134 | .sort({ updated_at: -1 })
135 | .limit(clipboardLimit)
136 | .exec(function(err, results) {
137 | if (err) {
138 | logger.error("DB clipboard find error: ", err);
139 | return reject(err);
140 | }
141 | return resolve(results);
142 | });
143 | });
144 | }
145 |
146 | /**
147 | *
148 | * @param {Any} r
149 | */
150 | function historyClick(r) {
151 | if (r.use_label) {
152 | clipboard.writeText(r.label);
153 | } else {
154 | clipboardDB.findOne({ _id: r._id }, function(err, result) {
155 | if (err) {
156 | return logger.error("[historyClick]Error in reading DB", err);
157 | }
158 | clipboard.writeText(result.text);
159 | });
160 | }
161 | }
162 |
163 | /**
164 | *
165 | * @param {Any} params
166 | */
167 | async function createTray() {
168 | try {
169 | if (!tray || tray.isDestroyed()) {
170 | tray = new Tray(path.join(__dirname, "images/16x16.png"));
171 | }
172 | tray.setToolTip(packageInfos.description);
173 | tray.setTitle(appName);
174 | tray.on("right-click", () => {
175 | tray.popUpContextMenu();
176 | });
177 | } catch (exception) {
178 | logger.error("Exception in create tray: ", exception);
179 | }
180 | }
181 |
182 | /**
183 | *
184 | * @param {Any} params
185 | */
186 | async function updateTray(params) {
187 | try {
188 | if (!params) params = {};
189 |
190 | await getAutostart();
191 | await getClipboardLimit();
192 |
193 | const results = await getClipboardItems();
194 |
195 | const trayItems = [titleMenu1, titleMenu2];
196 |
197 | if (results.length === 0) {
198 | trayItems.push(emptyFillerMenu);
199 | }
200 | let i = 1;
201 | for (const item of results) {
202 | if (item.text && item._id) {
203 | const trayEntry = {
204 | _id: item._id,
205 | label: item.text.slice(0, 50),
206 | click: historyClick,
207 | accelerator: `Command+${i}`
208 | };
209 | if (item.text.length <= 50) {
210 | trayEntry.use_label = true;
211 | }
212 | trayItems.push(trayEntry);
213 | i++;
214 | }
215 | }
216 | trayItems.push(menuSeparator, clearAllMenu, settingsMenu, quitMenu);
217 |
218 | if (!tray || tray.isDestroyed()) {
219 | createTray();
220 | }
221 | const contextMenu = Menu.buildFromTemplate(trayItems);
222 | tray.setContextMenu(contextMenu);
223 |
224 | setTimeout(() => {
225 | if (params.popup) tray.popUpContextMenu();
226 | }, 100);
227 | } catch (exception) {
228 | logger.error("Exception in update tray: ", exception);
229 | }
230 | }
231 |
232 | /**
233 | * Watch clipboard
234 | */
235 | function clipboardWatch() {
236 | let currentValue = clipboard.readText();
237 | setInterval(async () => {
238 | try {
239 | const newValue = clipboard.readText();
240 | if (currentValue !== newValue) {
241 | currentValue = newValue;
242 | await saveClipboard(currentValue);
243 | }
244 | } catch (error) {
245 | logger.error("error in clipboard watch or saveToDb: ", err);
246 | }
247 | }, 200);
248 | }
249 |
250 | /**
251 | * Save settings to database
252 | * @param {String} text
253 | * @return {Promise}
254 | */
255 | function saveClipboard(text) {
256 | return new Promise((resolve, reject) => {
257 | if (!text) {
258 | return resolve();
259 | }
260 | const doc = {
261 | hash: md5(text),
262 | text: text,
263 | updated_at: new Date()
264 | };
265 | clipboardDB.update(
266 | { hash: doc.hash },
267 | doc,
268 | { upsert: true },
269 | (err, numberOfUpdated, upsert) => {
270 | if (err) {
271 | logger.error("Error in in update settings: ", err);
272 | return reject(err);
273 | }
274 | if (upsert || numberOfUpdated) {
275 | updateTray();
276 | }
277 | return resolve({});
278 | }
279 | );
280 | });
281 | }
282 |
283 | if (process.platform == "darwin" && process.env.ENV != "development") {
284 | app.dock.hide();
285 | }
286 | // Quit when all windows are closed.
287 | app.on("window-all-closed", () => {
288 | // On macOS it is common for applications and their menu bar
289 | // to stay active until the user quits explicitly with Cmd + Q
290 | if (process.platform !== "darwin") {
291 | app.quit();
292 | }
293 | });
294 |
295 | app.on("ready", () => {
296 | globalShortcut.register("CommandOrControl+Alt+h", () => {
297 | updateTray({
298 | disabled: true,
299 | popup: true
300 | });
301 | });
302 |
303 | clipboardWatch();
304 | createTray();
305 | updateTray();
306 | updater.checkForUpdates();
307 | if (process.platform == "darwin" && process.env.ENV != "development") {
308 | app.dock.hide();
309 | }
310 | });
311 |
--------------------------------------------------------------------------------