mani-calc 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { exec } = require('child_process');
4
+ const { promisify } = require('util');
5
+
6
+ const execAsync = promisify(exec);
7
+
8
+ class WindowsSearchIntegration {
9
+ constructor(maniCalc) {
10
+ this.maniCalc = maniCalc;
11
+ this.protocolName = 'calc';
12
+ this.appPath = process.execPath;
13
+ this.scriptPath = path.join(__dirname, '..', '..', 'bin', 'cli.js');
14
+ }
15
+
16
+ async register() {
17
+ try {
18
+ // Register protocol handler for Windows Search
19
+ await this.registerProtocolHandler();
20
+
21
+ // Create search connector
22
+ await this.createSearchConnector();
23
+
24
+ return true;
25
+ } catch (error) {
26
+ throw new Error(`Failed to register Windows Search integration: ${error.message}`);
27
+ }
28
+ }
29
+
30
+ async registerProtocolHandler() {
31
+ // Create registry entries for custom protocol
32
+ const regContent = `Windows Registry Editor Version 5.00
33
+
34
+ [HKEY_CURRENT_USER\\Software\\Classes\\calc]
35
+ @="URL:Mani-Calc Protocol"
36
+ "URL Protocol"=""
37
+
38
+ [HKEY_CURRENT_USER\\Software\\Classes\\calc\\shell]
39
+
40
+ [HKEY_CURRENT_USER\\Software\\Classes\\calc\\shell\\open]
41
+
42
+ [HKEY_CURRENT_USER\\Software\\Classes\\calc\\shell\\open\\command]
43
+ @="\\"${process.execPath}\\" \\"${this.scriptPath}\\" \\"%1\\""
44
+ `;
45
+
46
+ const regFile = path.join(process.env.TEMP, 'mani-calc-protocol.reg');
47
+ fs.writeFileSync(regFile, regContent);
48
+
49
+ try {
50
+ await execAsync(`reg import "${regFile}"`);
51
+ fs.unlinkSync(regFile);
52
+ } catch (error) {
53
+ console.warn('Could not register protocol handler automatically. Manual registration may be required.');
54
+ }
55
+ }
56
+
57
+ async createSearchConnector() {
58
+ const connectorPath = path.join(
59
+ process.env.APPDATA,
60
+ 'Microsoft',
61
+ 'Windows',
62
+ 'Libraries',
63
+ 'ManiCalc.searchConnector-ms'
64
+ );
65
+
66
+ const connectorXml = `<?xml version="1.0" encoding="UTF-8"?>
67
+ <searchConnectorDescription xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">
68
+ <description>Mani-Calc - Instant Calculator</description>
69
+ <isSearchOnlyItem>false</isSearchOnlyItem>
70
+ <includeInStartMenuScope>true</includeInStartMenuScope>
71
+ <iconReference>shell32.dll,23</iconReference>
72
+ <templateInfo>
73
+ <folderType>{5c4f28b5-f869-4e84-8e60-f11db97c5cc7}</folderType>
74
+ </templateInfo>
75
+ <simpleLocation>
76
+ <url>calc:</url>
77
+ </simpleLocation>
78
+ </searchConnectorDescription>`;
79
+
80
+ try {
81
+ const dir = path.dirname(connectorPath);
82
+ if (!fs.existsSync(dir)) {
83
+ fs.mkdirSync(dir, { recursive: true });
84
+ }
85
+ fs.writeFileSync(connectorPath, connectorXml);
86
+ } catch (error) {
87
+ console.warn('Could not create search connector:', error.message);
88
+ }
89
+ }
90
+
91
+ async unregister() {
92
+ try {
93
+ // Remove protocol handler
94
+ await execAsync('reg delete "HKEY_CURRENT_USER\\Software\\Classes\\calc" /f').catch(() => { });
95
+
96
+ // Remove search connector
97
+ const connectorPath = path.join(
98
+ process.env.APPDATA,
99
+ 'Microsoft',
100
+ 'Windows',
101
+ 'Libraries',
102
+ 'ManiCalc.searchConnector-ms'
103
+ );
104
+
105
+ if (fs.existsSync(connectorPath)) {
106
+ fs.unlinkSync(connectorPath);
107
+ }
108
+
109
+ return true;
110
+ } catch (error) {
111
+ console.error('Failed to unregister:', error);
112
+ return false;
113
+ }
114
+ }
115
+
116
+ parseProtocolUrl(url) {
117
+ // Parse calc:query format
118
+ const match = url.match(/^calc:(.+)$/i);
119
+ if (match) {
120
+ return decodeURIComponent(match[1]);
121
+ }
122
+ return null;
123
+ }
124
+ }
125
+
126
+ module.exports = WindowsSearchIntegration;
@@ -0,0 +1,228 @@
1
+ const { app, BrowserWindow, globalShortcut, ipcMain } = require('electron');
2
+ const path = require('path');
3
+
4
+ class FloatingSearchBox {
5
+ constructor(maniCalc) {
6
+ this.maniCalc = maniCalc;
7
+ this.window = null;
8
+ this.isVisible = false;
9
+ this.hotkey = 'Alt+Space'; // Default hotkey
10
+ }
11
+
12
+ async initialize() {
13
+ // Wait for Electron app to be ready
14
+ if (!app.isReady()) {
15
+ await app.whenReady();
16
+ }
17
+
18
+ this.createWindow();
19
+ this.registerHotkey();
20
+ this.setupIPC();
21
+ }
22
+
23
+ createWindow() {
24
+ this.window = new BrowserWindow({
25
+ width: 600,
26
+ height: 80,
27
+ frame: false,
28
+ transparent: true,
29
+ alwaysOnTop: true,
30
+ skipTaskbar: true,
31
+ resizable: false,
32
+ show: false,
33
+ webPreferences: {
34
+ nodeIntegration: true,
35
+ contextIsolation: false
36
+ }
37
+ });
38
+
39
+ // Center the window
40
+ const { screen } = require('electron');
41
+ const display = screen.getPrimaryDisplay();
42
+ const { width, height } = display.workAreaSize;
43
+ const x = Math.floor((width - 600) / 2);
44
+ const y = Math.floor(height * 0.3); // 30% from top
45
+
46
+ this.window.setPosition(x, y);
47
+
48
+ // Load the HTML
49
+ this.window.loadFile(path.join(__dirname, 'overlay.html'));
50
+
51
+ // Hide when losing focus
52
+ this.window.on('blur', () => {
53
+ if (this.isVisible) {
54
+ this.hide();
55
+ }
56
+ });
57
+ }
58
+
59
+ registerHotkey() {
60
+ globalShortcut.register(this.hotkey, () => {
61
+ this.toggle();
62
+ });
63
+
64
+ console.log(`✓ Hotkey registered: ${this.hotkey}`);
65
+ }
66
+
67
+ setupIPC() {
68
+ // Handle query from renderer
69
+ ipcMain.on('process-query', async (event, query) => {
70
+ try {
71
+ // Check for system commands first
72
+ const systemResult = await this.handleSystemCommand(query);
73
+ if (systemResult) {
74
+ event.reply('query-result', systemResult);
75
+ setTimeout(() => this.hide(), 1000);
76
+ return;
77
+ }
78
+
79
+ // Process as calculation
80
+ const result = await this.maniCalc.processQuery(query);
81
+ event.reply('query-result', result);
82
+ } catch (error) {
83
+ event.reply('query-result', {
84
+ error: error.message,
85
+ type: 'error'
86
+ });
87
+ }
88
+ });
89
+
90
+ // Handle hide request
91
+ ipcMain.on('hide-window', () => {
92
+ this.hide();
93
+ });
94
+ }
95
+
96
+ async handleSystemCommand(query) {
97
+ const cmd = query.toLowerCase().trim();
98
+ const { exec } = require('child_process');
99
+ const { promisify } = require('util');
100
+ const execAsync = promisify(exec);
101
+
102
+ const commands = {
103
+ 'sleep': {
104
+ action: async () => {
105
+ await execAsync('rundll32.exe powrprof.dll,SetSuspendState 0,1,0');
106
+ return 'System going to sleep...';
107
+ },
108
+ description: 'Put computer to sleep'
109
+ },
110
+ 'shutdown': {
111
+ action: async () => {
112
+ await execAsync('shutdown /s /t 0');
113
+ return 'Shutting down...';
114
+ },
115
+ description: 'Shutdown computer'
116
+ },
117
+ 'restart': {
118
+ action: async () => {
119
+ await execAsync('shutdown /r /t 0');
120
+ return 'Restarting...';
121
+ },
122
+ description: 'Restart computer'
123
+ },
124
+ 'lock': {
125
+ action: async () => {
126
+ await execAsync('rundll32.exe user32.dll,LockWorkStation');
127
+ return 'Locking computer...';
128
+ },
129
+ description: 'Lock computer'
130
+ },
131
+ 'logout': {
132
+ action: async () => {
133
+ await execAsync('shutdown /l');
134
+ return 'Logging out...';
135
+ },
136
+ description: 'Log out current user'
137
+ },
138
+ 'empty recycle bin': {
139
+ action: async () => {
140
+ await execAsync('rd /s /q %systemdrive%\\$Recycle.bin');
141
+ return 'Recycle bin emptied';
142
+ },
143
+ description: 'Empty recycle bin'
144
+ },
145
+ 'volume up': {
146
+ action: async () => {
147
+ await execAsync('nircmd.exe changesysvolume 2000');
148
+ return 'Volume increased';
149
+ },
150
+ description: 'Increase volume'
151
+ },
152
+ 'volume down': {
153
+ action: async () => {
154
+ await execAsync('nircmd.exe changesysvolume -2000');
155
+ return 'Volume decreased';
156
+ },
157
+ description: 'Decrease volume'
158
+ },
159
+ 'mute': {
160
+ action: async () => {
161
+ await execAsync('nircmd.exe mutesysvolume 1');
162
+ return 'Volume muted';
163
+ },
164
+ description: 'Mute volume'
165
+ },
166
+ 'unmute': {
167
+ action: async () => {
168
+ await execAsync('nircmd.exe mutesysvolume 0');
169
+ return 'Volume unmuted';
170
+ },
171
+ description: 'Unmute volume'
172
+ }
173
+ };
174
+
175
+ if (commands[cmd]) {
176
+ try {
177
+ const message = await commands[cmd].action();
178
+ return {
179
+ result: message,
180
+ type: 'system_command',
181
+ formatted: message
182
+ };
183
+ } catch (error) {
184
+ return {
185
+ error: `Failed to execute: ${error.message}`,
186
+ type: 'error'
187
+ };
188
+ }
189
+ }
190
+
191
+ return null;
192
+ }
193
+
194
+ show() {
195
+ if (this.window) {
196
+ this.window.show();
197
+ this.window.focus();
198
+ this.isVisible = true;
199
+ this.window.webContents.send('focus-input');
200
+ }
201
+ }
202
+
203
+ hide() {
204
+ if (this.window) {
205
+ this.window.hide();
206
+ this.isVisible = false;
207
+ this.window.webContents.send('clear-input');
208
+ }
209
+ }
210
+
211
+ toggle() {
212
+ if (this.isVisible) {
213
+ this.hide();
214
+ } else {
215
+ this.show();
216
+ }
217
+ }
218
+
219
+ destroy() {
220
+ if (this.window) {
221
+ globalShortcut.unregister(this.hotkey);
222
+ this.window.close();
223
+ this.window = null;
224
+ }
225
+ }
226
+ }
227
+
228
+ module.exports = FloatingSearchBox;
@@ -0,0 +1,254 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Mani-Calc</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
16
+ background: transparent;
17
+ -webkit-app-region: drag;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .container {
22
+ width: 600px;
23
+ padding: 10px;
24
+ }
25
+
26
+ .search-box {
27
+ background: rgba(255, 255, 255, 0.95);
28
+ backdrop-filter: blur(20px);
29
+ border-radius: 12px;
30
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
31
+ overflow: hidden;
32
+ -webkit-app-region: no-drag;
33
+ }
34
+
35
+ .input-container {
36
+ display: flex;
37
+ align-items: center;
38
+ padding: 16px 20px;
39
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
40
+ }
41
+
42
+ .icon {
43
+ font-size: 24px;
44
+ margin-right: 12px;
45
+ color: #00D9FF;
46
+ }
47
+
48
+ #search-input {
49
+ flex: 1;
50
+ border: none;
51
+ outline: none;
52
+ font-size: 18px;
53
+ background: transparent;
54
+ color: #333;
55
+ }
56
+
57
+ #search-input::placeholder {
58
+ color: #999;
59
+ }
60
+
61
+ .result-container {
62
+ padding: 12px 20px;
63
+ background: rgba(0, 217, 255, 0.05);
64
+ display: none;
65
+ animation: slideDown 0.2s ease;
66
+ }
67
+
68
+ .result-container.show {
69
+ display: block;
70
+ }
71
+
72
+ @keyframes slideDown {
73
+ from {
74
+ opacity: 0;
75
+ transform: translateY(-10px);
76
+ }
77
+ to {
78
+ opacity: 1;
79
+ transform: translateY(0);
80
+ }
81
+ }
82
+
83
+ .result-text {
84
+ font-size: 16px;
85
+ color: #00D9FF;
86
+ font-weight: 600;
87
+ }
88
+
89
+ .result-text.error {
90
+ color: #ff4444;
91
+ }
92
+
93
+ .result-text.success {
94
+ color: #00D9FF;
95
+ }
96
+
97
+ .result-text.system {
98
+ color: #ff9800;
99
+ }
100
+
101
+ .hint {
102
+ font-size: 12px;
103
+ color: #999;
104
+ margin-top: 4px;
105
+ }
106
+
107
+ .shortcuts {
108
+ padding: 8px 20px;
109
+ background: rgba(0, 0, 0, 0.02);
110
+ display: none;
111
+ font-size: 11px;
112
+ color: #666;
113
+ }
114
+
115
+ .shortcuts.show {
116
+ display: block;
117
+ }
118
+
119
+ .shortcut-item {
120
+ display: inline-block;
121
+ margin-right: 12px;
122
+ }
123
+
124
+ .key {
125
+ background: rgba(0, 0, 0, 0.1);
126
+ padding: 2px 6px;
127
+ border-radius: 3px;
128
+ font-family: monospace;
129
+ }
130
+ </style>
131
+ </head>
132
+ <body>
133
+ <div class="container">
134
+ <div class="search-box">
135
+ <div class="input-container">
136
+ <div class="icon">⚡</div>
137
+ <input
138
+ type="text"
139
+ id="search-input"
140
+ placeholder="Type calculation or command..."
141
+ autocomplete="off"
142
+ spellcheck="false"
143
+ >
144
+ </div>
145
+
146
+ <div class="result-container" id="result-container">
147
+ <div class="result-text" id="result-text"></div>
148
+ <div class="hint" id="result-hint"></div>
149
+ </div>
150
+
151
+ <div class="shortcuts" id="shortcuts">
152
+ <span class="shortcut-item"><span class="key">Enter</span> Execute</span>
153
+ <span class="shortcut-item"><span class="key">Esc</span> Close</span>
154
+ <span class="shortcut-item"><span class="key">Alt+Space</span> Toggle</span>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <script>
160
+ const { ipcRenderer } = require('electron');
161
+
162
+ const input = document.getElementById('search-input');
163
+ const resultContainer = document.getElementById('result-container');
164
+ const resultText = document.getElementById('result-text');
165
+ const resultHint = document.getElementById('result-hint');
166
+ const shortcuts = document.getElementById('shortcuts');
167
+
168
+ let debounceTimer;
169
+
170
+ // Focus input when window is shown
171
+ ipcRenderer.on('focus-input', () => {
172
+ input.focus();
173
+ input.select();
174
+ shortcuts.classList.add('show');
175
+ });
176
+
177
+ // Clear input when window is hidden
178
+ ipcRenderer.on('clear-input', () => {
179
+ input.value = '';
180
+ resultContainer.classList.remove('show');
181
+ shortcuts.classList.remove('show');
182
+ });
183
+
184
+ // Handle input
185
+ input.addEventListener('input', (e) => {
186
+ const query = e.target.value.trim();
187
+
188
+ if (!query) {
189
+ resultContainer.classList.remove('show');
190
+ return;
191
+ }
192
+
193
+ // Debounce for live preview
194
+ clearTimeout(debounceTimer);
195
+ debounceTimer = setTimeout(() => {
196
+ processQuery(query, true);
197
+ }, 300);
198
+ });
199
+
200
+ // Handle Enter key
201
+ input.addEventListener('keydown', (e) => {
202
+ if (e.key === 'Enter') {
203
+ const query = input.value.trim();
204
+ if (query) {
205
+ processQuery(query, false);
206
+ }
207
+ } else if (e.key === 'Escape') {
208
+ ipcRenderer.send('hide-window');
209
+ }
210
+ });
211
+
212
+ // Process query
213
+ function processQuery(query, isPreview) {
214
+ if (!isPreview) {
215
+ ipcRenderer.send('process-query', query);
216
+ } else {
217
+ // Show preview for calculations
218
+ try {
219
+ // Simple preview for math expressions
220
+ if (/^[\d\s\+\-\*\/\(\)\.\^]+$/.test(query)) {
221
+ showResult('Calculating...', 'Press Enter to execute', 'success');
222
+ }
223
+ } catch (error) {
224
+ // Ignore preview errors
225
+ }
226
+ }
227
+ }
228
+
229
+ // Handle result from main process
230
+ ipcRenderer.on('query-result', (event, result) => {
231
+ if (result.error) {
232
+ showResult(`✗ ${result.error}`, '', 'error');
233
+ } else if (result.type === 'system_command') {
234
+ showResult(`✓ ${result.formatted}`, 'Copied to clipboard', 'system');
235
+ } else {
236
+ showResult(`✓ ${result.formatted}`, 'Copied to clipboard', 'success');
237
+ }
238
+ });
239
+
240
+ // Show result
241
+ function showResult(text, hint, type) {
242
+ resultText.textContent = text;
243
+ resultText.className = `result-text ${type}`;
244
+ resultHint.textContent = hint;
245
+ resultContainer.classList.add('show');
246
+ }
247
+
248
+ // Auto-focus on load
249
+ setTimeout(() => {
250
+ input.focus();
251
+ }, 100);
252
+ </script>
253
+ </body>
254
+ </html>