pyraya 0.1.0a9__tar.gz

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.
pyraya-0.1.0a9/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright 2026 RoXimn
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the “Software”),
5
+ to deal in the Software without restriction, including without limitation
6
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
7
+ and/or sell copies of the Software, and to permit persons to whom the
8
+ Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included
11
+ in all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
14
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
16
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
18
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
19
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,301 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyraya
3
+ Version: 0.1.0a9
4
+ Summary: PyRaya is a reusable, self-contained Qt text editor widget written in Python (PySide6)
5
+ Author: RoXimn
6
+ Author-email: RoXimn <roximn148@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: pydantic==2.13.5
10
+ Requires-Dist: pyside6>=6.11.2
11
+ Requires-Dist: romanalfaz>=0.1.0a10
12
+ Requires-Python: >=3.14
13
+ Description-Content-Type: text/markdown
14
+
15
+ # PyRaya (پیرایہ)
16
+
17
+ قلم کی روانی، سوچ کی آسانی؛ اردو تحریر کا نیا پیرایہ۔
18
+
19
+ A PySide6 text editor for Urdu. Drop the widget into your app and it handles
20
+ right-to-left paragraphs, a Nastaliq font stack, and typing Urdu from a roman
21
+ keyboard.
22
+
23
+ ⚠️ **Alpha** · `0.1.0a9` · Python 3.14+ · PySide6
24
+
25
+ ---
26
+
27
+ ## Why PyRaya exists
28
+
29
+ Urdu writers type on roman keyboards and expect Urdu script on the page. Qt
30
+ shapes Nastaliq well, then hands you the rest: paragraph direction, a font
31
+ stack that carries Nastaliq beside English, and a way to turn `salam` into
32
+ سلام. PyRaya does that work. Type a roman word, choose the Urdu spelling from
33
+ a list, and the page stays in Urdu the whole time.
34
+
35
+ ![Typing a Roman-Urdu sentence; each word converts to Arabic script from a live suggestion list](./docs/demo-roman-input.gif)
36
+
37
+ *Interacitve realtime conversion of each typed Urdu word from Roman-script input to Arabic-script.*
38
+
39
+ ## Install
40
+
41
+ No package is published yet. In a clone of this repository, `uv sync` builds a
42
+ virtual environment with the pinned dependencies, and `uv run pyraya` opens
43
+ the demo window.
44
+
45
+ ## Sixty seconds
46
+
47
+ ```python
48
+ import sys
49
+
50
+ from PySide6.QtWidgets import QApplication
51
+
52
+ from pyraya import PyRaya
53
+
54
+ app = QApplication(sys.argv)
55
+ editor = PyRaya()
56
+ editor.show()
57
+
58
+ sys.exit(app.exec())
59
+ ```
60
+
61
+ Run it and a right-aligned editor opens, cursor at the top right. Type
62
+ `salam` and a small line with a suggestion list appears; press space and the
63
+ selected Urdu word lands in the document, in Urdu script.
64
+
65
+ ## Features at a glance
66
+
67
+ - Right-to-left paragraphs, right-aligned from the first keystroke.
68
+ - Nastaliq and Latin on one line, from one font stack.
69
+ - Spaces and tabs drawn as visible marks.
70
+ - Spans tagged Native or Roman Urdu; Roman-Urdu text gets a faint
71
+ tint, Native text carries no tint. See [Regions](#regions).
72
+ - Typed Latin marks and digits land in their Urdu forms inside a
73
+ Roman-Urdu span; digits commit a roman-input word like punctuation.
74
+ See [Character substitution](#character-substitution).
75
+ - Roman input: type `salam`, pick سلام, keep writing. See
76
+ [Roman input](#roman-input).
77
+ - A wavy underline under words the dictionary does not know. See
78
+ [Spell check](#spell-check).
79
+ - Unknown words can be learned into the shared dictionary, from the
80
+ right-click menu or the `+ [..]` suggestion row. See
81
+ [Learn a word](#learn-a-word).
82
+ - Word offsets for analysis: `wordRuns(text)` and `editor.wordRuns()`.
83
+ See [Word runs](#word-runs).
84
+
85
+ ## Regions
86
+
87
+ A region kind marks a character span with intent, and there are two kinds:
88
+ `RegionType.Native` for English and other Latin text, `RegionType.RomanUrdu`
89
+ for Urdu written in roman letters. Kinds are stored as a tag on the document's
90
+ characters — a stored tag wins, and untagged text *acts as* Roman Urdu (it
91
+ gets the Roman-Urdu tint and takes roman input). Native text carries no tint;
92
+ nothing else changes for it.
93
+
94
+ Tag a span three ways. Select text and use the right-click menu ("Region:
95
+ Native", "Region: Roman Urdu"), or with a selection press `Ctrl+1` for Native
96
+ or `Ctrl+2` for Roman Urdu, or call the API:
97
+
98
+ ```python
99
+ from pyraya import RegionType
100
+
101
+ editor.setRegionType(RegionType.Native, 0, 12) # tag characters 0 through 11
102
+ editor.regions() # every tagged span as TextRegion(start, end, kind)
103
+ editor.clearRegions() # every character resolves to the seed kind again
104
+ ```
105
+
106
+ The tags live in the document itself, so they survive edits and undo. Each
107
+ method that changes regions emits `regionsChanged` once when it finishes.
108
+ `seedRegionKind()` answers the kind untagged text resolves to; the seed is
109
+ read-only. `setRegionColor(kind, color)` recolors one kind's tint, inside
110
+ this widget only — it touches no document state and repaints the tint
111
+ immediately.
112
+
113
+ ## Character substitution
114
+
115
+ A fixed per-region table rewrites typed Latin characters to their Urdu
116
+ equivalents the moment they enter the document, inside a Roman Urdu span:
117
+ the full stop, comma, question mark, and semicolon become their
118
+ Arabic-script forms, and the digits 0–9 become the Eastern Arabic-Indic
119
+ digits. The colon is deliberately untouched — Urdu has no dedicated colon
120
+ codepoint and conventionally keeps the Latin one. Native spans pass every
121
+ character through, and the table is fixed (no host API): it is part of the
122
+ region model, not a per-widget setting.
123
+
124
+ The substitution is region-scoped, not feature-scoped: it holds whether or
125
+ not the roman-input overlay is on, and whether or not the overlay is
126
+ active. A typed mark or digit with no overlay active lands directly in the
127
+ document in its Urdu form, tagged with the region kind. With the overlay
128
+ active, a digit is a committing key like punctuation: it terminates the
129
+ word and commits, landing in its Urdu form as the separator. The four
130
+ mapped marks and the ten digits are the whole table; Latin letters are not
131
+ in it, so they still seed the overlay as before.
132
+
133
+ ```python
134
+ from pyraya import RegionType, substituteChar
135
+
136
+ substituteChar(".", RegionType.RomanUrdu) # "۔"
137
+ substituteChar("5", RegionType.RomanUrdu) # "۵"
138
+ substituteChar("5", RegionType.Native) # None — passes through
139
+ ```
140
+
141
+ ## Roman input
142
+
143
+ The overlay is on by default. Type a roman letter inside a Roman Urdu region
144
+ and a small line appears over the nearest Urdu word: your roman letters are
145
+ edited in that line while the document keeps its Urdu script. A suggestion
146
+ list sits under the line, or above it when the bottom is crowded. Green rows
147
+ match the word exactly, orange rows are one edit away, red rows two, and five
148
+ rows fit at a time with the list scrolling so the picked row stays visible.
149
+ The lookup is capped to the dictionary's longest word, so a long typed word
150
+ stays fast; the cap affects only the lookup, so the full word still commits.
151
+
152
+ Space or any punctuation key commits the selected word, and a typed
153
+ Urdu mark such as ۔ or ، goes with it. A digit commits too, landing in
154
+ its Eastern Arabic-Indic form as the separator (see
155
+ [Character substitution](#character-substitution)). Enter does not start
156
+ a new line; `Ctrl+Enter` does. `Ctrl+Up` and `Ctrl+Down` commit the
157
+ selected word without any separator and move the cursor one line up or
158
+ down.
159
+ Esc closes the overlay, and `Ctrl+Esc` closes it and turns assist mode
160
+ off, so typing and clicking stop opening it until `Ctrl+Space` brings it
161
+ back, over the word under the cursor or empty at the cursor. Two quick
162
+ Esc presses turn assist mode off as well, whether or not the overlay is
163
+ shown. Turning the feature off restores
164
+ plain typing:
165
+
166
+ ```python
167
+ editor.setRomanInputEnabled(False) # roman letters type into the document
168
+ editor.setAssistEnabled(False) # the overlay stops opening on its own
169
+ ```
170
+
171
+ ## Spell check
172
+
173
+ The underline is on by default. Any Arabic-script word inside a Roman Urdu
174
+ region that the romanalfaz dictionary does not know gets a wavy brick-red
175
+ underline, so unknown words are visible without interrupting the flow. It is
176
+ display only: nothing corrects, nothing suggests, nothing enters the undo
177
+ stack, and Native-region text is never underlined. A *word* is a maximal run
178
+ of Arabic-script letters and combining marks: digits (`دوست۵` ends at
179
+ `دوست`), punctuation, and symbols are boundaries, and the invisible joiners
180
+ a copied word may carry (ZWNJ, ZWJ, LRM, RLM, ALM) neither split a word nor
181
+ form one. The ranges cover the Arabic block, its supplement, Extended-A, and
182
+ the presentation forms, so a word pasted from a PDF is still one word. The
183
+ switch is independent of roman input, so you can run the underline alone:
184
+
185
+ ```python
186
+ editor.setSpellCheckEnabled(False) # hide the underlines
187
+ ```
188
+
189
+ ## Word runs
190
+
191
+ The words in a text can be enumerated without touching the document:
192
+
193
+ ```python
194
+ from pyraya import wordRuns
195
+
196
+ text = "کام دوست، ٹھیک"
197
+ for start, end in wordRuns(text):
198
+ print(start, end, text[start:end])
199
+
200
+ for start, end in editor.wordRuns(): # absolute document offsets
201
+ ...
202
+ ```
203
+
204
+ Each run is a pair of offsets, not a substring — the caller slices. A run is
205
+ a maximal stretch of Arabic-script letters and combining marks: digits,
206
+ punctuation, and symbols end it, and the invisible joiners a copied word may
207
+ carry (ZWNJ, ZWJ, LRM, RLM, ALM) neither split a word nor form one.
208
+ `wordRuns(text)` takes a plain string, so a long text is one linear pass with
209
+ no document or `QApplication`; `editor.wordRuns()` adds each block's position
210
+ to give document offsets. Both only read: no edit, no undo step.
211
+
212
+ ## Learn a word
213
+
214
+ Words the dictionary does not know can be added to it. Right-click an
215
+ unknown word in a Roman Urdu region and choose "Add to dictionary", or
216
+ commit the `+ [..]` row in the suggestion list. Either way the dialog's
217
+ field opens holding the word under the cursor — the word you clicked, or
218
+ the one you are romanizing — so OK is live at once. A live roman preview
219
+ sits under it, and OK is enabled only while the field holds a mappable
220
+ word. Accepting adds the word to the dictionary
221
+ — its underline clears, and it becomes suggestable from roman input;
222
+ cancelling adds nothing, and in the `+ [..]` row the typed roman stays
223
+ as-is.
224
+
225
+ The dictionary is shared by every widget in the process, so a word
226
+ learned in one editor is known to all of them. Saving it to disk is the
227
+ host application's job: it polls `localDictionary().isModified()` and
228
+ calls `Vocabulary.save` on the shared Vocabulary — PyRaya never writes
229
+ the file. A host can replace the dialog with its own through
230
+ `setAddWordHandler`, and each successful learn emits `wordAdded`:
231
+
232
+ ```python
233
+ if editor.localDictionary().isModified():
234
+ editor.localDictionary().save("dictionary.sym")
235
+ ```
236
+
237
+ ## Keyboard shortcuts
238
+
239
+ **In the document:**
240
+
241
+ | Key | Action |
242
+ | --- | --- |
243
+ | `Ctrl+1` | Tag the current selection as a Native region |
244
+ | `Ctrl+2` | Tag the current selection as a Roman Urdu region |
245
+ | `Ctrl+Space` | Turn assist mode on and open the overlay over the word under the cursor — or, with no such word, an empty overlay at the cursor |
246
+ | `Ctrl+Esc` | Turn assist mode off and close a shown overlay |
247
+ | two quick `Esc` | Turn assist mode off — a single `Esc` in the document does nothing, but two within a short interval turn it off, whether or not the overlay is shown |
248
+ | `Left` / `Right` | Move the cursor in visual (screen) order — the document is right-to-left, so the keys are remapped |
249
+
250
+ **In the roman-input overlay** (open over a word; see [Roman input](#roman-input)):
251
+
252
+ | Key | Action |
253
+ | --- | --- |
254
+ | `Up` / `Down` | Move the highlighted suggestion, with wraparound |
255
+ | `Space` | Commit the selected word and a space |
256
+ | `Enter` | Commit the selected word, no newline |
257
+ | `Ctrl+Enter` | Commit the selected word and a newline |
258
+ | Typed punctuation | Commit the word and the mark — ASCII punctuation and the Urdu separator marks (؟ ، ؛ ۔ ٫ ٬ ؜) |
259
+ | Typed digit | Commit the word and the digit, in its Eastern Arabic-Indic form (0–9 → ۰–۹) |
260
+ | `Left` at the left edge / `Right` at the right edge | Move to the adjacent word without writing |
261
+ | `Ctrl+Up` / `Ctrl+Down` | Commit the selected word without a separator, and move the cursor one line up or down |
262
+ | `Esc` | Close the overlay; assist mode stays on |
263
+ | `Ctrl+Esc` | Close the overlay and turn assist mode off |
264
+
265
+ The standard Qt editing shortcuts (undo, redo, Home/End, ...) work as
266
+ usual, and `Ctrl+Space`, `Ctrl+Esc`, and two quick `Esc` presses work
267
+ whether or not the overlay is open.
268
+
269
+ ## Repository layout
270
+
271
+ ```text
272
+ pyraya/
273
+ ├── pyproject.toml # project metadata and tool configs
274
+ ├── uv.lock # pinned dependencies
275
+ ├── LICENSE # MIT
276
+ ├── AGENTS.md # behavior contracts and test conventions
277
+ ├── src/
278
+ │ └── pyraya/
279
+ │ ├── pyraya_widget.py # the PyRaya widget and its highlighter
280
+ │ ├── regions.py # region model: kinds, spans, seed, stored-tag property
281
+ │ ├── roman_input.py # roman-input engine and overlay widgets
282
+ │ ├── __init__.py # public API and the demo entry point
283
+ │ └── py.typed # typing marker
284
+ ├── tests/
285
+ │ └── fixtures/ # reference documents for the large-document tests
286
+ └── docs/
287
+ └── superpowers/ # design specs and implementation plans
288
+ ```
289
+
290
+ ## Where the details live
291
+
292
+ This page covers what PyRaya does and how to switch things on. The behavior
293
+ contracts, edge cases, and test coverage live in `AGENTS.md`, and the design
294
+ history under `docs/superpowers/`. Every public class, method, and constant
295
+ documents itself in its docstring, and `pyraya/__init__.py` lists the whole
296
+ public surface.
297
+
298
+ ## License
299
+
300
+ PyRaya is MIT licensed; see `LICENSE`. It is written by RoXimn and runs on
301
+ PySide6 with romanalfaz behind the roman-input features.
@@ -0,0 +1,287 @@
1
+ # PyRaya (پیرایہ)
2
+
3
+ قلم کی روانی، سوچ کی آسانی؛ اردو تحریر کا نیا پیرایہ۔
4
+
5
+ A PySide6 text editor for Urdu. Drop the widget into your app and it handles
6
+ right-to-left paragraphs, a Nastaliq font stack, and typing Urdu from a roman
7
+ keyboard.
8
+
9
+ ⚠️ **Alpha** · `0.1.0a9` · Python 3.14+ · PySide6
10
+
11
+ ---
12
+
13
+ ## Why PyRaya exists
14
+
15
+ Urdu writers type on roman keyboards and expect Urdu script on the page. Qt
16
+ shapes Nastaliq well, then hands you the rest: paragraph direction, a font
17
+ stack that carries Nastaliq beside English, and a way to turn `salam` into
18
+ سلام. PyRaya does that work. Type a roman word, choose the Urdu spelling from
19
+ a list, and the page stays in Urdu the whole time.
20
+
21
+ ![Typing a Roman-Urdu sentence; each word converts to Arabic script from a live suggestion list](./docs/demo-roman-input.gif)
22
+
23
+ *Interacitve realtime conversion of each typed Urdu word from Roman-script input to Arabic-script.*
24
+
25
+ ## Install
26
+
27
+ No package is published yet. In a clone of this repository, `uv sync` builds a
28
+ virtual environment with the pinned dependencies, and `uv run pyraya` opens
29
+ the demo window.
30
+
31
+ ## Sixty seconds
32
+
33
+ ```python
34
+ import sys
35
+
36
+ from PySide6.QtWidgets import QApplication
37
+
38
+ from pyraya import PyRaya
39
+
40
+ app = QApplication(sys.argv)
41
+ editor = PyRaya()
42
+ editor.show()
43
+
44
+ sys.exit(app.exec())
45
+ ```
46
+
47
+ Run it and a right-aligned editor opens, cursor at the top right. Type
48
+ `salam` and a small line with a suggestion list appears; press space and the
49
+ selected Urdu word lands in the document, in Urdu script.
50
+
51
+ ## Features at a glance
52
+
53
+ - Right-to-left paragraphs, right-aligned from the first keystroke.
54
+ - Nastaliq and Latin on one line, from one font stack.
55
+ - Spaces and tabs drawn as visible marks.
56
+ - Spans tagged Native or Roman Urdu; Roman-Urdu text gets a faint
57
+ tint, Native text carries no tint. See [Regions](#regions).
58
+ - Typed Latin marks and digits land in their Urdu forms inside a
59
+ Roman-Urdu span; digits commit a roman-input word like punctuation.
60
+ See [Character substitution](#character-substitution).
61
+ - Roman input: type `salam`, pick سلام, keep writing. See
62
+ [Roman input](#roman-input).
63
+ - A wavy underline under words the dictionary does not know. See
64
+ [Spell check](#spell-check).
65
+ - Unknown words can be learned into the shared dictionary, from the
66
+ right-click menu or the `+ [..]` suggestion row. See
67
+ [Learn a word](#learn-a-word).
68
+ - Word offsets for analysis: `wordRuns(text)` and `editor.wordRuns()`.
69
+ See [Word runs](#word-runs).
70
+
71
+ ## Regions
72
+
73
+ A region kind marks a character span with intent, and there are two kinds:
74
+ `RegionType.Native` for English and other Latin text, `RegionType.RomanUrdu`
75
+ for Urdu written in roman letters. Kinds are stored as a tag on the document's
76
+ characters — a stored tag wins, and untagged text *acts as* Roman Urdu (it
77
+ gets the Roman-Urdu tint and takes roman input). Native text carries no tint;
78
+ nothing else changes for it.
79
+
80
+ Tag a span three ways. Select text and use the right-click menu ("Region:
81
+ Native", "Region: Roman Urdu"), or with a selection press `Ctrl+1` for Native
82
+ or `Ctrl+2` for Roman Urdu, or call the API:
83
+
84
+ ```python
85
+ from pyraya import RegionType
86
+
87
+ editor.setRegionType(RegionType.Native, 0, 12) # tag characters 0 through 11
88
+ editor.regions() # every tagged span as TextRegion(start, end, kind)
89
+ editor.clearRegions() # every character resolves to the seed kind again
90
+ ```
91
+
92
+ The tags live in the document itself, so they survive edits and undo. Each
93
+ method that changes regions emits `regionsChanged` once when it finishes.
94
+ `seedRegionKind()` answers the kind untagged text resolves to; the seed is
95
+ read-only. `setRegionColor(kind, color)` recolors one kind's tint, inside
96
+ this widget only — it touches no document state and repaints the tint
97
+ immediately.
98
+
99
+ ## Character substitution
100
+
101
+ A fixed per-region table rewrites typed Latin characters to their Urdu
102
+ equivalents the moment they enter the document, inside a Roman Urdu span:
103
+ the full stop, comma, question mark, and semicolon become their
104
+ Arabic-script forms, and the digits 0–9 become the Eastern Arabic-Indic
105
+ digits. The colon is deliberately untouched — Urdu has no dedicated colon
106
+ codepoint and conventionally keeps the Latin one. Native spans pass every
107
+ character through, and the table is fixed (no host API): it is part of the
108
+ region model, not a per-widget setting.
109
+
110
+ The substitution is region-scoped, not feature-scoped: it holds whether or
111
+ not the roman-input overlay is on, and whether or not the overlay is
112
+ active. A typed mark or digit with no overlay active lands directly in the
113
+ document in its Urdu form, tagged with the region kind. With the overlay
114
+ active, a digit is a committing key like punctuation: it terminates the
115
+ word and commits, landing in its Urdu form as the separator. The four
116
+ mapped marks and the ten digits are the whole table; Latin letters are not
117
+ in it, so they still seed the overlay as before.
118
+
119
+ ```python
120
+ from pyraya import RegionType, substituteChar
121
+
122
+ substituteChar(".", RegionType.RomanUrdu) # "۔"
123
+ substituteChar("5", RegionType.RomanUrdu) # "۵"
124
+ substituteChar("5", RegionType.Native) # None — passes through
125
+ ```
126
+
127
+ ## Roman input
128
+
129
+ The overlay is on by default. Type a roman letter inside a Roman Urdu region
130
+ and a small line appears over the nearest Urdu word: your roman letters are
131
+ edited in that line while the document keeps its Urdu script. A suggestion
132
+ list sits under the line, or above it when the bottom is crowded. Green rows
133
+ match the word exactly, orange rows are one edit away, red rows two, and five
134
+ rows fit at a time with the list scrolling so the picked row stays visible.
135
+ The lookup is capped to the dictionary's longest word, so a long typed word
136
+ stays fast; the cap affects only the lookup, so the full word still commits.
137
+
138
+ Space or any punctuation key commits the selected word, and a typed
139
+ Urdu mark such as ۔ or ، goes with it. A digit commits too, landing in
140
+ its Eastern Arabic-Indic form as the separator (see
141
+ [Character substitution](#character-substitution)). Enter does not start
142
+ a new line; `Ctrl+Enter` does. `Ctrl+Up` and `Ctrl+Down` commit the
143
+ selected word without any separator and move the cursor one line up or
144
+ down.
145
+ Esc closes the overlay, and `Ctrl+Esc` closes it and turns assist mode
146
+ off, so typing and clicking stop opening it until `Ctrl+Space` brings it
147
+ back, over the word under the cursor or empty at the cursor. Two quick
148
+ Esc presses turn assist mode off as well, whether or not the overlay is
149
+ shown. Turning the feature off restores
150
+ plain typing:
151
+
152
+ ```python
153
+ editor.setRomanInputEnabled(False) # roman letters type into the document
154
+ editor.setAssistEnabled(False) # the overlay stops opening on its own
155
+ ```
156
+
157
+ ## Spell check
158
+
159
+ The underline is on by default. Any Arabic-script word inside a Roman Urdu
160
+ region that the romanalfaz dictionary does not know gets a wavy brick-red
161
+ underline, so unknown words are visible without interrupting the flow. It is
162
+ display only: nothing corrects, nothing suggests, nothing enters the undo
163
+ stack, and Native-region text is never underlined. A *word* is a maximal run
164
+ of Arabic-script letters and combining marks: digits (`دوست۵` ends at
165
+ `دوست`), punctuation, and symbols are boundaries, and the invisible joiners
166
+ a copied word may carry (ZWNJ, ZWJ, LRM, RLM, ALM) neither split a word nor
167
+ form one. The ranges cover the Arabic block, its supplement, Extended-A, and
168
+ the presentation forms, so a word pasted from a PDF is still one word. The
169
+ switch is independent of roman input, so you can run the underline alone:
170
+
171
+ ```python
172
+ editor.setSpellCheckEnabled(False) # hide the underlines
173
+ ```
174
+
175
+ ## Word runs
176
+
177
+ The words in a text can be enumerated without touching the document:
178
+
179
+ ```python
180
+ from pyraya import wordRuns
181
+
182
+ text = "کام دوست، ٹھیک"
183
+ for start, end in wordRuns(text):
184
+ print(start, end, text[start:end])
185
+
186
+ for start, end in editor.wordRuns(): # absolute document offsets
187
+ ...
188
+ ```
189
+
190
+ Each run is a pair of offsets, not a substring — the caller slices. A run is
191
+ a maximal stretch of Arabic-script letters and combining marks: digits,
192
+ punctuation, and symbols end it, and the invisible joiners a copied word may
193
+ carry (ZWNJ, ZWJ, LRM, RLM, ALM) neither split a word nor form one.
194
+ `wordRuns(text)` takes a plain string, so a long text is one linear pass with
195
+ no document or `QApplication`; `editor.wordRuns()` adds each block's position
196
+ to give document offsets. Both only read: no edit, no undo step.
197
+
198
+ ## Learn a word
199
+
200
+ Words the dictionary does not know can be added to it. Right-click an
201
+ unknown word in a Roman Urdu region and choose "Add to dictionary", or
202
+ commit the `+ [..]` row in the suggestion list. Either way the dialog's
203
+ field opens holding the word under the cursor — the word you clicked, or
204
+ the one you are romanizing — so OK is live at once. A live roman preview
205
+ sits under it, and OK is enabled only while the field holds a mappable
206
+ word. Accepting adds the word to the dictionary
207
+ — its underline clears, and it becomes suggestable from roman input;
208
+ cancelling adds nothing, and in the `+ [..]` row the typed roman stays
209
+ as-is.
210
+
211
+ The dictionary is shared by every widget in the process, so a word
212
+ learned in one editor is known to all of them. Saving it to disk is the
213
+ host application's job: it polls `localDictionary().isModified()` and
214
+ calls `Vocabulary.save` on the shared Vocabulary — PyRaya never writes
215
+ the file. A host can replace the dialog with its own through
216
+ `setAddWordHandler`, and each successful learn emits `wordAdded`:
217
+
218
+ ```python
219
+ if editor.localDictionary().isModified():
220
+ editor.localDictionary().save("dictionary.sym")
221
+ ```
222
+
223
+ ## Keyboard shortcuts
224
+
225
+ **In the document:**
226
+
227
+ | Key | Action |
228
+ | --- | --- |
229
+ | `Ctrl+1` | Tag the current selection as a Native region |
230
+ | `Ctrl+2` | Tag the current selection as a Roman Urdu region |
231
+ | `Ctrl+Space` | Turn assist mode on and open the overlay over the word under the cursor — or, with no such word, an empty overlay at the cursor |
232
+ | `Ctrl+Esc` | Turn assist mode off and close a shown overlay |
233
+ | two quick `Esc` | Turn assist mode off — a single `Esc` in the document does nothing, but two within a short interval turn it off, whether or not the overlay is shown |
234
+ | `Left` / `Right` | Move the cursor in visual (screen) order — the document is right-to-left, so the keys are remapped |
235
+
236
+ **In the roman-input overlay** (open over a word; see [Roman input](#roman-input)):
237
+
238
+ | Key | Action |
239
+ | --- | --- |
240
+ | `Up` / `Down` | Move the highlighted suggestion, with wraparound |
241
+ | `Space` | Commit the selected word and a space |
242
+ | `Enter` | Commit the selected word, no newline |
243
+ | `Ctrl+Enter` | Commit the selected word and a newline |
244
+ | Typed punctuation | Commit the word and the mark — ASCII punctuation and the Urdu separator marks (؟ ، ؛ ۔ ٫ ٬ ؜) |
245
+ | Typed digit | Commit the word and the digit, in its Eastern Arabic-Indic form (0–9 → ۰–۹) |
246
+ | `Left` at the left edge / `Right` at the right edge | Move to the adjacent word without writing |
247
+ | `Ctrl+Up` / `Ctrl+Down` | Commit the selected word without a separator, and move the cursor one line up or down |
248
+ | `Esc` | Close the overlay; assist mode stays on |
249
+ | `Ctrl+Esc` | Close the overlay and turn assist mode off |
250
+
251
+ The standard Qt editing shortcuts (undo, redo, Home/End, ...) work as
252
+ usual, and `Ctrl+Space`, `Ctrl+Esc`, and two quick `Esc` presses work
253
+ whether or not the overlay is open.
254
+
255
+ ## Repository layout
256
+
257
+ ```text
258
+ pyraya/
259
+ ├── pyproject.toml # project metadata and tool configs
260
+ ├── uv.lock # pinned dependencies
261
+ ├── LICENSE # MIT
262
+ ├── AGENTS.md # behavior contracts and test conventions
263
+ ├── src/
264
+ │ └── pyraya/
265
+ │ ├── pyraya_widget.py # the PyRaya widget and its highlighter
266
+ │ ├── regions.py # region model: kinds, spans, seed, stored-tag property
267
+ │ ├── roman_input.py # roman-input engine and overlay widgets
268
+ │ ├── __init__.py # public API and the demo entry point
269
+ │ └── py.typed # typing marker
270
+ ├── tests/
271
+ │ └── fixtures/ # reference documents for the large-document tests
272
+ └── docs/
273
+ └── superpowers/ # design specs and implementation plans
274
+ ```
275
+
276
+ ## Where the details live
277
+
278
+ This page covers what PyRaya does and how to switch things on. The behavior
279
+ contracts, edge cases, and test coverage live in `AGENTS.md`, and the design
280
+ history under `docs/superpowers/`. Every public class, method, and constant
281
+ documents itself in its docstring, and `pyraya/__init__.py` lists the whole
282
+ public surface.
283
+
284
+ ## License
285
+
286
+ PyRaya is MIT licensed; see `LICENSE`. It is written by RoXimn and runs on
287
+ PySide6 with romanalfaz behind the roman-input features.
@@ -0,0 +1,60 @@
1
+ [project]
2
+ name = "pyraya"
3
+ version = "0.1.0a9"
4
+ license = "MIT"
5
+ license-files = ["LICENSE"]
6
+ description = "PyRaya is a reusable, self-contained Qt text editor widget written in Python (PySide6)"
7
+ readme = "README.md"
8
+ requires-python = ">=3.14"
9
+ dependencies = [
10
+ "pydantic==2.13.5",
11
+ "pyside6>=6.11.2",
12
+ "romanalfaz>=0.1.0a10",
13
+ ]
14
+
15
+ [[project.authors]]
16
+ name = "RoXimn"
17
+ email = "roximn148@gmail.com"
18
+
19
+ [project.scripts]
20
+ pyraya = "pyraya:main"
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.12.6,<0.13.0"]
24
+ build-backend = "uv_build"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "pyrefly>=1.2.0",
29
+ "pytest>=9.1.1",
30
+ "ruff>=0.16.4",
31
+ ]
32
+
33
+ [tool.ruff]
34
+ line-length = 100
35
+ target-version = "py314"
36
+ src = ["src"]
37
+
38
+ [tool.ruff.lint]
39
+ select = [
40
+ "E",
41
+ "F",
42
+ "W",
43
+ "I",
44
+ "UP",
45
+ "B",
46
+ ]
47
+
48
+ [tool.ruff.lint.isort]
49
+ known-first-party = ["pyraya"]
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
53
+ pythonpath = ["src"]
54
+
55
+ [tool.pyrefly]
56
+ project-includes = [
57
+ "src",
58
+ "tests",
59
+ ]
60
+ python-version = "3.14"