codex-rtl 0.0.2 → 1.0.1

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.
package/README.md CHANGED
@@ -11,17 +11,25 @@ This CLI tool automatically injects a sophisticated RTL engine into Codex, addin
11
11
  - **Persian Keyboard Fix**: Maps `Shift + 2` to type `@` instead of `٬` on Persian keyboards.
12
12
  - **Beautiful Settings Panel**: A floating, non-intrusive UI widget at the bottom right corner.
13
13
  - **Vazirmatn Built-in**: Comes with the beautiful Vazirmatn variable font by default.
14
+ - **Theme Compatibility**: Seamlessly adapts colors based on Codex's active theme, using native color variables.
14
15
 
15
16
  ## Installation
16
17
 
17
18
  You don't need to download any files. Just run the following command in your terminal:
18
19
 
19
- ### macOS / Linux
20
- Because the tool needs to modify the Codex application files, you must run it with `sudo`:
20
+ ### macOS
21
+ Simply run:
22
+ ```bash
23
+ npx codex-rtl
24
+ ```
25
+ > **First time?** If you get a "Permission Denied" error, the tool will **automatically open** the App Management settings page for you. Just enable the toggle for your terminal app, then run the command again. No `sudo` needed!
26
+ >
27
+ > You can also grant permission manually: `System Settings > Privacy & Security > App Management`.
28
+
29
+ ### Linux
21
30
  ```bash
22
31
  sudo npx codex-rtl
23
32
  ```
24
- > **macOS Users:** If you get a "Permission Denied" error even with sudo, ensure your terminal (e.g. Terminal, iTerm2, VS Code) has **App Management** permissions enabled in `System Settings > Privacy & Security > App Management`.
25
33
 
26
34
  ### Windows
27
35
  Open **PowerShell** or **Command Prompt** as **Administrator** (Right-click -> Run as Administrator), then run:
@@ -39,9 +47,9 @@ npx codex-rtl
39
47
  If you ever want to revert Codex back to its original state (before the patch), simply run the command with the `--restore` flag:
40
48
 
41
49
  ```bash
42
- sudo npx codex-rtl --restore
50
+ npx codex-rtl --restore
43
51
  ```
44
- *(On Windows, run without `sudo` in an Administrator terminal)*
52
+ *(On Linux, run with `sudo`. On Windows, run in an Administrator terminal)*
45
53
 
46
54
  ## How it works
47
55
 
@@ -74,17 +82,25 @@ Feel free to open issues or submit pull requests. Let's make Codex accessible an
74
82
  - **حل مشکل کیبورد فارسی**: این ابزار کلید ترکیبی `Shift + 2` روی کیبورد فارسی را اصلاح می‌کند تا به جای «٬» علامت `@` تایپ شود.
75
83
  - **پنل تنظیمات زیبا**: تمام این تنظیمات در یک ویجتِ کوچک، مدرن و شناور در پایینِ صفحه قرار گرفته‌اند.
76
84
  - **فونت وزیرمتن**: فونت زیبای Vazirmatn Variable به صورت پیش‌فرض در این افزونه گنجانده شده است.
85
+ - **همگام‌سازی خودکار با تم (Theme Compatibility)**: هماهنگی و تغییر پویای رنگ سوییچ‌های پنل با تغییر تم رنگی Codex به صورت کاملاً بومی.
77
86
 
78
87
  ## آموزش نصب
79
88
 
80
89
  بدون نیاز به دانلود هیچ فایلی، فقط کافیست دستور زیر را در ترمینال سیستم خود اجرا کنید:
81
90
 
82
- ### در مک (macOS) و لینوکس
83
- از آنجایی که این ابزار قرار است فایل‌های سیستمی Codex را ویرایش کند، باید حتماً دسترسی `sudo` داشته باشد:
91
+ ### در مک (macOS)
92
+ کافیست دستور زیر را اجرا کنید:
93
+ ```bash
94
+ npx codex-rtl
95
+ ```
96
+ > **اولین بار؟** اگر خطای Permission Denied دریافت کردید، ابزار به صورت **خودکار** صفحهٔ تنظیمات App Management را برای شما باز می‌کند. فقط سوئیچ ترمینال خود (مثلاً Terminal، iTerm2 یا VS Code) را فعال کنید و دوباره دستور را اجرا کنید. نیازی به `sudo` نیست!
97
+ >
98
+ > همچنین می‌توانید به صورت دستی به مسیر `System Settings > Privacy & Security > App Management` بروید.
99
+
100
+ ### در لینوکس
84
101
  ```bash
85
102
  sudo npx codex-rtl
86
103
  ```
87
- > **کاربران مک (macOS):** اگر با وجود استفاده از sudo باز هم خطای Permission Denied دریافت کردید، باید به ترمینال خود (مثل Terminal، iTerm2 یا VS Code) دسترسی **App Management** بدهید. برای این کار به مسیر `System Settings > Privacy & Security > App Management` بروید و دسترسی ترمینال خود را فعال کنید.
88
104
 
89
105
  ### در ویندوز
90
106
  برنامهٔ **PowerShell** یا **Command Prompt** را در حالت **Administrator** (راست‌کلیک -> Run as Administrator) باز کنید و دستور زیر را بنویسید:
@@ -102,9 +118,9 @@ npx codex-rtl
102
118
  اگر زمانی خواستید Codex را به حالتِ اولیه (قبل از نصب این پچ) برگردانید، فقط کافیست دستور بالا را با فلگ `--restore` اجرا کنید:
103
119
 
