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 +19 -0
- pyraya-0.1.0a9/PKG-INFO +301 -0
- pyraya-0.1.0a9/README.md +287 -0
- pyraya-0.1.0a9/pyproject.toml +60 -0
- pyraya-0.1.0a9/pyproject.toml.orig +49 -0
- pyraya-0.1.0a9/src/pyraya/__init__.py +100 -0
- pyraya-0.1.0a9/src/pyraya/py.typed +0 -0
- pyraya-0.1.0a9/src/pyraya/pyraya_widget.py +3051 -0
- pyraya-0.1.0a9/src/pyraya/qamoos.sym +9237 -0
- pyraya-0.1.0a9/src/pyraya/regions.py +195 -0
- pyraya-0.1.0a9/src/pyraya/roman_input.py +1201 -0
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.
|
pyraya-0.1.0a9/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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.
|
pyraya-0.1.0a9/README.md
ADDED
|
@@ -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
|
+

|
|
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"
|