104
120
  ```bash
105
- sudo npx codex-rtl --restore
121
+ npx codex-rtl --restore
106
122
  ```
107
- *(کاربران ویندوز این دستور را بدون `sudo` و در یک ترمینال ادمین اجرا کنند)*
123
+ *(در لینوکس با `sudo` اجرا کنید. کاربران ویندوز این دستور را در یک ترمینال ادمین اجرا کنند)*
108
124
 
109
125
  ## این ابزار چگونه کار می‌کند؟
110
126
 
Binary file
package/bin/index.js CHANGED
@@ -1,3 +1,268 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { fileURLToPath } from 'url';
6
+ import picocolors from 'picocolors';
7
+ import ora from 'ora';
8
+ import prompts from 'prompts';
9
+ import * as asar from '@electron/asar';
10
+ import { execSync } from 'child_process';
2
11
 
3
- console.log("codex-rtl has been successfully reserved on npm. Stay tuned for future releases!");
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const { blue, cyan, green, red, yellow, bold } = picocolors;
15
+
16
+ const pkgPath = path.join(__dirname, '..', 'package.json');
17
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
18
+ console.log(bold(cyan(`\n✨ Codex Smart RTL Patcher v${pkg.version}\n`)));
19
+
20
+ function handleMacPermissionError(err) {
21
+ if (os.platform() === 'darwin') {
22
+ console.error(yellow('\nOn macOS, you can either:'));
23
+ console.error(yellow(' 1. Grant your terminal "App Management" permission to run without sudo.'));
24
+ console.error(yellow(' 2. Or, run this command with sudo (e.g. sudo npx codex-rtl)'));
25
+ console.log(blue('\nOpening System Settings directly to App Management for you...'));
26
+ try {
27
+ execSync('open "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AppBundles"');
28
+ console.log(green('✔ Settings opened! Please enable the toggle for your terminal, then try again.\n'));
29
+ } catch (e) {
30
+ console.error(yellow('To open manually, go to: System Settings > Privacy & Security > App Management\n'));
31
+ }
32
+ }
33
+ }
34
+
35
+ function getDefaultPath() {
36
+ if (os.platform() === 'darwin') {
37
+ return '/Applications/Codex.app/Contents/Resources/app.asar';
38
+ } else if (os.platform() === 'win32') {
39
+ return path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Codex', 'resources', 'app.asar');
40
+ } else {
41
+ return '/opt/Codex/resources/app.asar';
42
+ }
43
+ }
44
+
45
+ async function getAsarPath() {
46
+ let asarPath = getDefaultPath();
47
+ if (fs.existsSync(asarPath)) {
48
+ console.log(blue(`ℹ Found Codex installation at:`));
49
+ console.log(` ${asarPath}\n`);
50
+ return asarPath;
51
+ }
52
+
53
+ console.log(yellow(`⚠ Could not find Codex at default location.`));
54
+ const response = await prompts({
55
+ type: 'text',
56
+ name: 'customPath',
57
+ message: 'Please enter the full path to app.asar:'
58
+ });
59
+
60
+ if (!response.customPath || !fs.existsSync(response.customPath)) {
61
+ console.error(red('\n✖ Invalid path. Aborting.\n'));
62
+ process.exit(1);
63
+ }
64
+ return response.customPath;
65
+ }
66
+
67
+ const args = process.argv.slice(2);
68
+ const isRestore = args.includes('--restore');
69
+
70
+ async function main() {
71
+ const asarPath = await getAsarPath();
72
+ const backupPath = asarPath + '.bak';
73
+
74
+ if (isRestore) {
75
+ if (!fs.existsSync(backupPath)) {
76
+ console.error(red('✖ No backup found to restore.\n'));
77
+ process.exit(1);
78
+ }
79
+ const spinner = ora('Restoring original app.asar...').start();
80
+ try {
81
+ fs.copyFileSync(backupPath, asarPath);
82
+ spinner.succeed('Successfully restored original Codex!\n');
83
+ process.exit(0);
84
+ } catch (e) {
85
+ spinner.fail('Failed to restore.');
86
+ console.error(red(e.message));
87
+ handleMacPermissionError(e);
88
+ process.exit(1);
89
+ }
90
+ }
91
+
92
+ const spinner = ora('Checking permissions and backing up...').start();
93
+ try {
94
+ fs.accessSync(path.dirname(asarPath), fs.constants.W_OK);
95
+ if (!fs.existsSync(backupPath)) {
96
+ fs.copyFileSync(asarPath, backupPath);
97
+ }
98
+ } catch (e) {
99
+ spinner.fail('Permission Denied.');
100
+ console.error(red('\nSystem Error: ' + e.message));
101
+ if (os.platform() === 'win32') {
102
+ console.error(yellow('\nPlease run your terminal (PowerShell/CMD) as Administrator and try again.\n'));
103
+ } else if (os.platform() === 'darwin') {
104
+ handleMacPermissionError(e);
105
+ } else {
106
+ console.error(yellow('\nPlease run this command with sudo.\n'));
107
+ }
108
+ process.exit(1);
109
+ }
110
+
111
+ const extractDir = path.join(path.dirname(asarPath), 'app-extracted-codex-rtl-temp');
112
+ spinner.text = 'Extracting app.asar (this may take a few seconds)...';
113
+ try {
114
+ if (fs.existsSync(extractDir)) {
115
+ fs.rmSync(extractDir, { recursive: true, force: true });
116
+ }
117
+ asar.extractAll(asarPath, extractDir);
118
+ } catch (e) {
119
+ spinner.fail('Failed to extract ASAR.');
120
+ console.error(red(e.message));
121
+ process.exit(1);
122
+ }
123
+
124
+ spinner.text = 'Injecting RTL features and enabling DevTools...';
125
+ try {
126
+ const buildDir = path.join(extractDir, '.vite', 'build');
127
+ if (!fs.existsSync(buildDir)) {
128
+ throw new Error('.vite/build not found in ASAR. Unsupported Codex version.');
129
+ }
130
+
131
+ // 1. Find main-*.js
132
+ const files = fs.readdirSync(buildDir);
133
+ const mainFiles = files.filter(f => f.startsWith('main-') && f.endsWith('.js'));
134
+
135
+ for (const mainFile of mainFiles) {
136
+ const mainJsPath = path.join(buildDir, mainFile);
137
+ let mainCode = fs.readFileSync(mainJsPath, 'utf8');
138
+
139
+ // Force devTools: true
140
+ const devtoolsRegex = /devTools:\s*this\.options\.allowDevtools/g;
141
+ if (devtoolsRegex.test(mainCode)) {
142
+ mainCode = mainCode.replace(devtoolsRegex, 'devTools:true');
143
+ fs.writeFileSync(mainJsPath, mainCode, 'utf8');
144
+ }
145
+ }
146
+
147
+ // 2. Inject RTL Loader into bootstrap.js
148
+ const bootstrapPath = path.join(buildDir, 'bootstrap.js');
149
+ if (!fs.existsSync(bootstrapPath)) {
150
+ throw new Error('.vite/build/bootstrap.js not found in ASAR. Unsupported Codex version.');
151
+ }
152
+
153
+ let bootstrapCode = fs.readFileSync(bootstrapPath, 'utf8');
154
+
155
+ if (bootstrapCode.includes('/* CODEX RTL PATCH START */')) {
156
+ spinner.succeed('Codex is already patched!');
157
+ fs.rmSync(extractDir, { recursive: true, force: true });
158
+ console.log(green('\n✨ Enjoy your RTL experience!\n'));
159
+ process.exit(0);
160
+ }
161
+
162
+ const loaderCode = `
163
+ /* CODEX RTL PATCH START */
164
+ try {
165
+ const { app } = require('electron');
166
+ app.on('browser-window-created', (event, win) => {
167
+ // DevTools Shortcut Handler (F12, Cmd+Option+I, Ctrl+Shift+I)
168
+ win.webContents.on('before-input-event', (ev, input) => {
169
+ const isShortcut = input.key === 'F12' ||
170
+ (input.control && input.shift && input.key.toLowerCase() === 'i') ||
171
+ (input.meta && input.alt && input.key.toLowerCase() === 'i');
172
+ if (isShortcut && input.type === 'keyDown') {
173
+ try {
174
+ win.webContents.toggleDevTools();
175
+ ev.preventDefault();
176
+ } catch (e) {}
177
+ }
178
+ });
179
+
180
+ win.webContents.on('console-message', (ev, level, message) => {
181
+ if (typeof message === 'string' && message.startsWith('SAVE_RTL_CONFIG|')) {
182
+ try {
183
+ const data = message.substring(16);
184
+ const configPath = require('path').join(require('os').homedir(), '.codex-rtl.json');
185
+ require('fs').writeFileSync(configPath, data);
186
+ } catch (e) {}
187
+ }
188
+ });
189
+
190
+ win.webContents.on('dom-ready', () => {
191
+ try {
192
+ const path = require('path');
193
+ const fs = require('fs');
194
+ const fontPath = path.join(__dirname, 'Vazirmatn-Variable.woff2');
195
+ const payloadPath = path.join(__dirname, 'payload.js');
196
+ if (!fs.existsSync(fontPath) || !fs.existsSync(payloadPath)) return;
197
+
198
+ const fontBase64 = fs.readFileSync(fontPath).toString('base64');
199
+ let payload = fs.readFileSync(payloadPath, 'utf8');
200
+
201
+ // Read config
202
+ let rtlConfig = { faFont: '', enFont: '', codeFont: '', lh: '1.6', isRTL: true, forceRTL: false, fixAtSign: true };
203
+ try {
204
+ const configPath = path.join(require('os').homedir(), '.codex-rtl.json');
205
+ if (fs.existsSync(configPath)) {
206
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
207
+ rtlConfig = { ...rtlConfig, ...cfg };
208
+ }
209
+ } catch (e) {}
210
+
211
+ // Replace placeholders in payload
212
+ payload = payload.replace('__FONT_BASE64__', fontBase64);
213
+ payload = payload.replace('__RTL_CONFIG__', JSON.stringify(rtlConfig));
214
+
215
+ win.webContents.executeJavaScript(payload).catch(err => console.error("Failed to inject RTL:", err));
216
+ } catch (e) {
217
+ console.error("Failed to read RTL patch assets:", e);
218
+ }
219
+ });
220
+ });
221
+ } catch(e) {
222
+ console.error("RTL patch initialization failed:", e);
223
+ }
224
+ /* CODEX RTL PATCH END */
225
+ `;
226
+
227
+ // Append loader to bootstrap.js
228
+ bootstrapCode += '\n' + loaderCode;
229
+ fs.writeFileSync(bootstrapPath, bootstrapCode, 'utf8');
230
+
231
+ // 3. Copy font file
232
+ const fontSource = path.join(__dirname, 'Vazirmatn-Variable.woff2');
233
+ const fontDest = path.join(buildDir, 'Vazirmatn-Variable.woff2');
234
+ if (fs.existsSync(fontSource)) {
235
+ fs.copyFileSync(fontSource, fontDest);
236
+ }
237
+
238
+ // 4. Copy payload file
239
+ const payloadSource = path.join(__dirname, 'payload.js');
240
+ const payloadDest = path.join(buildDir, 'payload.js');
241
+ if (fs.existsSync(payloadSource)) {
242
+ fs.copyFileSync(payloadSource, payloadDest);
243
+ }
244
+
245
+ } catch (e) {
246
+ spinner.fail('Injection failed.');
247
+ console.error(red(e.message));
248
+ if (fs.existsSync(extractDir)) fs.rmSync(extractDir, { recursive: true, force: true });
249
+ process.exit(1);
250
+ }
251
+
252
+ spinner.text = 'Repacking app.asar (almost done)...';
253
+ try {
254
+ await asar.createPackage(extractDir, asarPath);
255
+ fs.rmSync(extractDir, { recursive: true, force: true });
256
+ spinner.succeed('Successfully patched Codex!');
257
+ console.log(green('\n✨ RTL Features and DevTools have been enabled. Please restart Codex to see the changes.\n'));
258
+ } catch (e) {
259
+ spinner.fail('Failed to repack ASAR.');
260
+ console.error(red(e.message));
261
+ process.exit(1);
262
+ }
263
+ }
264
+
265
+ main().catch(e => {
266
+ console.error(red('\n✖ An unexpected error occurred:'), e.message);
267
+ process.exit(1);
268
+ });
package/bin/payload.js ADDED
@@ -0,0 +1,454 @@
1
+ const fontBase64 = '__FONT_BASE64__';
2
+ const rtlConfig = __RTL_CONFIG__;
3
+
4
+ let isRTL = rtlConfig.isRTL !== false;
5
+ let forceRTL = rtlConfig.forceRTL === true;
6
+ let fixAtSign = rtlConfig.fixAtSign !== false;
7
+ let savedFaFont = rtlConfig.faFont || '';
8
+ let savedEnFont = rtlConfig.enFont || '';
9
+ let savedCodeFont = rtlConfig.codeFont || '';
10
+ let savedLH = rtlConfig.lh || '1.6';
11
+
12
+ if (!document.getElementById('rtl-widget-style')) {
13
+ let widgetStyle = document.createElement('style');
14
+ widgetStyle.id = 'rtl-widget-style';
15
+ widgetStyle.innerHTML = `
16
+ .rtl-widget-container {
17
+ position: fixed;
18
+ bottom: 16px;
19
+ right: 16px;
20
+ z-index: 99999;
21
+ direction: ltr;
22
+ font-family: inherit;
23
+ }
24
+ .rtl-widget-trigger {
25
+ width: 40px;
26
+ height: 40px;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ border-radius: 9999px;
31
+ background-color: var(--scrollbar-thumb, #555);
32
+ color: #fff;
33
+ cursor: pointer;
34
+ opacity: 0.8;
35
+ transition: all 0.3s ease-in-out;
36
+ }
37
+ .rtl-widget-trigger:hover {
38
+ transform: scale(1.1);
39
+ opacity: 1;
40
+ }
41
+ .rtl-widget-container:hover .rtl-widget-trigger {
42
+ opacity: 0;
43
+ transform: scale(0.5);
44
+ pointer-events: none;
45
+ }
46
+ .rtl-widget-panel {
47
+ position: absolute;
48
+ bottom: 0;
49
+ right: 0;
50
+ width: 270px;
51
+ transform: scale(0);
52
+ transform-origin: bottom right;
53
+ opacity: 0;
54
+ pointer-events: none;
55
+ transition: all 0.3s ease-in-out;
56
+ }
57
+ .rtl-widget-container:hover .rtl-widget-panel {
58
+ transform: scale(1);
59
+ opacity: 1;
60
+ pointer-events: auto;
61
+ }
62
+ .rtl-tooltip {
63
+ visibility: hidden;
64
+ opacity: 0;
65
+ transition: opacity 0.15s ease-in-out;
66
+ pointer-events: none;
67
+ position: absolute;
68
+ bottom: 100%;
69
+ left: 50%;
70
+ transform: translateX(-50%);
71
+ margin-bottom: 8px;
72
+ width: 200px;
73
+ padding: 8px;
74
+ border-radius: 8px;
75
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5);
76
+ z-index: 999999 !important;
77
+ white-space: normal;
78
+ text-align: center;
79
+ background-color: #202123 !important;
80
+ background-color: light-dark(#ffffff, #202123) !important;
81
+ border: 1px solid #4d4d4d !important;
82
+ border: 1px solid light-dark(#e5e7eb, #4d4d4d) !important;
83
+ color: var(--color-token-foreground, #fff) !important;
84
+ font-size: 11px;
85
+ line-height: 1.4;
86
+ }
87
+ .rtl-tooltip::after {
88
+ content: "";
89
+ position: absolute;
90
+ top: 100%;
91
+ left: 50%;
92
+ transform: translateX(-50%);
93
+ border-width: 5px;
94
+ border-style: solid;
95
+ border-color: #4d4d4d transparent transparent transparent !important;
96
+ border-color: light-dark(#e5e7eb, #4d4d4d) transparent transparent transparent !important;
97
+ }
98
+ .rtl-info-icon {
99
+ position: relative;
100
+ display: inline-flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ z-index: 50;
104
+ opacity: 0.5;
105
+ transition: opacity 0.15s ease-in-out;
106
+ }
107
+ .rtl-info-icon:hover {
108
+ opacity: 1;
109
+ }
110
+ .rtl-info-icon:hover .rtl-tooltip {
111
+ visibility: visible;
112
+ opacity: 1;
113
+ }
114
+ .rtl-github-link {
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ gap: 8px;
119
+ font-size: 12px;
120
+ font-weight: 600;
121
+ opacity: 0.7;
122
+ text-decoration: none;
123
+ padding-top: 4px;
124
+ padding-bottom: 2px;
125
+ transition: all 0.15s ease-in-out;
126
+ color: var(--color-token-foreground, #fff) !important;
127
+ }
128
+ .rtl-github-link:hover {
129
+ opacity: 1;
130
+ color: #ffd700 !important; /* Gold color */
131
+ }
132
+ `;
133
+ document.head.appendChild(widgetStyle);
134
+ }
135
+
136
+ const rtlStyle = document.createElement('style');
137
+ rtlStyle.id = 'codex-rtl-style';
138
+
139
+ const updateDynamicCSS = () => {
140
+ if (!isRTL) {
141
+ if (rtlStyle.parentNode) rtlStyle.parentNode.removeChild(rtlStyle);
142
+ return;
143
+ }
144
+
145
+ let faFontRule = '';
146
+ let faFontName = "'PersianOnlyFont'";
147
+
148
+ if (savedFaFont) {
149
+ faFontName = "'UserPersianFont', 'PersianOnlyFont'";
150
+ let baseFaFont = savedFaFont.replace(/[-\s]?Regular$/i, '');
151
+ faFontRule = `
152
+ @font-face {
153
+ font-family: 'UserPersianFont';
154
+ src: local('${savedFaFont}'), local('${baseFaFont}');
155
+ font-weight: 400;
156
+ unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF;
157
+ }
158
+ @font-face {
159
+ font-family: 'UserPersianFont';
160
+ src: local('${baseFaFont} Bold'), local('${baseFaFont}-Bold'), local('${baseFaFont}Bold');
161
+ font-weight: 700;
162
+ unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF;
163
+ }
164
+ `;
165
+ }
166
+
167
+ let enFontStr = savedEnFont ? `'${savedEnFont}', ui-sans-serif, system-ui, sans-serif` : 'ui-sans-serif, system-ui, sans-serif';
168
+ let codeFontStr = savedCodeFont ? `'${savedCodeFont}', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace` : 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace';
169
+
170
+ let forceRtlStyle = forceRTL ? `
171
+ /* Force RTL layout for all text components excluding the widget panel */
172
+ p:not(.rtl-widget-container *),
173
+ li:not(.rtl-widget-container *),
174
+ h1:not(.rtl-widget-container *),
175
+ h2:not(.rtl-widget-container *),
176
+ h3:not(.rtl-widget-container *),
177
+ h4:not(.rtl-widget-container *),
178
+ h5:not(.rtl-widget-container *),
179
+ h6:not(.rtl-widget-container *),
180
+ textarea:not(.rtl-widget-container *),
181
+ [contenteditable="true"]:not(.rtl-widget-container *),
182
+ [contenteditable="true"] p:not(.rtl-widget-container *),
183
+ [data-lexical-text="true"]:not(.rtl-widget-container *) {
184
+ direction: rtl !important;
185
+ text-align: right !important;
186
+ unicode-bidi: isolate !important;
187
+ }
188
+ ` : '';
189
+
190
+ rtlStyle.textContent = `
191
+ ${faFontRule}
192
+ @font-face {
193
+ font-family: 'PersianOnlyFont';
194
+ src: url('data:font/woff2;base64,${fontBase64}') format('woff2');
195
+ font-weight: 100 900;
196
+ unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF;
197
+ }
198
+
199
+ :root, :host, html, body {
200
+ font-family: ${faFontName}, ${enFontStr}, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
201
+ --diffs-font-family: ${codeFontStr} !important;
202
+ --diffs-font-fallback: ${codeFontStr} !important;
203
+ }
204
+
205
+ /* Smart RTL using CSS unicode-bidi plaintext */
206
+ p, h1, h2, h3, h4, h5, h6, li, span, div, [role="presentation"] {
207
+ unicode-bidi: plaintext !important;
208
+ text-align: start !important;
209
+ }
210
+
211
+ /* Explicitly keep our widget panel LTR and left-aligned */
212
+ .rtl-widget-container, .rtl-widget-container * {
213
+ direction: ltr !important;
214
+ text-align: left !important;
215
+ unicode-bidi: isolate !important;
216
+ }
217
+
218
+ /* Force RTL rules if enabled */
219
+ ${forceRtlStyle}
220
+
221
+ /* Ensure code blocks are kept LTR and get the code font applied */
222
+ pre:not(.rtl-widget-container *),
223
+ code:not(.rtl-widget-container *),
224
+ pre:not(.rtl-widget-container *) *,
225
+ code:not(.rtl-widget-container *) *,
226
+ pre:not(.rtl-widget-container *) span,
227
+ code:not(.rtl-widget-container *) span,
228
+ [data-line] span {
229
+ unicode-bidi: isolate !important;
230
+ direction: ltr !important;
231
+ text-align: left !important;
232
+ font-family: ${codeFontStr} !important;
233
+ }
234
+
235
+ /* Text input fields and lexical text editors */
236
+ textarea:not(.rtl-widget-container *),
237
+ [contenteditable="true"]:not(.rtl-widget-container *),
238
+ [contenteditable="true"] p:not(.rtl-widget-container *),
239
+ [data-lexical-text="true"]:not(.rtl-widget-container *) {
240
+ unicode-bidi: plaintext !important;
241
+ text-align: start !important;
242
+ }
243
+
244
+ /* Adjust list padding when elements render as RTL */
245
+ ul:not(#_)[dir="rtl"], ol:not(#_)[dir="rtl"],
246
+ [dir="rtl"] ul:not(#_), [dir="rtl"] ol:not(#_) {
247
+ padding-left: 0 !important;
248
+ padding-right: 1.25rem !important;
249
+ }
250
+
251
+ /* Custom Line Height */
252
+ p:not(.rtl-widget-container *),
253
+ li:not(.rtl-widget-container *),
254
+ h1:not(.rtl-widget-container *),
255
+ h2:not(.rtl-widget-container *),
256
+ h3:not(.rtl-widget-container *),
257
+ [contenteditable="true"] p:not(.rtl-widget-container *),
258
+ [data-lexical-text="true"]:not(.rtl-widget-container *) {
259
+ line-height: ${savedLH} !important;
260
+ }
261
+ `;
262
+
263
+ if (!rtlStyle.parentNode) {
264
+ document.head.appendChild(rtlStyle);
265
+ }
266
+ };
267
+
268
+ // Initial apply
269
+ updateDynamicCSS();
270
+
271
+ // Global Shift+2 keyboard handler for @
272
+ document.addEventListener('keydown', (e) => {
273
+ if (!fixAtSign) return;
274
+ if (e.code === 'Digit2' && e.shiftKey) {
275
+ if (e.key === '٬' || e.key === '،') {
276
+ e.preventDefault();
277
+ document.execCommand('insertText', false, '@');
278
+ }
279
+ }
280
+ }, { capture: true });
281
+
282
+ // Floating widget for Codex RTL
283
+ const widgetWrapper = document.createElement('div');
284
+ widgetWrapper.className = 'rtl-widget-container';
285
+ widgetWrapper.innerHTML = `
286
+ <div class="rtl-widget-trigger">
287
+ <svg height="20" width="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M2 12h20"></path><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
288
+ </div>
289
+
290
+ <div class="rtl-widget-panel">
291
+ <div class="relative flex max-h-full min-h-0 flex-col rounded-3xl bg-token-dropdown-background pt-3 border border-token-border-default shadow-md">
292
+ <div class="flex flex-col gap-2 px-3 pb-3 pt-0 w-full h-full" style="display: flex; flex-direction: column; gap: 8px;">
293
+
294
+ <div class="border-b border-token-border-default pb-2 mb-1 text-center" style="text-align: center !important;">
295
+ <div class="electron:heading-lg heading-base truncate text-center" style="color: var(--color-token-foreground); display: flex !important; justify-content: center !important; text-align: center; width: 100%;">Codex Smart RTL</div>
296
+ </div>
297
+
298
+ <div class="flex items-center justify-between gap-4 px-1" style="display: flex; justify-content: space-between; align-items: center;">
299
+ <span id="rtl-toggle-label" class="font-medium text-xs" style="font-size: 12px; color: var(--color-token-foreground);">${isRTL ? 'Enabled' : 'Disabled'}</span>
300
+ <button id="rtl-toggle-btn" type="button" class="relative inline-flex items-center rounded-full transition-colors duration-200 h-6 w-11" style="background-color: ${isRTL ? 'var(--color-token-charts-blue, #339cff)' : '#555'}; border: none; cursor: pointer; height: 24px; width: 44px; border-radius: 9999px; position: relative;">
301
+ <span id="rtl-toggle-knob" class="inline-block transform rounded-full bg-white transition-transform h-4 w-4" style="margin-left: 4px; transform: ${isRTL ? 'translateX(20px)' : 'translateX(0)'}; transition: transform 0.2s; height: 16px; width: 16px; border-radius: 9999px; background: #fff; display: block;"></span>
302
+ </button>
303
+ </div>
304
+
305
+ <div id="rtl-settings-wrapper" class="flex flex-col gap-2 transition-all duration-300" style="position: relative; z-index: 10; opacity: ${isRTL ? '1' : '0.4'}; pointer-events: ${isRTL ? 'auto' : 'none'}; display: flex; flex-direction: column; gap: 8px; transition: opacity 0.3s;">
306
+ <div class="flex items-center justify-between gap-2 px-1 mt-1" style="display: flex; justify-content: space-between; align-items: center;">
307
+ <div style="display: flex; align-items: center; gap: 4px;">
308
+ <span class="font-medium text-xs" style="font-size: 12px; color: var(--color-token-foreground);">Force RTL</span>
309
+ <div class="rtl-info-icon" style="color: var(--color-token-foreground); cursor: pointer; margin-left: 2px;">
310
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 -960 960 960" fill="currentColor"><path d="M450-290h60V-520H450v230Zm52.92-307.75q9.38-9.29 9.38-23.02t-9.29-23.02T480-653.07t-23.02,9.29t-9.29,23.02t9.38,23.02T480-588.46t22.92-9.29ZM480.07-100q-78.84,0-148.2-29.92T211.18-211.13T129.93-331.76T100-479.93t29.92-148.2t81.21-120.68t120.63-81.25T479.93-860t148.2,29.92t120.68,81.21t81.25,120.63T860-480.07t-29.92,148.2T748.87-211.18T628.24-129.93T480.07-100ZM480-160q134,0 227-93t93-227T707-707T480-800T253-707T160-480t93,227t227,93Zm0-320Z"></path></svg>
311
+ <div class="rtl-tooltip">Forces RTL layout on all elements, even if the text starts with English characters.</div>
312
+ </div>
313
+ </div>
314
+ <button id="rtl-force-btn" type="button" class="relative inline-flex items-center rounded-full transition-colors duration-200 h-6 w-11" style="background-color: ${forceRTL ? 'var(--color-token-charts-blue, #339cff)' : '#555'}; border: none; cursor: pointer; height: 24px; width: 44px; border-radius: 9999px; position: relative;">
315
+ <span id="rtl-force-knob" class="inline-block transform rounded-full bg-white transition-transform h-4 w-4" style="margin-left: 4px; transform: ${forceRTL ? 'translateX(20px)' : 'translateX(0)'}; transition: transform 0.2s; height: 16px; width: 16px; border-radius: 9999px; background: #fff; display: block;"></span>
316
+ </button>
317
+ </div>
318
+
319
+ <div class="h-px bg-token-border-default w-full"></div>
320
+
321
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 8px;">
322
+ <span class="font-medium text-xs" style="font-size: 12px; color: var(--color-token-foreground);">FA/AR Font</span>
323
+ <input id="rtl-fafont-input" type="text" placeholder="Default: Vazirmatn" value="${savedFaFont}" class="focus-visible:ring-token-focus h-7 w-full max-w-[8.5rem] rounded-lg border border-token-border bg-token-input-background px-2 text-xs text-token-text-primary shadow-sm outline-none focus-visible:ring-2 max-sm:max-w-none" spellcheck="false">
324
+ </div>
325
+
326
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 8px;">
327
+ <span class="font-medium text-xs" style="font-size: 12px; color: var(--color-token-foreground);">EN Font</span>
328
+ <input id="rtl-enfont-input" type="text" placeholder="Default: System" value="${savedEnFont}" class="focus-visible:ring-token-focus h-7 w-full max-w-[8.5rem] rounded-lg border border-token-border bg-token-input-background px-2 text-xs text-token-text-primary shadow-sm outline-none focus-visible:ring-2 max-sm:max-w-none" spellcheck="false">
329
+ </div>
330
+
331
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 8px;">
332
+ <span class="font-medium text-xs" style="font-size: 12px; color: var(--color-token-foreground);">Code Font</span>
333
+ <input id="rtl-codefont-input" type="text" placeholder="Default: System" value="${savedCodeFont}" class="focus-visible:ring-token-focus h-7 w-full max-w-[8.5rem] rounded-lg border border-token-border bg-token-input-background px-2 text-xs text-token-text-primary shadow-sm outline-none focus-visible:ring-2 max-sm:max-w-none" spellcheck="false">
334
+ </div>
335
+
336
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 8px; height: 28px;">
337
+ <span class="font-medium text-xs" style="font-size: 12px; color: var(--color-token-foreground);">Line Height</span>
338
+ <div style="display: flex; align-items: center; gap: 8px;">
339
+ <input id="rtl-lh-input" type="range" min="1.2" max="2.5" step="0.1" value="${savedLH}" style="width: 80px; cursor: pointer; accent-color: var(--color-token-charts-blue, #339cff);">
340
+ <button id="rtl-lh-reset" type="button" class="text-token-text-primary opacity-50 hover:opacity-100 transition-opacity cursor-pointer text-sm" style="background: none; border: none; padding: 0;">↺</button>
341
+ </div>
342
+ </div>
343
+
344
+ <div class="h-px bg-token-border-default w-full"></div>
345
+
346
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 8px;">
347
+ <div style="display: flex; align-items: center; gap: 4px;">
348
+ <span class="font-medium text-xs" style="font-size: 12px; color: var(--color-token-foreground);">Type @ with Shift+2</span>
349
+ <div class="rtl-info-icon" style="color: var(--color-token-foreground); cursor: pointer; margin-left: 2px;">
350
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 -960 960 960" fill="currentColor"><path d="M450-290h60V-520H450v230Zm52.92-307.75q9.38-9.29 9.38-23.02t-9.29-23.02T480-653.07t-23.02,9.29t-9.29,23.02t9.38,23.02T480-588.46t22.92-9.29ZM480.07-100q-78.84,0-148.2-29.92T211.18-211.13T129.93-331.76T100-479.93t29.92-148.2t81.21-120.68t120.63-81.25T479.93-860t148.2,29.92t120.68,81.21t81.25,120.63T860-480.07t-29.92,148.2T748.87-211.18T628.24-129.93T480.07-100ZM480-160q134,0 227-93t93-227T707-707T480-800T253-707T160-480t93,227t227,93Zm0-320Z"></path></svg>
351
+ <div class="rtl-tooltip">Automatically converts '٬' to '@' when you press Shift+2 on a Persian keyboard layout.</div>
352
+ </div>
353
+ </div>
354
+ <button id="rtl-at-btn" type="button" class="relative inline-flex items-center rounded-full transition-colors duration-200 h-6 w-11" style="background-color: ${fixAtSign ? 'var(--color-token-charts-blue, #339cff)' : '#555'}; border: none; cursor: pointer; height: 24px; width: 44px; border-radius: 9999px; position: relative;">
355
+ <span id="rtl-at-knob" class="inline-block transform rounded-full bg-white transition-transform h-4 w-4" style="margin-left: 4px; transform: ${fixAtSign ? 'translateX(20px)' : 'translateX(0)'}; transition: transform 0.2s; height: 16px; width: 16px; border-radius: 9999px; background: #fff; display: block;"></span>
356
+ </button>
357
+ </div>
358
+
359
+ <div class="h-px bg-token-border-default w-full"></div>
360
+
361
+ <!-- GitHub -->
362
+ <a href="https://github.com/mmnaderi/codex-rtl" target="_blank" class="rtl-github-link">
363
+ <svg height="14" width="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path></svg>
364
+ Star on GitHub
365
+ </a>
366
+ </div>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ `;
371
+ document.body.appendChild(widgetWrapper);
372
+
373
+ const toggleBtn = document.getElementById('rtl-toggle-btn');
374
+ const toggleKnob = document.getElementById('rtl-toggle-knob');
375
+ const toggleLabel = document.getElementById('rtl-toggle-label');
376
+ const settingsWrapper = document.getElementById('rtl-settings-wrapper');
377
+ const forceBtn = document.getElementById('rtl-force-btn');
378
+ const forceKnob = document.getElementById('rtl-force-knob');
379
+ const atBtn = document.getElementById('rtl-at-btn');
380
+ const atKnob = document.getElementById('rtl-at-knob');
381
+ const faFontInput = document.getElementById('rtl-fafont-input');
382
+ const enFontInput = document.getElementById('rtl-enfont-input');
383
+ const codeFontInput = document.getElementById('rtl-codefont-input');
384
+ const lhInput = document.getElementById('rtl-lh-input');
385
+ const lhResetBtn = document.getElementById('rtl-lh-reset');
386
+
387
+ const saveConfig = () => {
388
+ console.log("SAVE_RTL_CONFIG|" + JSON.stringify({
389
+ isRTL: isRTL,
390
+ forceRTL: forceRTL,
391
+ fixAtSign: fixAtSign,
392
+ faFont: faFontInput.value.trim(),
393
+ enFont: enFontInput.value.trim(),
394
+ codeFont: codeFontInput.value.trim(),
395
+ lh: lhInput.value
396
+ }));
397
+ };
398
+
399
+ toggleBtn.addEventListener('click', () => {
400
+ isRTL = !isRTL;
401
+ saveConfig();
402
+ toggleLabel.innerText = isRTL ? 'Enabled' : 'Disabled';
403
+ settingsWrapper.style.opacity = isRTL ? '1' : '0.4';
404
+ settingsWrapper.style.pointerEvents = isRTL ? 'auto' : 'none';
405
+ toggleKnob.style.transform = isRTL ? 'translateX(20px)' : 'translateX(0)';
406
+ toggleBtn.style.backgroundColor = isRTL ? 'var(--color-token-charts-blue, #339cff)' : '#555';
407
+ updateDynamicCSS();
408
+ });
409
+
410
+ forceBtn.addEventListener('click', () => {
411
+ forceRTL = !forceRTL;
412
+ saveConfig();
413
+ forceKnob.style.transform = forceRTL ? 'translateX(20px)' : 'translateX(0)';
414
+ forceBtn.style.backgroundColor = forceRTL ? 'var(--color-token-charts-blue, #339cff)' : '#555';
415
+ updateDynamicCSS();
416
+ });
417
+
418
+ atBtn.addEventListener('click', () => {
419
+ fixAtSign = !fixAtSign;
420
+ saveConfig();
421
+ atKnob.style.transform = fixAtSign ? 'translateX(20px)' : 'translateX(0)';
422
+ atBtn.style.backgroundColor = fixAtSign ? 'var(--color-token-charts-blue, #339cff)' : '#555';
423
+ });
424
+
425
+ faFontInput.addEventListener('input', () => {
426
+ savedFaFont = faFontInput.value.trim();
427
+ saveConfig();
428
+ updateDynamicCSS();
429
+ });
430
+
431
+ enFontInput.addEventListener('input', () => {
432
+ savedEnFont = enFontInput.value.trim();
433
+ saveConfig();
434
+ updateDynamicCSS();
435
+ });
436
+
437
+ codeFontInput.addEventListener('input', () => {
438
+ savedCodeFont = codeFontInput.value.trim();
439
+ saveConfig();
440
+ updateDynamicCSS();
441
+ });
442
+
443
+ lhInput.addEventListener('input', () => {
444
+ savedLH = lhInput.value;
445
+ saveConfig();
446
+ updateDynamicCSS();
447
+ });
448
+
449
+ lhResetBtn.addEventListener('click', () => {
450
+ lhInput.value = '1.6';
451
+ savedLH = '1.6';
452
+ saveConfig();
453
+ updateDynamicCSS();
454
+ });
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "codex-rtl",
3
- "version": "0.0.2",
3
+ "version": "1.0.1",
4
4
  "description": "A smart RTL (Right-to-Left) patcher with UI settings for the Codex application.",
5
- "main": "index.js",
5
+ "main": "./bin/index.js",
6
6
  "bin": {
7
7
  "codex-rtl": "./bin/index.js"
8
8
  },
@@ -32,5 +32,12 @@
32
32
  "bugs": {
33
33
  "url": "https://github.com/mmnaderi/codex-rtl/issues"
34
34
  },
35
- "homepage": "https://github.com/mmnaderi/codex-rtl#readme"
35
+ "homepage": "https://github.com/mmnaderi/codex-rtl#readme",
36
+ "dependencies": {
37
+ "@electron/asar": "^4.2.0",
38
+ "ora": "^9.4.1",
39
+ "picocolors": "^1.1.1",
40
+ "prompts": "^2.4.2"
41
+ },
42
+ "type": "module"
36
43
  }