├── README.md ├── additional_documentation ├── Pyboard Editor.doc ├── Pyboard Editor.pdf ├── UserGuide.html └── h ├── lcd_io.md ├── package.json ├── peteensy.py ├── pye ├── pye.mpy ├── pye.py ├── pye_core.py ├── pye_gen.py ├── pye_lcd.py ├── pye_ux.py ├── pye_win ├── pye_win.py ├── pye_x3.mpy ├── pye_xbee.py ├── shebang ├── strip.sh └── tuning_pye.py /README.md: -------------------------------------------------------------------------------- 1 | # Micropython-Editor 2 | 3 | ## Installation 4 | ```sh 5 | mpremote mip install github:robert-hh/Micropython-Editor 6 | ``` 7 | 8 | ## Description 9 | 10 | A small text editor written in Python running on 11 | 12 | - MicroPython.org modules like the PyBoards, ESP32, ESP8266, Teensy 3.5, 3.6 and 4.x, RP2040, W60x 13 | - pycom.io modules like WipPy, Lopy, SiPy, FiPy, GPy 14 | - Adafruit Circuitpython modules, 15 | - SiPeed MaixPy modules, 16 | - Linux, OS X and Android's terminal windows 17 | 18 | It allows allows to edit files locally on the boards. The offspring was the editor widget of pfalcon at 19 | . I ported it to PyBoard and the other modules 20 | and added a lot of functions: 21 | 22 | - Use sys.stdin.read() and sys.stdout.write() for input and output of the MicroPython version. 23 | - Changed the read keyboard function to comply with byte-by-byte input on serial lines. 24 | - Added support for Tab, BackTab, Save, Del and Backspace joining lines, Find, 25 | Replace, Goto Line, Undo, Redo, Open file, Auto-Indent, Set Flags, Copy/Delete & Paste, 26 | Indent, Dedent, Block-Comment, Scrolling, line-shift 27 | - Handling tab (0x09) on reading & writing files, 28 | - Added a status line, and single line prompts for Quit, Save, Find, Replace, 29 | Goto, Open file and Flag settings. 30 | - Support the simultaneous editing of multiple files. 31 | - Basic mouse functions for scrolling up/down, setting the cursor and highlighting text. 32 | 33 | The editor assumes a VT100 terminal. It works in Insert mode. The following list 34 | shows most of the commands.: 35 | 36 | |Key(s)|Function| 37 | |:---|:---| 38 | |Up Down Left Right| Cursor movement by one line or char| 39 | |Ctrl-Left| Move the cursor left to the start of the (next) word| 40 | |Ctrl-Right| Move the cursor right behind the end of the (next) word| 41 | |Shift-Up -Down -Left -Right| Highlight the text or extend the highlighted area| 42 | |Ctrl-Shift-Left -Right|Highlight the next or previous word or extend the highlighted area| 43 | |Ctrl-Up Ctr-Down|Scroll the windows down/up| 44 | |Alt-Up Alt-Down|Move the current line or highlighted area up/down by one line| 45 | |Alt-Left Alt-Right|Move the character under the cursor left/right| 46 | |PgUp & PgDd|Page up/down| 47 | |Home|Toggle the position between the start-of-code and the start of line| 48 | |End|Toggle the position between the end-of-the-code and end-of-line| 49 | |Enter|Enter a line break at the cursor position. Auto-indent is supported| 50 | |Backspace|Delete char left to the cursor (The key must be set to ASCII-Del). If text are highlighted, delete the highlighted area| 51 | |Del|Delete the char under the cursor. At the end of the line join the next line. If autoindent is enabled, delete also the leading spaces of the joined line. If text are highlighted, delete the highlighted area. In line edit mode, Del as first keystroke will clear the entry.| 52 | |Ctrl-Del|Delete the word under the cursor or space up to the next non-space| 53 | |Shift-Del|Delete the actual line| 54 | |Ctrl-O|Open a new file. If the file name is left empty, an empty buffer is opened| 55 | |Ctrl-W|Switch to the next file buffer| 56 | |Ctrl-Q|Close a file buffer or end line-edit| 57 | |Ctrl-S|Save to file with the option to change the file name. If a new name is given and that file already exists, ask for confirmation.| 58 | |Ctrl-F|Find| 59 | |Ctrl-N|Repeat the last find| 60 | |Ctrl-H or Ctrl-R|Find and Replace| 61 | |Ctrl-G|Go to a line| 62 | |Ctrl-T|Go to the first line| 63 | |Ctrl-B|Go to the last line| 64 | |Ctrl-K|Goto the bracket matching the one under the cursor| 65 | |Ctrl-Home & Ctr-End|Go to the first/last line| 66 | |Ctrl-PgUp & Ctrl-PgDd|Switch to the previous/next file| 67 | |Alt-Ins|Bookmark a location in a file| 68 | |Alt-PgUp & Alt-PgDn|Cycle trough bookmarked locations| 69 | |Alt-Home & Alt-End|Cycle trough locations with changes, based on the Undo list| 70 | |Ctrl-L or Ctrl-Space|Start highlighting at the current position, or clear the highlight. The highlight can then be extended by moving the cursor| 71 | |Ctrl-X|Cut the highlighted text| 72 | |Ctrl-C or Ctrl-D|Copy the highlighted text| 73 | |Ctrl-V|Insert the copied/cut text.| 74 | |Ctrl-Z|Undo the last change(s)| 75 | |Ctrl-Y|Redo the last undo(s), repeating what had been undone by undo| 76 | |Ctrl-P|Comment/Uncomment a line or highlighted area| 77 | |Ctrl-A|Change settings for tab size, search case sensitivity, auto-indent, comment string and writing tabs (opt)| 78 | |Ctrl-E|Redraw the screen. On the Micro devices it shows the amount of free memory| 79 | || 80 | ||Mouse support| 81 | || 82 | |Mouse scroll wheel|Move the cursor up and down, scrolling the content as needed.| 83 | |Mouse left click|Within the text: Double click starts highlighting at the current position, or clears the highlight. The click speed does not matter. If the cursor is on a word, the whole word will be highlighted. If not, just the character under the cursor. The highlight can then be extended by moving the cursor clicking at different positions.| 84 | |Mouse right click|Opens the "find" dialogue in a text file and the "open file" dialogue in a directory listing.| 85 | |Mouse left click|In line edit mode: Clicking on a word copies it to the edit line. Clicking a second time at the same place acts as enter.| 86 | 87 | **Instead of Ctrl-letter (e.g. Ctrl-Q), Alt-letter (e.g. Alt-Q) or ESC-letter (e.g. ESC followed by Q) can be used, avoiding conflicts with key binding of some terminal emulators.** 88 | 89 | The editor is contained in the files pye_gen.py and pye_core.py. Start pye from the REPL prompt 90 | e.g. with 91 | 92 | from pye import pye 93 | res = pye(object_1, object_2, ..[, tabsize=n][, undo=n]) 94 | 95 | You may also use the combined version pye.py resp. pye.mpy: 96 | 97 | from pye import pye 98 | res = pye(object_1, object_2, ..[, tabsize=n][, undo=n]) 99 | 100 | If object_n is a string, it's considered as the name of a file to be edited 101 | or a directory to be opened. If it’s a file, the content will be loaded, 102 | and the name of the file will be returned when pye is closed. If the 103 | file does not exist, an error is displayed, but the edit window is given that 104 | name. If it’s a directory, the list of file names will be loaded to the edit 105 | window. If object_n is a list of other items, they will be converted to strings 106 | and edited, and the edited list will be returned as strings. 107 | If no object is named, pye() will show the list of files in the current directory, 108 | creating a list of strings, unless you save to a file. In that case, 109 | the file name will be returned. 110 | It is always the last buffer closed, which determines the return value of pye(). 111 | 112 | Optional named parameters: 113 | 114 | tabsize=n Tab step (integer). The default is 4 115 | undo=n Size of the undo stack (integer). The minimal size is 4. 116 | 117 | The Linux/Darwin version can be called from the command line with: 118 | 119 | python3 pye_ux.py [filename(s)] 120 | 121 | or using the combined executable version pye: 122 | 123 | pye [filename(s)] 124 | 125 | Obviously, you may use micropython too. 126 | 127 | More details can be found in the doc file. On reading files, tab characters 128 | are expanded to spaces with a tab size of 8, and trailing white space on a 129 | line will be discarded. Optionally, tabs can be written when saving the file, replacing 130 | spaces with tabs when possible. However, the original state of tabs will NOT be restored when 131 | the file is written. The screen size is determined, when the editor is 132 | started, when the Redraw-key (Ctrl-E) is hit or on any file window change (Ctrl-W). 133 | 134 | The editor works also well in a Linux or MAC terminal environment (and also in some 135 | terminal apps of Android - tested with Termux) with both python3 and micropython. 136 | For that purpose, a small main() section is embedded in pye_ux.py, which 137 | when called with CPython also accepts data from a pipe or redirection. 138 | 139 | ## Files 140 | 141 | ### Base files 142 | 143 | - pye_core.py: Core file with comments, intended to be the same for all platforms 144 | - pye_ux.py: Front-end for Linux CPython and Linux MicroPython 145 | - pye_gen.py: Front-end for Micropython and Circuitpython modules with I/O through 146 | sys.stdout and sys.stdin. 147 | - peteensy.py: A front-end for Teensy 3.5 and 3.6 . 148 | - pye_win.py: an experimental front end for the cmd window of Windows 10. It requires 149 | enabling the VT100 support, as detailed e.g. here: https://stackoverflow.com/questions/51680709/colored-text-output-in-powershell-console-using-ansi-vt100-codes 150 | - pye_xbee.py: Core file for XBEE 3 devices. 151 | - Pyboard Editor.pdf: A short documentation 152 | - strip.sh: sample Shell script which creates the different derived files out of pye.py and 153 | the front-end files, using cat and sed. 154 | - README.md: This one 155 | 156 | ### Derived files 157 | 158 | - pye.py, pye.mpy: Condensed source files of pye_core.py + pye_gen.py for 159 | all MicroPython boards. In order to use it on an board with small memory 160 | like the esp8266, you have to put pye_mp.py into the directory esp8266/modules, 161 | esp32/modules or stm32/modules (micropython.org) or esp32/frozen (pycom.io) and 162 | rebuild micropython. A cross-compiled version may executed from the file system. 163 | - pye_x3.py, pye_x3.mpy: Condensed file for XBEE 3 devices. The .mpy file can 164 | be imported from the file system, but it should be better bundled into flash with 165 | uos.bundle(). 166 | - pye: Executable single file for Linux 167 | - pye_win: Executable single file for Windows 10 168 | 169 | ### Further front-ends 170 | 171 | - pye_lcd.py: Front-end provided by user @kmatch98 for Circuitpython using a LCD as output 172 | device and UART as input device. The port requires further python scripts. 173 | - lcd_io.md: Readme for the pye_lcd version, provided by @kmatch98. 174 | - pye_tft.py: Sample front-end using a tft with a SSD1963 controller as output device. The port 175 | requires the SSD1963 drivers: https://github.com/robert-hh/SSD1963-TFT-Library-for-PyBoard 176 | 177 | ## Branches 178 | 179 | |Branch|Features| 180 | |:---|:---| 181 | |master|Actual main line with slowly changing features.| 182 | |pye2|Similar to the master branch, but the column does not change during vertical moves (stale).| 183 | |pye_min|Feature-reduced version, which is somewhat smaller (stale).| 184 | 185 | ## Version History 186 | 187 | **1.0** Initial release with all the basic functions 188 | 189 | **1.1** Same function set, but simplified keyboard mapping. 190 | 191 | - Removed the duplicated definitions for cursor motion keys. 192 | - Allowed both \r and \n for ENTER, and both \x08 and \x7f for BACKSPACE, which 193 | avoid some hassle with terminal settings. 194 | - Removed auto-indent from the minimal version. 195 | 196 | **1.2** Mouse support added, as well as some other minor changes. 197 | 198 | - Simple Mouse support for scrolling and placing the cursor 199 | - Flags setting for search case, auto-indent on/off and status line on/off 200 | - GOTO line sets cursor to the middle row 201 | - The function pye(..) returns a value now 202 | 203 | **1.3** UNDO added. Added a multilevel UNDO (Ctrl-Z) for most functions that 204 | 205 | change the content. Other changes: 206 | 207 | - Backspace at the first non-blank character mimics BackTab, if Auto-indent is 208 | enabled 209 | - Added a REDRAW (Ctrl-E) function, which checks for the changed screen size 210 | after the window size has been changed. 211 | - Added a line number column on the left side of the screen (can be turned off). 212 | - Improved the scrolling speed, such that it lags less. 213 | - Some code simplification and straightening, such that functions group better 214 | and are easier to understand. 215 | 216 | **1.4** GET file added. Adding a function GET (Ctrl-O), which inserts the 217 | content of a file before the current line. Other changes: 218 | 219 | - Both HOME and END stop at start of text is passing by on their way to their 220 | destination. 221 | - Flag allowing to replace spaces by Tab when writing the file, complementary to 222 | what is done while reading. Tabsize is 8. A tab is inserted whenever possible, 223 | even if it replaces a single space character. 224 | - Fixed a mild amnesia in UNDO 225 | 226 | **1.5** WiPy Port and body shaping: 227 | 228 | - Support for WiPy added. WiPy runs only the minimal version. 229 | - Aligned function set of the minimal version, in order to comply with WiPy. 230 | Dropped Mouse support, GET file, Line number column, and write tabs; but 231 | included Tab, Backtab, the buffer functions Yank, Dup & ZAP and scrolling optimization. 232 | - LEFT and RIGHT move to the adjacent line if needed 233 | - When used with Linux **and** CPython, a terminal window resize cause redrawing 234 | the screen content. The REDRAW key (Ctrl-E) stays functional and is required 235 | for all other use cases, when the window size is changed. 236 | - HOME toggles again between start-of-line and start-of-text. END moves always 237 | to end-of-line 238 | - Dropped context sensitive behavior of Tab, Backtab, Backspace and Delete. Too confusing. 239 | - Dropped the line number column, and made the status line permanent in all modes. 240 | - Rearranged the code such that any platform related sections are grouped together. 241 | 242 | **1.6** WiPy fixes and further trimming: 243 | 244 | - Making rarely used small functions inline again, which saves some space. Important for WiPy. 245 | - Catch Ctrl-C on WiPy. Not really nice yet, since the next input byte is lost. 246 | - Tab Size can be set with the Ctrl-A command (if available). 247 | - Simplified Linux main(). No calling options any more. 248 | - Always ask when leaving w/o saving after the content was changed. 249 | 250 | **1.7** WiPy fixes and other minor changes 251 | 252 | - Fixed a memory leak in REDRAW, removed every instance of "del name" 253 | - Changed REDRAW to tell the amount of free memory 254 | - changed wr() for WiPy 255 | - Simplified the internal interface to init_tty() 256 | - Changed the handling of reading an empty file 257 | - Non-supported functions in the minimal version like REPLACE trigger a "Sorry" message 258 | - Try to recover from MemoryError by clearing the line-buffer and UNDO 259 | 260 | **1.7a** Size reduction for WiPy & Tabbify for PyBoard 261 | 262 | - Reduced KEYMAP in the WiPy version by omitting entries, where the function 263 | code is identical to the key value (e.g. \x08 -> 8). Not fool proof, but it helps 264 | reducing the size. 265 | - Adding a "Tabbify" behavior for the full version. Tab/Backtab with the cursor 266 | at col 1 indents/unindents the line and moves the cursor one line down. 267 | 268 | **1.7b** Further size reduction for WiPy 269 | 270 | - Moved setting of the change flag into the function add_undo() 271 | - Removed skipping to the adjacent line with Right/Left in the WiPy Version 272 | - Temporary avoidance of the memory leak when a file is not found 273 | 274 | **1.7c** Re-establish try-except for file-not-found error 275 | 276 | - Removed the temporary fix for memory corruption by exception for file-not-found error 277 | - Changed string formatting to Python3 style 278 | 279 | **1.8** Clean Copy & Paste, Indent, Un-Indent 280 | 281 | - Added a Highlight Line key for Line Delete, Line Copy, Indent and Un-Indent 282 | - Fixed a glitch, that allowed to paste text longer 283 | then the available space on the status line. No harm was done, just the screen 284 | content scrolled up. After leaving the line edit mode, a redraw fixed that. 285 | - Changed Line Delete, Line Copy and Buffer Insert into a cleaner Copy & Paste mode 286 | - Added a cleaner Indent and Un-Indent method; for WiPy too 287 | - Removed the attempt to recover from out-of-memory situations: did not work. 288 | - Still runs on WiPy, but really at it's limit 289 | 290 | **1.9** Refinement of Highlight and Undo 291 | 292 | - Highlight setting affects Save and Replace now. With Save, only the highlighted range is 293 | written, with replace, search & replace is done in the highlighted area only. 294 | - The Undo history is kept after Save. So you can go back to a state before saving 295 | - Removed UART mode on WiPy. Not stable yet. UART mode can be achieved by 296 | redirecting REPL. 297 | - A variant of pye.py, called pye2.py, keeps the cursor column even if the 298 | cursor is moved beyond the text in a line, instead of moving to the end of text 299 | if a line is shorter than the actual cursor column. Another variant, pye3, tries 300 | to go back to the cursor column which once was set by a horizontal move. 301 | That's more how gedit works. Not sure which I like better. 302 | 303 | **1.10** Further refinement of Highlight 304 | 305 | - When the highlight is set, the whole area affected is now highlighted instead of 306 | just the line with the highlight. 307 | - Paste, Delete and Backspace now also take notice of the line Highlight. You can Highlight 308 | a line range and delete it (or cut it). Implicit deleting highlighted lines when 309 | pressing the Enter or character key was considered but rejected (easy - just 3 310 | lines of code). 311 | - Except for Delete, Backspace, Cut and Paste, Highlight has to be toggled off when 312 | not needed any more. 313 | - Right click (Button 2) or Ctrl-Click on the mouse sets/unsets the Highlight, left 314 | Click extends it, when set. 315 | 316 | **1.11** Minor fixes 317 | 318 | - Change the way a highlighted area is highlighted from reverse to a different 319 | background colour. That works well for black chars on yellow background (code 43). 320 | For white chars on black background, the setting for background colour in the 321 | function hilite() has to be changed, e.g. to blue (code 44). 322 | - Save to a temporary file first, and rename it to the target name when 323 | successfully written. 324 | - Lazy screen update: defer screen update, until all chars from the keyboard 325 | are processed. Not provided for WiPY, even if needed there most. WiPy has no 326 | way to tell if more chars are waiting in the input or at least a read with timeout. 327 | 328 | **1.12** Bracket Match and Minor changes 329 | 330 | - Ctrl-K causes the cursor set to the matching bracket, if any. Pretty raw, not elegant. 331 | Brackets in comments and strings are counting as well. 332 | - On Copy the highlight will be cleared, since it is assumed that the just copied 333 | lines will not be overwritten. 334 | - High level try/except catching internal errors (mostly coding errors) 335 | - Separate cpp options for including scroll optimization, replace or bracket 336 | match into the minimal version. Changes in strip.sh script to generate the 337 | minimal wipye version too. 338 | - Some editorial changes and fixing of typos. 339 | 340 | **1.12b** Fixing an inconsistency in the Save command 341 | 342 | - Fixing an inconsistency in the Save command, which caused the change flag being 343 | reset when writing just a block 344 | - Squeezing a few lines out of the source code 345 | 346 | **1.12c** Speed up pasting again 347 | 348 | - Speed up pasting again. Slowing down pasting was caused by an in-function 349 | import statement in V1.11. 350 | - Squeezing another few lines out of the source code by combining two functions, 351 | which were anyhow called one after the other, resulting in an enormous long function 352 | handling the keyboard input. 353 | 354 | **1.12d** Split undo of Indent/Un-Indent 355 | 356 | - Split undo for Indent and Un-Indent 357 | - Fixed a minor inconvenience when going left at the line start (squeezed too much in v1.12b) 358 | - Move a few lines around, such that keys which are more likely used with fast 359 | repeats are checked for earlier. 360 | - Some editorial changes 361 | 362 | **2.0** Edit multiple files 363 | 364 | - Support for editing multiple files at once and copy/paste between them 365 | - Ctrl-W steps through the list of files/buffers 366 | - Ctrl-O opens a new file/buffer. 367 | 368 | **2.1** Some shrinking for WiPy 369 | 370 | - Make Indent/Un-Indent optional in the WiPy version, to allow all variants to 371 | get compiled w/o running out of memory. The final code saving is just a few 372 | hundred bytes, so it's still not clear to me why these few extra lines don’t fit. 373 | - Fixing a glitch which added an extra line when undoing the delete of all lines 374 | - Some shifting around of code lines 375 | - Making the MOUSE support an extra option 376 | - Removed the extra indent after ':' as the last char on the line. More confusing 377 | than helpful. 378 | - Update of the doc file 379 | 380 | **2.2** Further cleaning and some slight improvements 381 | 382 | - Moved error catching one level up to the function pye(), catching load-file 383 | errors too. 384 | - If open file names a directory, the list of files is loaded to the edit buffer. 385 | - Ctrl-V in line edit mode inserts the first line of the paste buffer 386 | - The WiPy version does not support undo for Indent/Un-indent, even if Indent is 387 | enabled. It is too memory consuming at runtime. It's questionable whether this 388 | is needed at all. 389 | - And of course: update of the doc file 390 | 391 | **2.3** Minor fixes & changes 392 | 393 | - Catched file not found errors when starting pye, introduced in version 2.2 394 | - Added a flag to pye2 such that it supports both vertical cursor movement types 395 | - use uos.stat with micropython, since os.stat is not supported on linux-micropython 396 | - When opening a directory, replace the name '.' by the result of os.getcwd(), 397 | avoiding error 22 on PyBoard and WiPy 398 | 399 | **2.4** Fix for the regular expression search variant 400 | 401 | - Fix a glitch, that the regular expression variant of search and replace did 402 | not find patterns anchored at the end, single line starts or single line endings. 403 | That fix required changes not only to the find function, such that all variants 404 | of pye are affected. 405 | - Consider '.' **and** '..' in file open as directory names, avoiding stat() on these. 406 | 407 | **2.5** Fix a small bug of edit_line()'s paste command 408 | 409 | - Fixed a glitch, that allowed to paste text longer then the available space on 410 | the status line. No harm was done, just the screen content scrolled up. After 411 | leaving the line edit mode, a redraw fixed that. 412 | 413 | **2.6** Adapted to change lib names in micropython 414 | 415 | - For micropython replaced \_io with uio 416 | - Preliminary esp8266 version. 417 | 418 | **2.7** Change file save method and settings dialogue 419 | 420 | - Further adaptation to esp8266, which is now identical to the WiPy version 421 | - Changed file save method, such that it works now across devices 422 | - Made settings dialogue visible in basic mode, allowing to change both the 423 | autoindent flag and the search case flag 424 | - Create the ESP8266 version with all features but mouse support. 425 | 426 | **2.8** Support of UTF-8 characters on keyboard input 427 | 428 | - This in implemented for all versions. However WiPy does not support it in 429 | the lower levels of sys.stdin. 430 | 431 | **2.9** Support for teensy 3.5 and 3.6 432 | 433 | - The only change was to add the teensy names to the platform detection 434 | - Implement full function set for line-edit by default 435 | 436 | **2.10** Support for esp32; simplified mouse handling 437 | 438 | - The only change was adding the esp32 name to the platform detection 439 | - Do not use global symbols for mouse parameters 440 | 441 | **2.11** Small changes to the esp8266 version 442 | 443 | - Faster paste from the keyboard 444 | - Enabled Mouse support as default 445 | 446 | **2.12** Use the kbd_intr() method to disable Keyboard Interrupts on Ctrl-C. 447 | 448 | This method is supported by (almost) all micropython.org ports and maybe 449 | sometime also supported by the pycom.io ports. The method of trying to catch 450 | KeyboardInterrupt is still present. 451 | 452 | **2.13** Reduce the number of derived variants and make the full version the 453 | default one. Update the documentation. 454 | 455 | **2.14** Remove the PyBoard support using an UART as keyboard/display interface. 456 | Use kbd_intr() too on Pyboard. 457 | 458 | **2.15** Make a combined MicroPython version, which runs on all boards except Teensy 3.5 / 3.6. For teensy, a short wrapper is included. 459 | 460 | **2.16** Optimize search with Regular expressions 461 | 462 | **2.17** Remove all option variants from the source files, and make pye_sml.py 463 | a pure micropython boards version with reduced function set. The only options 464 | remaining are Linux/CPython vs. MicroPython 465 | 466 | **2.18** On deleting the end of a line, remove space characters from the joined line, if autoindent is active. This behavior mirrors autoindent. 467 | 468 | **2.19** Add a toggle key for commenting/uncommenting a line or highlighted area. The 469 | default comment string is '#', but can be changed through the settings command. 470 | 471 | **2.20** Change the End-Key to toggle between EOL and last non-space char. 472 | 473 | **2.21** Add Ctrl-\ as alternative key to close a file 474 | 475 | **2.22** Add Ctrl-Space asl alternative to highlight line (Ctrl-L), and Block comment adds the comment string after leading spaces. 476 | 477 | **2.23** Change the End key to toggle between end-of-line and end-of-code 478 | 479 | **2.24** Changed the Paste key in line edit mode, in that it pastes the word under the cursor of the active window. 480 | 481 | **2.25** Version number is shown with redraw command, and thus at startup and 482 | window change 483 | 484 | **2.26** Better separation of port-specific and OS-specific flags 485 | 486 | **2.27** Change Home/End key behavior. Py w/o filename will open a window with 487 | the list of files in the current dir 488 | 489 | **2.28** Add word left & right with ctrl-left and ctrl-right 490 | 491 | **2.29** Add shift-up and shift-down for setting/extending the highlighted area 492 | 493 | **2.30** Add Delete Word with Ctr-Del 494 | 495 | **2.31** Re-Map Ctrl-Up and Ctrl-Down for scrolling 496 | 497 | **2.32** Comment toggle ignores empty lines 498 | 499 | **2.33** Scroll step is 1 for keyboard and 3 for mouse 500 | 501 | **2.34** Added a branch with character mode highlight/cut/paste/delete. Intended to be the new master 502 | 503 | **2.35** Change behavior of the column position during vertical moves, in that it tries to keep the position 504 | 505 | **2.36** Add the redo function, which restates changes undone by undo. 506 | 507 | **2.37** Entering text replaces an highlighted area. 508 | 509 | **2.38** Add Ctrl-Shift-Left and Ctrl-Shift-Right for move & highlight 510 | 511 | **2.39** Move a line with Alt-Up and Alt-Dn 512 | 513 | **2.40** Extend move up/down to move highlighted areas too. 514 | 515 | **2.47** Move all terminal control strings into an list. 516 | 517 | **2.48** Split pye.py into a core file and port specific front-ends. If a single file is required, just put the core and the front-end into a single file. 518 | 519 | **2.49** Change the default search item behavior of find and replace as well as open file. 520 | 521 | **2.50** Limit the span for bracket match to 50 lines and improve the speed 522 | 523 | **2.51** Re-establish automatic screen redraw on resize for Linux 524 | 525 | **2.52** Fix regression error on file creation 526 | 527 | **2.53** Fix regression error on handling directories 528 | 529 | **2.54** Fix regression error in the Linux variant 530 | 531 | **2.55** Cope with the change from renaming the sys module to usys 532 | 533 | **2.56** Fix regression error when opening multiple files 534 | 535 | **2.57** Slight speed improvement when pasting 536 | 537 | **2.58** Fix a glitch in redo after undo after pasting a full line. 538 | 539 | **2.59** Change mouse support. Clicking twice at the same position enables/disables marking. 540 | 541 | **2.60** Extend mouse support 542 | 543 | **2.61** Further straighten mouse support. Double Click is click at the cursor position, and the mark can be extended to both sides with the mouse without affecting what was already marked. 544 | 545 | **2.62** Ask for confirmation before overwriting an existing file other than the one initially opened. 546 | 547 | **2.63** Change mouse behavior again. Right click opens the "find" dialogue in a text window or "open file" dialogue in a directory listing. 548 | 549 | **2.64** Enable additional file name chars in the file save dialogue (was omitted) 550 | 551 | **2.65** Check for invalid key sequence. Show the search item in "not found" messages 552 | 553 | **2.66** Fix an display error when doing a left click beyond the end of text during line edit 554 | 555 | **2.67** Make double click in the text more consistent. Mark/Unmark always requires a double click. 556 | 557 | **2.68** Make double click timed. Time is < 2 seconds 558 | 559 | **2.69** Minor reorganisation in the function get_input. Firt version of a Window 10 front-end. 560 | 561 | **2.70** Move character under the cursor left/right with the Alt-Left/Alt-Right key. 562 | 563 | **2.72** Add delete line and force quit 564 | 565 | **2.73** Use Ctrl-PgUp and Ctr-PgDn to switch files. 566 | 567 | **2.74** Add functions to bookmark locations and go to them, using Alt-Home and Alt-PgUp / Alt-PgDn 568 | 569 | **2.75** Fix an error with character swap. 570 | 571 | **2.76** Cycle through places with changes. 572 | 573 | **2.77** Change key binding for bookmark location to Alt-Ins, using Alt-Home and Alt-End to cycle through 574 | locations with changes. 575 | 576 | -------------------------------------------------------------------------------- /additional_documentation/Pyboard Editor.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hh/Micropython-Editor/ed544c7a984be6db3d88c1682924409bae339da1/additional_documentation/Pyboard Editor.doc -------------------------------------------------------------------------------- /additional_documentation/Pyboard Editor.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hh/Micropython-Editor/ed544c7a984be6db3d88c1682924409bae339da1/additional_documentation/Pyboard Editor.pdf -------------------------------------------------------------------------------- /additional_documentation/UserGuide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Phemto -pye - a tiny editor for a tiny environment written in python 5 | 33 | 34 | 35 | 36 | 37 |
38 |

Phemto

39 | A python editor that's tiny for tiny environments 40 |

pye

41 |

User's Guide

42 |
43 | 44 |

45 | Allows editing files directly on python based target boards, using a 46 | terminal emulator such as MSWindows putty, Mac os terminal or Linux terminal. 47 | 48 |

Operations include:

49 | 57 | 58 |

59 |

Features

60 | 61 | 74 | 75 |

76 | The editor operates in insert mode, that is, characters typed are inserted, control+key, alt+key and ,key are commands.
77 | Cursor Keys, home, end, page^ … work as you would expect. 78 | 79 | 80 |

Installation

81 | Transfer to the target board: 82 |
  1. The code: 83 |
      84 |
    • pye.mpy (frozen bytecode for Pyboard, ESP8266, ESP32, PyCom devices, Maixpy …) or 85 |
    • compiled byte code which reduces start-up time and saves memory and file space or 86 |
    • pye.py source 87 |
    88 |
  2. The file h for online help in an alternate window(optional). 89 |
90 |

91 | 92 |

Starting pye

93 | From python REPL prompt:
94 | 95 | >>>> from pye import pye
96 | >>>> [result =] pye([ object_1, object_2, …[, 97 | tabsize=n][, undo=n ] 98 | 99 | ) 100 |
101 | 102 | 124 | 125 |

126 |

131 | 132 | 137 | 151 |

152 |

Keystroke commands (Operations)

153 | 154 | Notations: 155 | 160 | 163 |

164 | 165 | 166 | 169 | 179 | 188 | 189 | 213 | 214 | 229 | 252 | 270 | 286 | 318 |
FunctionKeys 167 | 168 |
Open a new file to a window ^o 170 |
Save ^s 171 |
Quit a window 172 | or the line edit mode. ^q
173 | 175 |
Previous window ^[PgUp] 176 |
Next window ^[PgDn]
^W 177 | 178 |
Inserting
180 |
Characters typed are inserted 181 |
Insert spaces to the next tab stop. ( Default tab stops are every 4 columns)
182 | Indent the selection.
[tab] 183 | 184 |
line break at the cursor position. Auto-indent depends on setting. 185 | [enter] 186 | 187 |
Movement
go to: 190 |
Top of the file ^t
^[home] 191 |
Left to the start of the previous word 192 | skipping over to the previous line. ^← 193 | 194 |
Start-of-line OR start-of-text [home] 195 |
Right after the next word or line end, 196 | wrapping over to the next line. ^→ 197 |
End-of-code OR End-of-line [end] 198 | 199 | 200 |
Line # ^g 201 |
Bottom of the file ^b
^[end] 202 | 203 |
previous Change
204 | next
205 |
~[home]
~[end] 206 | 207 | 208 |
Bookmark a location in any window. ~[ins] 209 |
previous Bookmark
210 | next
~[PgUp]
~[PgDn] 211 | 212 |
Match bracket the cursor is on.
215 | Bracket pairs are ( ), [ ], { } and < > 216 |
^k
217 |
Current line or selected up / down with the cursor 218 | ^↑ / ^↓ 219 |
Cursor up / down a screen [PgUp] / [PgDn] 220 | 221 |
Scroll the window up / down ^↑ / ^↓ 222 | 223 | 224 | 225 | 226 | 227 | 228 |
Selecting text 230 | 231 |
Start selection
232 | Once selecting is started it can be extended by moving the cursor.
233 | The selection is effected by Delete, Backspace, Copy, Cut, Paste, Tab, 234 | Backtab, Find, Save and Replace.
235 | 236 | End selecting by pressing ^L or ^space or an operation(copy, delete,...). 237 |
shift →
shift ←

238 | ^L

^space 239 | 240 |

Select previous / next word or extend the Selection. ^shift ← / ^shift → 241 |
Copy selection to the clipboard ^c 242 |
Cut selection to the clipboard. 243 | The selection is cleared.
244 | This implements the "Cut & Paste" feature.
^x 245 | 246 |
Paste the clipboard contents at the cursor position.
247 | If text is selected, it is replaced.
248 | In the line edit mode: paste the item under the cursor.
^v 249 |
Move selection up/ down ~↑ / ~↓ 250 | 251 |
Deleting 253 |
Selection [del]
[backspace] 254 | 255 |
Char left of the cursor.
256 | Join to the previous line if cursor is at the beginning of the line.
257 |
[backspace] 258 | 259 | 260 |
Char under (before) the cursor.
261 | Join with the next line if cursor is at the end of the line
262 | With auto-indent, delete the leading spaces of the line.
[del] 263 | 264 |
Word under the cursor or spaces up to the next non-space. ^[del] 265 |
Spaces back to the previous tab stop or non-space [backtab] 266 |
Line shift[del] 267 |
Un-indent a single line or multiple selected lines. ^u
[backtab] 268 | 269 |
Finding 271 | 272 |
Find a regular expression.
273 | For example ^def finds def at the beginning of a line.
276 | Skips a selection.
277 | Case sensitivity is set by ^a. Stops at the end of file.
278 | The search string is remembered.
^f 279 | 280 |
Next find ^n 281 | 282 |
Find and replace. Optionally:  yes, all, no(skip), quit
283 | With a selection only the selection is searched and all only applies to the selection.
^r
^h 284 | 285 |
Additional Commands 287 | 288 |
Redraw the screen and display file size and free space
289 | 290 | *main.py Row: 5/116 Col: 30. 2524KB Memory free. File 213KB, 3121KB free. 291 |
^e 292 |
Undo the last change(s). ^z 293 |
Redo the operation undone by Undo 294 | immediately after Undo(s).
295 | Any change to the text invalidates the redo history.
^y 296 | 297 |
Comment / Uncomment a line or selected area.
298 | That is: insert / remove the comment character(s) (default '#')
^p
^_ 299 | 300 |
Adjust Attributes that is: those provided when pye was started.
301 | Enter y, n, a number or several characters in comma separated fields (example:y,n,4,#,n)
302 | Empty field leaves the value unchanged.

303 | 304 |

    The defaults are :
    305 |
  • auto-indent: y
    306 |
  • search case sensitive: n
    307 |
  • tab-size: 4 ; that is a tab stop occurs every 4 columns
    308 |
  • comment string #
    309 |
  • write-tabs: n
    310 | 313 |
^a 314 | 315 | 316 | 317 |
Mouse Actions 319 |
Set the cursor. Button 1 320 | 321 |
Set / Clear highlighting. Double click
Button 1 322 |
Find and open a directory window button 2 323 |
    Search button1 324 |
    Open file button1 325 | 326 | 327 |
Scroll Up / Down the screen content by 3 lines per tick. Scroll Wheel 328 | 329 |
330 |

331 | 332 | 333 | 334 | 335 | 336 |

Additional notes

337 | 338 |

339 |

Status line

340 |
341 | *main.py Row: 5/116 Col: 30      
342 | ^- asterisk indicates window contents have been changed. 343 |
344 |

345 | For online help use ^oh to open the file h in a new window and use ^w 346 | to switch between helpfile and editing window. 347 | 348 |

349 | 350 |

Opening Files

351 | When opening a new window the filename will be prompted for.
352 | If the name is empty, an empty window will be opened.
353 | If the file cannot be loaded (for example it does not exist), an empty window with that name will be opened.
354 | If the name is a directory a list of filenames will be loaded. 355 | 356 |

357 | 358 |

Exiting

359 | 360 | When saving, the content is written to a temporary file (basename + ?.pyetmp?) then it is 361 | renamed to the filename.
362 | When saving an unnamed window, the filename will be prompted for.
363 | If the filename is invalid, the temporary file will have the content.
364 | After saving the window will be named as the saved file, which acts as Save-as.
365 | 366 |

367 | For quit, if the edited text was changed, confirmation is asked for.

368 | 369 | When the last window is closed, the pye terminates providing the return value: 370 | 371 | 372 |
Editing Return Value 373 |
  file  name of the file 374 |
  list  list 375 |
  directory   name of the directory 376 |
377 | 378 |

Options

379 | 380 | The option to replace all sequences of spaces by 381 | tabs, tab size 8. If the initial file contained tab characters, this is the default. 382 | When reading files, tabs are replaced by spaces, tab size 8, and white space at the end of a line is discarded.

383 | 384 |

385 | The undo stack size per window can be set pye() default: micro 50, PC systems 500. 386 | 387 | 388 |

389 |

PC Usage

390 |

391 | The PC version can be called from the command line with:
392 | pye.py [filename , … ] 393 | 394 |

395 | Using python3 pye.py [filename , … ] content can be redirected or pipe'd to pye. 396 | 397 |

398 | 399 |

Internals

400 | 401 |

402 | Uses stdin/stdout on the MicroPython boards. 403 |

Board specific information

404 |

405 | WiPy1: only works only on a Telnet session with the UART connection closed.
406 | ESP8266: only the frozen bytecode variant can be used.
407 | 408 |

Memory Considerations

409 |

410 | The size of a file that can be edited is limited by the memory of the target board.
411 | Files up to about 13kb on ESP8266 and 68kb on PyBoard should fit.
412 | REDRAW updates available memory show in the status line. 413 | In addition to the file, clipboard and especially undo consume memory.
414 | The undo stack can be limited in the call to pye using undo=n.
415 | The clipboard size can be reduced by copying a single line into it.
416 | 417 |

418 | When saving a file on PyBoard, the changes may not 419 | be visible in the file system of a connected PC until you disconnect and reconnect the Pyboard drive.
420 | See also the related discussion in the MicroPython Forum. 421 | 422 |

423 | Saving to the internal flash file system of PyBoard is slow. 424 |

425 | 426 | In some cases, Escape seems to lock the keyboard, it unlocks by typing any alpha character. 427 | 428 |

Terminal Emulators

429 | 466 | 467 | 496 |

497 | See Pyboard_Editor.doc in GitHub 498 | for additional details. 499 | 500 |

501 | DGerman@Real-World-Systems.com version 1.5, 8/10/22 for pye version 2.77 502 | 503 | 504 | 505 | 506 | -------------------------------------------------------------------------------- /additional_documentation/h: -------------------------------------------------------------------------------- 1 | ^ is [control] or [esc] ~ is [alt] | ^open file ^save ^quit 2 | Previous window ^[PgUp] | 3 | Scroll up ^[up] | ^find ^next find & replace ^r ^h 4 | Go to: | undo ^z redo ^y 5 | top of file ^t ^[home] | redraw ^e 6 | | 7 | beginning of / after word ^<- / ^-> | Selection start / end ^l 8 | start-of-text and the start of line [home] | Select text [shift]-arrow 9 | end-of-the-text and end-of-line [end] | Moving cursor selects more. 10 | | Copy / Cut selection to clipboard ^c / ^x 11 | line # ^g Matching ()[] {} <> ^k | Paste clipboard ^v 12 | | Delete selection [delete] 13 | change previous / next ~[home] ~[end] | Move selection up / down ^up / ^down 14 | Create Bookmark ~[ins] | 15 | Go to prior / next bookmark ~[PgUp] / ~[PgDn] | Indent / un-indent [tab] / [backtab] 16 | | Toggle comment (add/ remove #) ^p ^_ 17 | Delete: | Move current line up / down ~up / ~down 18 | Char left of the cursor [backspace] | Move character under cursor ~<- ~-> 19 | Char under the cursor [delete] | Insert a line break [enter] 20 | Word under the cursor ^[delete] | 21 | Space up to the next non-space ^[delete] | Change pye attributes ^a 22 | line Shift-[delete] | (indent, find case, tab size, 23 | | comment string, write tabs) 24 | Join with previous line, if at beginning of line [backspace] 25 | Join with next line, if at end of line and 26 | delete leading spaces of the joined line. [backspace] 27 | Clear the entry in line edit mode, [delete] as first keystroke 28 | 29 | Go to Bottom ^b ^[end] v.99 phor pye v2.77 30 | Scroll the window down ^[down] 31 | Next window ^[PgDn] ^w 32 | ___________________________________________ 33 | Mac keyboard: 34 | Go to: 35 | before/after word ^cmd-left/right arrow 36 | start-of-text and the start of line fn-left 37 | end-of-the-text and end-of-line fn-right 38 | scroll up/down fn-arrow-up/down 39 | 40 | Simple usage: transfer this file ( h ) to the target filesystem to 41 | the same directory as the file being edited. 42 | To view it ^oh. To switch back to the file being edited window ^w . Back to help ^w and so on. 43 | Edit this file ( h ), with pye to customize it. 44 | -------------------------------------------------------------------------------- /lcd_io.md: -------------------------------------------------------------------------------- 1 | ## lcd_io usage 2 | 3 | This front-end includes code to allow the pye editor to run on a CircuitPython board with an attached LCD display and accepting text input via UART. This version is called `pye_lcd.py`. (Note: By using the main pye.py file, you can separately select only LCD display or only UART input by properly selecting the high level Boolean variables: `direct_lcd_io` and `uart_input`.) 4 | 5 | This branch relies on the simpleTerminal and editorTerminal class found at [https://github.com/kmatch98/simpleTerminal](https://github.com/kmatch98/simpleTerminal) 6 | 7 | To setup the display for your requirements, review and update the `init_display` function as follows: 8 | 9 | 1. Update the pin connections. 10 | 2. Set up the SPI bus. 11 | 3. Select the correct display library and set the appropriate display settings. 12 | 13 | The following sections describes the steps to update the code to match your display requirements: 14 | 15 | 1. Update the pin connections to match your chip's wiring. 16 | 17 | ```python 18 | spi = board.SPI() 19 | tft_cs = board.D12 # arbitrary, pin not used for my display 20 | tft_dc = board.D2 21 | tft_backlight = board.D4 22 | tft_reset=board.D3 23 | ``` 24 | 25 | This is the meaning of each of these four connections for the SPI bus: 26 | 27 | |Pin Name in code |Connection| 28 | |:---|:---| 29 | |`spi`| `board.SPI()` selects clock, MOSI and MISO for this board 30 | |`tft_cs`|Chip Select| 31 | |`tft.dc`| Data/Command pin| 32 | |`tft_backlight`| Backlight PWM control| 33 | |`tft_reset`| Reset| 34 | 35 | 2. Set up the SPI bus. 36 | 37 | Edit the `displayio.FourWire` parameters to correspond to the requirements of your display. For example set the `baudrate`, `polarity`, and `phase` to correspond to the requirements of your display. 38 | 39 | ```python 40 | display_bus = displayio.FourWire( 41 | spi, 42 | command=tft_dc, 43 | chip_select=tft_cs, 44 | reset=tft_reset, 45 | baudrate=24000000, 46 | polarity=1, 47 | phase=1, 48 | ) 49 | ``` 50 | ***Example:*** In this example, this display uses SPI\_Mode3, requiring `polarity=1` and `phase=1`. 51 | If using a display requiring SPI_Mode1, then set `polarity=0` and `phase=1`. More details can be found in this [SPI mode table on Wikipedia] (https://en.wikipedia.org/wiki/Serial_Peripheral_Interface#Mode_numbers). 52 | 53 | 3. Select the correct display library and set the appropriate display settings. 54 | 55 | Select the correct display library and display settings in following lines: 56 | 57 | ```python 58 | Editor.xPixels=240 # number of xPixels for the display 59 | Editor.yPixels=240 # number of yPixels for the display 60 | 61 | Editor.display = ST7789(display_bus, 62 | width=Editor.xPixels, 63 | height=Editor.yPixels, 64 | rotation=0, 65 | rowstart=80, 66 | colstart=0) 67 | 68 | ``` 69 | 70 | Edit the `Editor.xPixels=240` and `Editor.yPixels=240` to correspond to the pixel dimensions of your display. 71 | 72 | Update the following parameters as required for your display: `rotation=0`, `rowstart=80` and `colstart=0`. 73 | 74 | 75 | ***Example:*** This example shows a 240x240 pixel display using an ST7789 display controller chip. Because this chip can handle up to 320x240 pixel display, so to accommodate our 240x240 display size, we utilize an 80 pixel offset using the `rowstart` parameter. Your display library is likely is different than this, and may not require any offset. 76 | 77 | ## Using UART as an input 78 | Double-check that the settings in the `init_tty` function match your UART hardware connections, especially for `board.TX`, `board.RX` and `baudrate`: 79 | 80 | ```python 81 | Editor.uart = busio.UART(board.TX, 82 | board.RX, 83 | baudrate=115200, 84 | timeout=0.1, 85 | receiver_buffer_size=64) 86 | ``` 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | ["pye.mpy", "github:robert-hh/Micropython-Editor/pye.mpy"], 4 | ["pye.py", "github:robert-hh/Micropython-Editor/pye.py"] 5 | ], 6 | "version": "2.78" 7 | } 8 | -------------------------------------------------------------------------------- /peteensy.py: -------------------------------------------------------------------------------- 1 | # 2 | # Front-end for Teensy 3.5 and 3.6 3 | # 4 | import sys 5 | 6 | class IO_DEVICE: 7 | def __init__(self): 8 | try: 9 | from micropython import kbd_intr 10 | kbd_intr(-1) 11 | except ImportError: 12 | pass 13 | if hasattr(sys.stdin, "buffer"): 14 | self.rd_raw_fct = sys.stdin.buffer.read 15 | else: 16 | self.rd_raw_fct = sys.stdin.read 17 | 18 | def wr(self, s): 19 | sys.stdout.write(s) 20 | 21 | def rd(self): 22 | return sys.stdin.read(1) 23 | 24 | def rd_raw(self): 25 | return self.rd_raw_fct(1) 26 | 27 | def deinit_tty(self): 28 | try: 29 | from micropython import kbd_intr 30 | kbd_intr(3) 31 | except ImportError: 32 | pass 33 | 34 | def get_screen_size(self): 35 | self.wr('\x1b[999;999H\x1b[6n') 36 | pos = '' 37 | char = self.rd() ## expect ESC[yyy;xxxR 38 | while char != 'R': 39 | pos += char 40 | char = self.rd() 41 | return [int(i, 10) for i in pos.lstrip("\n\x1b[").split(';')] 42 | 43 | ## test, if the Editor class is already present 44 | if "pye_edit" not in globals().keys(): 45 | from pye import pye_edit 46 | 47 | def pye(*args, tab_size=4, undo=50): 48 | from pyb import USB_VCP 49 | USB_VCP().setinterrupt(-1) 50 | io_device = IO_DEVICE() 51 | ret = pye_edit(*args, tab_size=tab_size, undo=undo, io_device=io_device) 52 | io_device.deinit_tty() 53 | USB_VCP().setinterrupt(3) 54 | return ret 55 | 56 | -------------------------------------------------------------------------------- /pye.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hh/Micropython-Editor/ed544c7a984be6db3d88c1682924409bae339da1/pye.mpy -------------------------------------------------------------------------------- /pye.py: -------------------------------------------------------------------------------- 1 | PYE_VERSION = " V2.78 " 2 | try: 3 | import usys as sys 4 | except: 5 | import sys 6 | import gc 7 | if sys.implementation.name == "micropython": 8 | is_micropython = True 9 | import uos as os 10 | from uio import StringIO 11 | elif sys.implementation.name == "circuitpython": 12 | is_micropython = True 13 | import os 14 | from io import StringIO 15 | else: 16 | is_micropython = False 17 | def const(x): 18 | return x 19 | import os 20 | from _io import StringIO 21 | from re import compile as re_compile 22 | import time 23 | KEY_NONE = const(0x00) 24 | KEY_UP = const(0x0B) 25 | KEY_DOWN = const(0x0D) 26 | KEY_LEFT = const(0x1F) 27 | KEY_RIGHT = const(0x1E) 28 | KEY_HOME = const(0x10) 29 | KEY_END = const(0x03) 30 | KEY_PGUP = const(0xFFF1) 31 | KEY_PGDN = const(0xFFF2) 32 | KEY_WORD_LEFT = const(0xFFF3) 33 | KEY_WORD_RIGHT = const(0xFFF4) 34 | KEY_SHIFT_UP = const(0xFFF5) 35 | KEY_ALT_UP = const(0xFFEA) 36 | KEY_SHIFT_DOWN = const(0xFFF6) 37 | KEY_ALT_DOWN = const(0xFFEB) 38 | KEY_SHIFT_LEFT = const(0xFFF0) 39 | KEY_ALT_LEFT = const(0xFFE9) 40 | KEY_SHIFT_CTRL_LEFT = const(0xFFED) 41 | KEY_SHIFT_RIGHT = const(0xFFEF) 42 | KEY_ALT_RIGHT = const(0xFFE8) 43 | KEY_SHIFT_CTRL_RIGHT = const(0xFFEC) 44 | KEY_QUIT = const(0x11) 45 | KEY_FORCE_QUIT = const(0xFFE6) 46 | KEY_ENTER = const(0x0A) 47 | KEY_BACKSPACE = const(0x08) 48 | KEY_DELETE = const(0x7F) 49 | KEY_DEL_WORD = const(0xFFF7) 50 | KEY_DEL_LINE = const(0xFFE7) 51 | KEY_WRITE = const(0x13) 52 | KEY_TAB = const(0x09) 53 | KEY_BACKTAB = const(0x15) 54 | KEY_FIND = const(0x06) 55 | KEY_GOTO = const(0x07) 56 | KEY_MOUSE = const(0x1B) 57 | KEY_SCRLUP = const(0x1C) 58 | KEY_SCRLDN = const(0x1D) 59 | KEY_FIND_AGAIN = const(0x0E) 60 | KEY_REDRAW = const(0x05) 61 | KEY_UNDO = const(0x1A) 62 | KEY_REDO = const(0xFFEE) 63 | KEY_CUT = const(0x18) 64 | KEY_PASTE = const(0x16) 65 | KEY_COPY = const(0x04) 66 | KEY_FIRST = const(0x14) 67 | KEY_LAST = const(0x02) 68 | KEY_REPLC = const(0x12) 69 | KEY_TOGGLE = const(0x01) 70 | KEY_GET = const(0x0F) 71 | KEY_MARK = const(0x0C) 72 | KEY_NEXT = const(0x17) 73 | KEY_PREV = const(0xFFE5) 74 | KEY_COMMENT = const(0xFFFC) 75 | KEY_MATCH = const(0xFFFD) 76 | KEY_INDENT = const(0xFFFE) 77 | KEY_DEDENT = const(0xFFFF) 78 | KEY_PLACE = const(0xFFE4) 79 | KEY_NEXT_PLACE = const(0xFFE3) 80 | KEY_PREV_PLACE = const(0xFFE2) 81 | KEY_UNDO_PREV = const(0xFFE1) 82 | KEY_UNDO_NEXT = const(0xFFE0) 83 | KEY_UNDO_YANK = const(0xFFDF) 84 | class Editor: 85 | KEYMAP = { 86 | "\x1b[A": KEY_UP, 87 | "\x1b[1;2A": KEY_SHIFT_UP, 88 | "\x1b[1;3A": KEY_ALT_UP, 89 | "\x1b[B": KEY_DOWN, 90 | "\x1b[1;2B": KEY_SHIFT_DOWN, 91 | "\x1b[1;3B": KEY_ALT_DOWN, 92 | "\x1b[D": KEY_LEFT, 93 | "\x1b[1;2D": KEY_SHIFT_LEFT, 94 | "\x1b[1;6D": KEY_SHIFT_CTRL_LEFT, 95 | "\x1b[1;3D": KEY_ALT_LEFT, 96 | "\x1b[C": KEY_RIGHT, 97 | "\x1b[1;2C": KEY_SHIFT_RIGHT, 98 | "\x1b[1;6C": KEY_SHIFT_CTRL_RIGHT, 99 | "\x1b[1;3C": KEY_ALT_RIGHT, 100 | "\x1b[H": KEY_HOME, 101 | "\x1bOH": KEY_HOME, 102 | "\x1b[1~": KEY_HOME, 103 | "\x1b[F": KEY_END, 104 | "\x1bOF": KEY_END, 105 | "\x1b[4~": KEY_END, 106 | "\x1b[5~": KEY_PGUP, 107 | "\x1b[6~": KEY_PGDN, 108 | "\x1b[5;5~": KEY_PREV, 109 | "\x1b[6;5~": KEY_NEXT, 110 | "\x1b[1;5D": KEY_WORD_LEFT, 111 | "\x1b[1;5C": KEY_WORD_RIGHT, 112 | "\x03": KEY_COPY, 113 | "\r": KEY_ENTER, 114 | "\x7f": KEY_BACKSPACE, 115 | "\x1b[3~": KEY_DELETE, 116 | "\x1b[Z": KEY_BACKTAB, 117 | "\x19": KEY_REDO, 118 | "\x08": KEY_REPLC, 119 | "\x12": KEY_REPLC, 120 | "\x11": KEY_QUIT, 121 | "\x1b": KEY_QUIT, 122 | "\n": KEY_ENTER, 123 | "\x13": KEY_WRITE, 124 | "\x06": KEY_FIND, 125 | "\x0e": KEY_FIND_AGAIN, 126 | "\x07": KEY_GOTO, 127 | "\x05": KEY_REDRAW, 128 | "\x1a": KEY_UNDO, 129 | "\x09": KEY_TAB, 130 | "\x15": KEY_BACKTAB, 131 | "\x18": KEY_CUT, 132 | "\x16": KEY_PASTE, 133 | "\x04": KEY_UNDO_YANK, 134 | "\x0c": KEY_MARK, 135 | "\x00": KEY_MARK, 136 | "\x14": KEY_FIRST, 137 | "\x02": KEY_LAST, 138 | "\x01": KEY_TOGGLE, 139 | "\x17": KEY_NEXT, 140 | "\x0f": KEY_GET, 141 | "\x10": KEY_COMMENT, 142 | "\x1f": KEY_COMMENT, 143 | "\x1b[1;5A": KEY_SCRLUP, 144 | "\x1b[1;5B": KEY_SCRLDN, 145 | "\x1b[1;5H": KEY_FIRST, 146 | "\x1b[1;5F": KEY_LAST, 147 | "\x1b[3;5~": KEY_DEL_WORD, 148 | "\x1b[3;2~": KEY_DEL_LINE, 149 | "\x0b": KEY_MATCH, 150 | "\x1b[M": KEY_MOUSE, 151 | "\x1b[2;3~": KEY_PLACE, 152 | "\x1b[5;3~": KEY_PREV_PLACE, 153 | "\x1b[6;3~": KEY_NEXT_PLACE, 154 | "\x1b[1;3H": KEY_UNDO_PREV, 155 | "\x1b[1;3F": KEY_UNDO_NEXT, 156 | } 157 | TERMCMD = [ 158 | "\x1b[{row};{col}H", 159 | "\x1b[0K", 160 | "\x1b[?25h", 161 | "\x1b[?25l", 162 | "\x1b[0m", 163 | "\x1b[1;37;44m", 164 | "\x1b[43m", 165 | "\x1b[?9h", 166 | "\x1b[?9l", 167 | "\x1bM", 168 | "\n", 169 | "\x1b[1;{stop}r", 170 | "\x1b[r", 171 | "\b", 172 | "{chd}{file} Row: {row}/{total} Col: {col} {msg}", 173 | "{chd}{file} {row}:{col} {msg}", 174 | ] 175 | yank_buffer = [] 176 | find_pattern = "" 177 | case = "n" 178 | autoindent = "y" 179 | replc_pattern = "" 180 | comment_char = "\x23 " 181 | word_char = "_\\" 182 | file_char = "_.-" 183 | match_span = 50 184 | place_list = [] 185 | place_index = 0 186 | max_places = 20 187 | def __init__(self, tab_size, undo_limit, io_device): 188 | self.top_line = self.cur_line = self.row = self.vcol = self.col = self.margin = 0 189 | self.tab_size = tab_size 190 | self.changed = "" 191 | self.hash = 0 192 | self.message = self.fname = "" 193 | self.content = [""] 194 | self.undo = [] 195 | self.undo_limit = undo_limit 196 | self.undo_index = 0 197 | self.redo = [] 198 | self.clear_mark() 199 | self.write_tabs = "n" 200 | self.work_dir = os.getcwd() 201 | self.io_device = io_device 202 | self.wr = io_device.wr 203 | self.is_dir = False 204 | self.key_max = 0 205 | for _ in Editor.KEYMAP.keys(): 206 | self.key_max = max(self.key_max, len(_)) 207 | def goto(self, row, col): 208 | self.wr(Editor.TERMCMD[0].format(row=row + 1, col=col + 1)) 209 | def clear_to_eol(self): 210 | self.wr(Editor.TERMCMD[1]) 211 | def cursor(self, onoff): 212 | self.wr(Editor.TERMCMD[2] if onoff else Editor.TERMCMD[3]) 213 | def hilite(self, mode): 214 | if mode == 1: 215 | self.wr(Editor.TERMCMD[5]) 216 | elif mode == 2: 217 | self.wr(Editor.TERMCMD[6]) 218 | else: 219 | self.wr(Editor.TERMCMD[4]) 220 | def mouse_reporting(self, onoff): 221 | self.wr( 222 | Editor.TERMCMD[7] if onoff else Editor.TERMCMD[8] 223 | ) 224 | def scroll_region(self, stop): 225 | self.wr( 226 | Editor.TERMCMD[11].format(stop=stop) if stop else Editor.TERMCMD[12] 227 | ) 228 | def scroll_up(self, scrolling): 229 | if Editor.TERMCMD[9]: 230 | Editor.scrbuf[scrolling:] = Editor.scrbuf[:-scrolling] 231 | Editor.scrbuf[:scrolling] = [""] * scrolling 232 | self.goto(0, 0) 233 | self.wr(Editor.TERMCMD[9] * scrolling) 234 | def scroll_down(self, scrolling): 235 | if Editor.TERMCMD[10]: 236 | Editor.scrbuf[:-scrolling] = Editor.scrbuf[scrolling:] 237 | Editor.scrbuf[-scrolling:] = [""] * scrolling 238 | self.goto(Editor.height - 1, 0) 239 | self.wr(Editor.TERMCMD[10] * scrolling) 240 | def redraw(self, flag): 241 | self.cursor(False) 242 | Editor.height, Editor.width = self.io_device.get_screen_size() 243 | Editor.height -= 1 244 | Editor.scrbuf = [(False, "\x00")] * Editor.height 245 | self.row = min(Editor.height - 1, self.row) 246 | self.scroll_region(Editor.height) 247 | self.mouse_reporting(True) 248 | if flag: 249 | self.message = PYE_VERSION 250 | if is_micropython: 251 | gc.collect() 252 | if flag: 253 | self.message += "{} Bytes Memory available".format(gc.mem_free()) 254 | self.changed = "" if self.hash == self.hash_buffer() else "*" 255 | def get_input( 256 | self, 257 | ): 258 | while True: 259 | in_buffer = self.io_device.rd() 260 | if in_buffer[0] == "\x1b": 261 | while True: 262 | in_buffer += self.io_device.rd() 263 | c = in_buffer[-1] 264 | if c == "~" or (c.isalpha() and len(in_buffer) > 2): 265 | break 266 | if len(in_buffer) == 2 and c.isalpha() and c != "O": 267 | in_buffer = chr(ord(in_buffer[1]) & 0x1F) 268 | break 269 | if len(in_buffer) >= self.key_max: 270 | break 271 | if in_buffer == "\x1b\x1b": 272 | in_buffer = "\x1b" 273 | break 274 | if in_buffer in Editor.KEYMAP: 275 | c = Editor.KEYMAP[in_buffer] 276 | if c != KEY_MOUSE: 277 | return c, None 278 | else: 279 | mouse_fct = ord(self.io_device.rd_raw()) 280 | mouse_x = ord(self.io_device.rd_raw()) - 33 281 | mouse_y = ord(self.io_device.rd_raw()) - 33 282 | if mouse_fct == 0x61: 283 | return KEY_SCRLDN, 3 284 | elif mouse_fct == 0x60: 285 | return KEY_SCRLUP, 3 286 | else: 287 | return KEY_MOUSE, [mouse_x, mouse_y, mouse_fct] 288 | elif ord(in_buffer[0]) >= 32: 289 | return KEY_NONE, in_buffer 290 | def display_window(self): 291 | self.cur_line = min(self.total_lines - 1, max(self.cur_line, 0)) 292 | self.vcol = max(0, min(self.col, len(self.content[self.cur_line]))) 293 | if self.vcol >= Editor.width + self.margin: 294 | self.margin = self.vcol - Editor.width + (Editor.width >> 2) 295 | elif self.vcol < self.margin: 296 | self.margin = max(self.vcol - (Editor.width >> 2), 0) 297 | if not (self.top_line <= self.cur_line < self.top_line + Editor.height): 298 | self.top_line = max(self.cur_line - self.row, 0) 299 | self.row = self.cur_line - self.top_line 300 | self.cursor(False) 301 | line = self.top_line 302 | if self.mark is None: 303 | flag = 0 304 | else: 305 | start_line, start_col, end_line, end_col = self.mark_range() 306 | start_col = max(start_col - self.margin, 0) 307 | end_col = max(end_col - self.margin, 0) 308 | for c in range(Editor.height): 309 | if line == self.total_lines: 310 | if Editor.scrbuf[c] != (False, ""): 311 | self.goto(c, 0) 312 | self.clear_to_eol() 313 | Editor.scrbuf[c] = (False, "") 314 | else: 315 | if self.mark is not None: 316 | flag = ( 317 | (start_line <= line < end_line) 318 | + ((start_line == line) << 1) 319 | + (((end_line - 1) == line) << 2) 320 | ) 321 | l = (flag, self.content[line][self.margin : self.margin + Editor.width]) 322 | if (flag and line == self.cur_line) or l != Editor.scrbuf[ 323 | c 324 | ]: 325 | self.goto(c, 0) 326 | if flag == 0: 327 | self.wr(l[1]) 328 | elif flag == 7: 329 | self.wr(l[1][:start_col]) 330 | self.hilite(2) 331 | self.wr(l[1][start_col:end_col]) 332 | self.hilite(0) 333 | self.wr(l[1][end_col:]) 334 | elif flag == 3: 335 | self.wr(l[1][:start_col]) 336 | self.hilite(2) 337 | self.wr(l[1][start_col:]) 338 | self.wr(" ") 339 | self.hilite(0) 340 | elif flag == 5: 341 | self.hilite(2) 342 | self.wr(l[1][:end_col]) 343 | self.hilite(0) 344 | self.wr(l[1][end_col:]) 345 | else: 346 | self.hilite(2) 347 | self.wr(l[1]) 348 | self.wr(" ") 349 | self.hilite(0) 350 | if len(l[1]) < Editor.width: 351 | self.clear_to_eol() 352 | Editor.scrbuf[c] = l 353 | line += 1 354 | self.goto(Editor.height, 0) 355 | self.hilite(1) 356 | self.wr( 357 | Editor.TERMCMD[14 if Editor.width > 40 else 15].format( 358 | chd=self.changed, 359 | file=self.fname, 360 | row=self.cur_line + 1, 361 | total=self.total_lines, 362 | col=self.vcol + 1, 363 | msg=self.message, 364 | )[: self.width - 1] 365 | ) 366 | self.clear_to_eol() 367 | self.hilite(0) 368 | self.goto(self.row, self.vcol - self.margin) 369 | self.cursor(True) 370 | def spaces(self, line, pos=None): 371 | return ( 372 | len(line) - len(line.lstrip(" ")) 373 | if pos is None 374 | else len(line[:pos]) - len(line[:pos].rstrip(" ")) 375 | ) 376 | def mark_range(self): 377 | if self.mark_order(self.cur_line, self.col) >= 0: 378 | return (self.mark[0], self.mark[1], self.cur_line + 1, self.col) 379 | else: 380 | return (self.cur_line, self.col, self.mark[0] + 1, self.mark[1]) 381 | def mark_order(self, line, col): 382 | return col - self.mark[1] if self.mark[0] == line else line - self.mark[0] 383 | def line_range(self): 384 | res = self.mark_range() 385 | return (res[0], res[2]) if res[3] > 0 else (res[0], res[2] - 1) 386 | def line_edit( 387 | self, prompt, default, zap=None 388 | ): 389 | def push_msg(msg): 390 | self.wr(msg + Editor.TERMCMD[13] * len(msg)) 391 | self.goto(Editor.height, 0) 392 | self.hilite(1) 393 | self.wr(prompt) 394 | self.wr(default) 395 | self.clear_to_eol() 396 | res = default 397 | pos = len(res) 398 | del_all = True 399 | mouse_last = None 400 | while True: 401 | key, char = self.get_input() 402 | if key == KEY_NONE: 403 | if len(prompt) + len(res) < self.width - 2: 404 | res = res[:pos] + char + res[pos:] 405 | self.wr(res[pos]) 406 | pos += len(char) 407 | push_msg(res[pos:]) 408 | elif key in (KEY_ENTER, KEY_TAB): 409 | self.hilite(0) 410 | return res 411 | elif key in (KEY_QUIT, KEY_COPY): 412 | self.hilite(0) 413 | return None 414 | elif key == KEY_LEFT: 415 | if pos > 0: 416 | self.wr(Editor.TERMCMD[13]) 417 | pos -= 1 418 | elif key == KEY_RIGHT: 419 | if pos < len(res): 420 | self.wr(res[pos]) 421 | pos += 1 422 | elif key == KEY_HOME: 423 | self.wr(Editor.TERMCMD[13] * pos) 424 | pos = 0 425 | elif key == KEY_END: 426 | self.wr(res[pos:]) 427 | pos = len(res) 428 | elif key == KEY_DELETE: 429 | if del_all: 430 | self.wr(Editor.TERMCMD[13] * pos) 431 | self.wr(" " * len(res)) 432 | self.wr(Editor.TERMCMD[13] * len(res)) 433 | pos = 0 434 | res = "" 435 | else: 436 | if pos < len(res): 437 | res = res[:pos] + res[pos + 1 :] 438 | push_msg(res[pos:] + " ") 439 | elif key == KEY_BACKSPACE: 440 | if pos > 0: 441 | res = res[: pos - 1] + res[pos:] 442 | self.wr(Editor.TERMCMD[13]) 443 | pos -= 1 444 | push_msg(res[pos:] + " ") 445 | elif key == KEY_PASTE: 446 | res += self.getsymbol(self.content[self.cur_line], self.col, zap)[ 447 | : Editor.width - pos - len(prompt) - 1 448 | ] 449 | push_msg(res[pos:]) 450 | elif key == KEY_MOUSE: 451 | if char[1] < Editor.height and (char[1] + self.top_line) < self.total_lines: 452 | self.col = char[0] + self.margin 453 | self.cur_line = char[1] + self.top_line 454 | if (self.col, self.cur_line) != mouse_last: 455 | mouse_last = (self.col, self.cur_line) 456 | self.wr(Editor.TERMCMD[13] * pos) 457 | self.wr(" " * len(res)) 458 | self.wr(Editor.TERMCMD[13] * len(res)) 459 | pos = 0 460 | res = self.getsymbol(self.content[self.cur_line], self.col, zap) 461 | push_msg(res) 462 | else: 463 | self.hilite(0) 464 | return res 465 | del_all = False 466 | def getsymbol(self, s, pos, zap): 467 | if pos < len(s) and zap is not None: 468 | start = self.skip_while(s, pos, zap, -1) 469 | stop = self.skip_while(s, pos, zap, 1) 470 | return s[start + 1 : stop] 471 | else: 472 | return "" 473 | def issymbol(self, c, zap): 474 | return c.isalpha() or c.isdigit() or c in zap 475 | def skip_until(self, s, pos, zap, way): 476 | stop = -1 if way < 0 else len(s) 477 | while pos != stop and not self.issymbol(s[pos], zap): 478 | pos += way 479 | return pos 480 | def skip_while(self, s, pos, zap, way): 481 | stop = -1 if way < 0 else len(s) 482 | while pos != stop and self.issymbol(s[pos], zap): 483 | pos += way 484 | return pos 485 | def move_up(self): 486 | if self.cur_line > 0: 487 | self.cur_line -= 1 488 | if self.cur_line < self.top_line: 489 | self.scroll_up(1) 490 | def skip_up(self): 491 | if self.col == 0 and self.cur_line > 0: 492 | self.col = len(self.content[self.cur_line - 1]) 493 | self.move_up() 494 | return True 495 | else: 496 | return False 497 | def move_left(self): 498 | self.col = self.vcol 499 | if not self.skip_up(): 500 | self.col -= 1 501 | def move_down(self): 502 | if self.cur_line < self.total_lines - 1: 503 | self.cur_line += 1 504 | if self.cur_line == self.top_line + Editor.height: 505 | self.scroll_down(1) 506 | def skip_down(self, l): 507 | if self.col >= len(l) and self.cur_line < self.total_lines - 1: 508 | self.col = 0 509 | self.move_down() 510 | return True 511 | else: 512 | return False 513 | def move_right(self, l): 514 | if not self.skip_down(l): 515 | self.col += 1 516 | def find_in_file(self, pattern, col, end): 517 | Editor.find_pattern = pattern 518 | if Editor.case != "y": 519 | pattern = pattern.lower() 520 | try: 521 | rex = re_compile(pattern) 522 | except: 523 | self.message = "Invalid pattern: " + pattern 524 | return None 525 | start = self.cur_line 526 | if col > len(self.content[start]) or ( 527 | pattern[0] == "^" and col != 0 528 | ): 529 | start, col = start + 1, 0 530 | for line in range(start, end): 531 | l = self.content[line][col:] 532 | if Editor.case != "y": 533 | l = l.lower() 534 | match = rex.search(l) 535 | if match: 536 | self.cur_line = line 537 | if pattern[-1:] == "$" and match.group(0)[-1:] != "$": 538 | self.col = col + len(l) - len(match.group(0)) 539 | else: 540 | self.col = col + l.find(match.group(0)) 541 | return len(match.group(0)) 542 | col = 0 543 | else: 544 | self.message = Editor.find_pattern + " not found (again)" 545 | return None 546 | def undo_add(self, lnum, text, key, span=1, chain=False): 547 | if ( 548 | len(self.undo) == 0 549 | or key == KEY_NONE 550 | or self.undo[-1][3] != key 551 | or self.undo[-1][0] != lnum 552 | ): 553 | self.changed = "*" 554 | if len(self.undo) >= self.undo_limit: 555 | del self.undo[0] 556 | self.undo.append([lnum, span, text, key, self.col, chain]) 557 | self.redo = [] 558 | def undo_redo(self, undo, redo): 559 | chain = True 560 | redo_start = len(redo) 561 | while len(undo) > 0 and chain: 562 | action = undo.pop() 563 | if action[3] not in (KEY_INDENT, KEY_DEDENT, KEY_COMMENT): 564 | self.cur_line = action[0] 565 | self.col = action[4] 566 | if len(redo) >= self.undo_limit: 567 | del redo[0] 568 | if action[1] >= 0: 569 | redo.append( 570 | action[0:1] 571 | + [len(action[2])] 572 | + [self.content[action[0] : action[0] + action[1]]] 573 | + action[3:] 574 | ) 575 | if action[0] < self.total_lines: 576 | self.content[action[0] : action[0] + action[1]] = action[2] 577 | else: 578 | self.content += action[2] 579 | else: 580 | redo.append( 581 | action[0:1] 582 | + [1] 583 | + [ 584 | self.content[action[0] : action[0] - action[1] + 1] 585 | ] 586 | + action[3:] 587 | ) 588 | del self.content[action[0] : action[0] - action[1]] 589 | self.content[action[0]] = action[2][0] 590 | chain = action[5] 591 | if (len(redo) - redo_start) > 0: 592 | redo[-1][5] = True 593 | redo[redo_start][5] = False 594 | self.total_lines = len(self.content) 595 | self.changed = "" if self.hash == self.hash_buffer() else "*" 596 | self.clear_mark() 597 | def set_mark(self, flag=999999999): 598 | if self.mark is None: 599 | self.mark = (self.cur_line, self.col) 600 | if self.mark_flag < flag: 601 | self.mark_flag = flag 602 | def check_mark(self): 603 | if self.mark is not None: 604 | self.mark_flag -= 1 605 | if self.mark_flag <= 0: 606 | self.clear_mark() 607 | def clear_mark(self): 608 | self.mark = None 609 | self.mark_flag = 0 610 | self.mouse_last = (0, 0, 0) 611 | def yank_mark(self): 612 | start_row, start_col, end_row, end_col = self.mark_range() 613 | Editor.yank_buffer = self.content[start_row:end_row] 614 | Editor.yank_buffer[-1] = Editor.yank_buffer[-1][:end_col] 615 | Editor.yank_buffer[0] = Editor.yank_buffer[0][start_col:] 616 | def delete_mark(self, yank): 617 | if yank: 618 | self.yank_mark() 619 | start_row, start_col, end_row, end_col = self.mark_range() 620 | self.undo_add(start_row, self.content[start_row:end_row], KEY_NONE, 1, False) 621 | self.content[start_row] = ( 622 | self.content[start_row][:start_col] + self.content[end_row - 1][end_col:] 623 | ) 624 | if start_row + 1 < end_row: 625 | del self.content[start_row + 1 : end_row] 626 | self.col = start_col 627 | if self.content == []: 628 | self.content = [""] 629 | self.undo[-1][1] = 1 630 | self.total_lines = len(self.content) 631 | self.cur_line = start_row 632 | self.clear_mark() 633 | def handle_edit_keys(self, key, char): 634 | l = self.content[self.cur_line] 635 | if key == KEY_NONE: 636 | self.col = self.vcol 637 | if self.mark is not None: 638 | self.delete_mark(False) 639 | l = self.content[self.cur_line] 640 | chain = True 641 | else: 642 | chain = False 643 | self.undo_add(self.cur_line, [l], 0x20 if char == " " else 0x41, 1, chain) 644 | self.content[self.cur_line] = l[: self.col] + char + l[self.col :] 645 | self.col += len(char) 646 | return key 647 | elif key == KEY_SHIFT_CTRL_LEFT: 648 | self.set_mark() 649 | key = KEY_WORD_LEFT 650 | elif key == KEY_SHIFT_CTRL_RIGHT: 651 | self.set_mark() 652 | key = KEY_WORD_RIGHT 653 | elif key == KEY_MOUSE: 654 | if char[2] == 0x22: 655 | key = KEY_GET if self.is_dir else KEY_FIND 656 | elif char[1] < Editor.height: 657 | col = char[0] + self.margin 658 | line = char[1] + self.top_line 659 | if (col, line) == self.mouse_last[:2] and (time.time() - self.mouse_last[2]) < 2: 660 | self.mouse_last = (0, 0, 0) 661 | if ( 662 | self.mark is None 663 | and col < len(l) 664 | and self.issymbol(l[col], Editor.word_char) 665 | ): 666 | self.col = self.skip_while(l, col, Editor.word_char, -1) + 1 667 | self.set_mark() 668 | self.col = self.skip_while(l, self.col, Editor.word_char, 1) 669 | else: 670 | key = KEY_MARK 671 | else: 672 | if self.mark is not None: 673 | if ( 674 | self.mark_order(self.cur_line, self.col) * self.mark_order(line, col) 675 | < 0 676 | ): 677 | self.mark = self.cur_line, self.col 678 | self.cur_line, self.col = line, col 679 | self.mouse_last = (col, line, time.time()) 680 | if key == KEY_DOWN: 681 | self.move_down() 682 | elif key == KEY_UP: 683 | self.move_up() 684 | elif key == KEY_LEFT: 685 | self.move_left() 686 | elif key == KEY_RIGHT: 687 | self.move_right(l) 688 | elif key == KEY_WORD_LEFT: 689 | self.col = self.vcol 690 | if self.skip_up(): 691 | l = self.content[self.cur_line] 692 | pos = self.skip_until(l, self.col - 1, Editor.word_char, -1) 693 | self.col = self.skip_while(l, pos, Editor.word_char, -1) + 1 694 | elif key == KEY_WORD_RIGHT: 695 | if self.skip_down(l): 696 | l = self.content[self.cur_line] 697 | pos = self.skip_until(l, self.col, Editor.word_char, 1) 698 | self.col = self.skip_while(l, pos, Editor.word_char, 1) 699 | elif key == KEY_DELETE: 700 | self.col = self.vcol 701 | if self.mark is not None: 702 | self.delete_mark(False) 703 | elif self.col < len(l): 704 | self.undo_add(self.cur_line, [l], KEY_DELETE) 705 | self.content[self.cur_line] = l[: self.col] + l[self.col + 1 :] 706 | elif (self.cur_line + 1) < self.total_lines: 707 | self.undo_add(self.cur_line, [l, self.content[self.cur_line + 1]], KEY_NONE) 708 | self.content[self.cur_line] = l + ( 709 | self.content.pop(self.cur_line + 1).lstrip() 710 | if Editor.autoindent == "y" and self.col > 0 711 | else self.content.pop(self.cur_line + 1) 712 | ) 713 | self.total_lines -= 1 714 | elif key == KEY_BACKSPACE: 715 | self.col = self.vcol 716 | if self.mark is not None: 717 | self.delete_mark(False) 718 | elif self.col > 0: 719 | self.undo_add(self.cur_line, [l], KEY_BACKSPACE) 720 | self.content[self.cur_line] = l[: self.col - 1] + l[self.col :] 721 | self.col -= 1 722 | elif self.cur_line > 0: 723 | self.undo_add(self.cur_line - 1, [self.content[self.cur_line - 1], l], KEY_NONE) 724 | self.col = len(self.content[self.cur_line - 1]) 725 | self.content[self.cur_line - 1] += self.content.pop(self.cur_line) 726 | self.cur_line -= 1 727 | self.total_lines -= 1 728 | elif key == KEY_DEL_WORD: 729 | if self.col < len(l): 730 | pos = self.skip_while(l, self.col, Editor.word_char, 1) 731 | pos += self.spaces(l[pos:]) 732 | if self.col < pos: 733 | self.undo_add(self.cur_line, [l], KEY_DEL_WORD) 734 | self.content[self.cur_line] = l[: self.col] + l[pos:] 735 | elif key == KEY_DEL_LINE: 736 | if self.cur_line < (self.total_lines - 1): 737 | self.undo_add(self.cur_line, [l, self.content[self.cur_line + 1]], KEY_NONE, 1) 738 | else: 739 | self.undo_add(self.cur_line, [l], KEY_NONE, 1) 740 | self.content.pop(self.cur_line) 741 | if self.content == []: 742 | self.content = [""] 743 | elif key == KEY_HOME: 744 | self.col = self.spaces(l) if self.col == 0 else 0 745 | elif key == KEY_END: 746 | ni = len(l.split(Editor.comment_char.strip())[0].rstrip()) 747 | ns = self.spaces(l) 748 | self.col = ni if self.col >= len(l) and ni > ns else len(l) 749 | elif key == KEY_PGUP: 750 | self.cur_line -= Editor.height 751 | elif key == KEY_PGDN: 752 | self.cur_line += Editor.height 753 | elif key == KEY_FIND: 754 | pat = self.line_edit("Find: ", Editor.find_pattern, "_") 755 | if pat: 756 | self.clear_mark() 757 | self.find_in_file(pat, self.col + 1, self.total_lines) 758 | self.row = Editor.height >> 1 759 | elif key == KEY_FIND_AGAIN: 760 | if Editor.find_pattern: 761 | self.find_in_file(Editor.find_pattern, self.col + 1, self.total_lines) 762 | self.row = Editor.height >> 1 763 | elif key == KEY_GOTO: 764 | line = self.line_edit("Goto Line: ", "") 765 | if line: 766 | self.cur_line = int(line) - 1 767 | self.row = Editor.height >> 1 768 | elif key == KEY_FIRST: 769 | self.check_mark() 770 | self.cur_line = 0 771 | elif key == KEY_LAST: 772 | self.check_mark() 773 | self.cur_line = self.total_lines - 1 774 | self.row = Editor.height - 1 775 | elif key == KEY_TOGGLE: 776 | pat = self.line_edit( 777 | "Autoindent {}, Search Case {}" 778 | ", Tabsize {}, Comment {}, Tabwrite {}: ".format( 779 | Editor.autoindent, 780 | Editor.case, 781 | self.tab_size, 782 | Editor.comment_char, 783 | self.write_tabs, 784 | ), 785 | "", 786 | ) 787 | try: 788 | res = [i.lstrip().lower() for i in pat.split(",")] 789 | if res[0]: 790 | Editor.autoindent = "y" if res[0][0] == "y" else "n" 791 | if res[1]: 792 | Editor.case = "y" if res[1][0] == "y" else "n" 793 | if res[2]: 794 | self.tab_size = int(res[2]) 795 | if res[3]: 796 | Editor.comment_char = res[3] 797 | if res[4]: 798 | self.write_tabs = "y" if res[4][0] == "y" else "n" 799 | except IndexError: 800 | pass 801 | elif key == KEY_SCRLUP: 802 | ni = 1 if char is None else 3 803 | if self.top_line > 0: 804 | self.top_line = max(self.top_line - ni, 0) 805 | self.cur_line = min(self.cur_line, self.top_line + Editor.height - 1) 806 | self.scroll_up(ni) 807 | elif key == KEY_SCRLDN: 808 | ni = 1 if char is None else 3 809 | if self.top_line + Editor.height < self.total_lines: 810 | self.top_line = min(self.top_line + ni, self.total_lines - 1) 811 | self.cur_line = max(self.cur_line, self.top_line) 812 | self.scroll_down(ni) 813 | elif key == KEY_MATCH: 814 | if self.col < len(l): 815 | brackets = "<{[()]}>" 816 | srch = l[self.col] 817 | i = brackets.find(srch) 818 | if i >= 0: 819 | match = brackets[7 - i] 820 | level = 0 821 | way = 1 if i < 4 else -1 822 | i = self.cur_line 823 | c = self.col + way 824 | lstop = ( 825 | min(self.total_lines, i + Editor.match_span) 826 | if way > 0 827 | else max(-1, i - Editor.match_span) 828 | ) 829 | while i != lstop: 830 | l = self.content[i] 831 | cstop = len(l) if way > 0 else -1 832 | if srch in l or match in l: 833 | while c != cstop: 834 | if l[c] == match: 835 | if level == 0: 836 | self.cur_line, self.col = i, c 837 | return key 838 | else: 839 | level -= 1 840 | elif l[c] == srch: 841 | level += 1 842 | c += way 843 | i += way 844 | c = 0 if way > 0 else len(self.content[i]) - 1 845 | self.message = "No match in {} lines".format(abs(lstop - self.cur_line)) 846 | elif key == KEY_MARK: 847 | if self.mark is None: 848 | self.set_mark() 849 | self.move_right(l) 850 | else: 851 | self.clear_mark() 852 | elif key == KEY_SHIFT_DOWN: 853 | self.set_mark() 854 | self.move_down() 855 | elif key == KEY_SHIFT_UP: 856 | self.set_mark() 857 | self.move_up() 858 | elif key == KEY_SHIFT_LEFT: 859 | self.set_mark() 860 | self.move_left() 861 | elif key == KEY_SHIFT_RIGHT: 862 | self.set_mark() 863 | self.move_right(l) 864 | elif key == KEY_ALT_LEFT: 865 | if self.col > 0 and self.col < len(l): 866 | self.undo_add(self.cur_line, [l], KEY_ALT_LEFT) 867 | i = self.col 868 | self.content[self.cur_line] = l[: i - 1] + l[i] + l[i - 1] + l[i + 1 :] 869 | self.move_left() 870 | elif key == KEY_ALT_RIGHT: 871 | if self.col < (len(l) - 1): 872 | self.undo_add(self.cur_line, [l], KEY_ALT_RIGHT) 873 | i = self.col 874 | self.content[self.cur_line] = l[:i] + l[i + 1] + l[i] + l[i + 2 :] 875 | self.move_right(l) 876 | elif key == KEY_ALT_UP: 877 | if self.mark is None: 878 | start_line = self.cur_line 879 | end_line = start_line + 1 880 | else: 881 | start_line, end_line = self.line_range() 882 | if start_line > 0: 883 | self.mark = (self.mark[0] - 1, self.mark[1]) 884 | if start_line > 0: 885 | self.undo_add( 886 | start_line - 1, 887 | self.content[start_line - 1 : end_line], 888 | KEY_NONE, 889 | end_line - start_line + 1, 890 | ) 891 | self.content[start_line - 1 : end_line - 1], self.content[end_line - 1] = ( 892 | self.content[start_line:end_line], 893 | self.content[start_line - 1], 894 | ) 895 | self.move_up() 896 | elif key == KEY_ALT_DOWN: 897 | if self.mark is None: 898 | start_line = self.cur_line 899 | end_line = start_line + 1 900 | else: 901 | start_line, end_line = self.line_range() 902 | if end_line < self.total_lines: 903 | self.mark = (self.mark[0] + 1, self.mark[1]) 904 | if self.cur_line == end_line == (self.total_lines - 1): 905 | self.move_left() 906 | if end_line < self.total_lines: 907 | self.undo_add( 908 | start_line, 909 | self.content[start_line : end_line + 1], 910 | KEY_NONE, 911 | end_line - start_line + 1, 912 | ) 913 | self.content[start_line + 1 : end_line + 1], self.content[start_line] = ( 914 | self.content[start_line:end_line], 915 | self.content[end_line], 916 | ) 917 | self.move_down() 918 | elif key == KEY_ENTER: 919 | self.col = self.vcol 920 | self.clear_mark() 921 | self.undo_add(self.cur_line, [l], KEY_NONE, 2) 922 | self.content[self.cur_line] = l[: self.col] 923 | ni = 0 924 | if Editor.autoindent == "y": 925 | ni = min(self.spaces(l), self.col) 926 | self.cur_line += 1 927 | self.content[self.cur_line : self.cur_line] = [" " * ni + l[self.col :]] 928 | self.total_lines += 1 929 | self.col = ni 930 | elif key == KEY_TAB: 931 | if self.mark is None: 932 | self.col = self.vcol 933 | self.undo_add(self.cur_line, [l], KEY_TAB) 934 | ni = self.tab_size - self.col % self.tab_size 935 | self.content[self.cur_line] = l[: self.col] + " " * ni + l[self.col :] 936 | self.col += ni 937 | else: 938 | lrange = self.line_range() 939 | self.undo_add( 940 | lrange[0], 941 | self.content[lrange[0] : lrange[1]], 942 | KEY_INDENT, 943 | lrange[1] - lrange[0], 944 | ) 945 | for i in range(lrange[0], lrange[1]): 946 | if len(self.content[i]) > 0: 947 | self.content[i] = ( 948 | " " * (self.tab_size - self.spaces(self.content[i]) % self.tab_size) 949 | + self.content[i] 950 | ) 951 | elif key == KEY_BACKTAB: 952 | if self.mark is None: 953 | self.col = self.vcol 954 | ni = min( 955 | (self.col - 1) % self.tab_size + 1, self.spaces(l, self.col) 956 | ) 957 | if ni > 0: 958 | self.undo_add(self.cur_line, [l], KEY_BACKTAB) 959 | self.content[self.cur_line] = l[: self.col - ni] + l[self.col :] 960 | self.col -= ni 961 | else: 962 | lrange = self.line_range() 963 | self.undo_add( 964 | lrange[0], 965 | self.content[lrange[0] : lrange[1]], 966 | KEY_DEDENT, 967 | lrange[1] - lrange[0], 968 | ) 969 | for i in range(lrange[0], lrange[1]): 970 | ns = self.spaces(self.content[i]) 971 | if ns > 0: 972 | self.content[i] = self.content[i][(ns - 1) % self.tab_size + 1 :] 973 | elif key == KEY_REPLC: 974 | count = 0 975 | pat = self.line_edit("Replace: ", Editor.find_pattern, "_") 976 | if pat: 977 | rpat = self.line_edit( 978 | "With: ", Editor.replc_pattern if Editor.replc_pattern else pat 979 | ) 980 | if rpat is not None: 981 | Editor.replc_pattern = rpat 982 | q = "" 983 | cur_line, cur_col = self.cur_line, self.col 984 | if self.mark is not None: 985 | (self.cur_line, self.col, end_line, end_col) = self.mark_range() 986 | else: 987 | end_line = self.total_lines 988 | end_col = 999999 989 | self.message = "Replace (yes/No/all/quit) ? " 990 | chain = False 991 | while True: 992 | ni = self.find_in_file(pat, self.col, end_line) 993 | if ni is not None and ( 994 | self.cur_line != (end_line - 1) or self.col < end_col 995 | ): 996 | if q != "a": 997 | self.display_window() 998 | key, char = self.get_input() 999 | q = char.lower() 1000 | if q == "q" or key == KEY_QUIT: 1001 | break 1002 | elif q in ("a", "y"): 1003 | self.undo_add( 1004 | self.cur_line, 1005 | [self.content[self.cur_line]], 1006 | KEY_NONE, 1007 | 1, 1008 | chain, 1009 | ) 1010 | self.content[self.cur_line] = ( 1011 | self.content[self.cur_line][: self.col] 1012 | + rpat 1013 | + self.content[self.cur_line][self.col + ni :] 1014 | ) 1015 | self.col += len(rpat) + (ni == 0) 1016 | count += 1 1017 | chain = True 1018 | else: 1019 | self.col += 1 1020 | else: 1021 | break 1022 | self.cur_line, self.col = cur_line, cur_col 1023 | self.message = "'{}' replaced {} times".format(pat, count) 1024 | elif key == KEY_CUT: 1025 | if self.mark is not None: 1026 | self.delete_mark(True) 1027 | elif key == KEY_COPY: 1028 | if self.mark is not None: 1029 | self.yank_mark() 1030 | self.clear_mark() 1031 | elif key == KEY_PASTE: 1032 | if Editor.yank_buffer: 1033 | self.col = self.vcol 1034 | if self.mark is not None: 1035 | self.delete_mark(False) 1036 | chain = True 1037 | else: 1038 | chain = False 1039 | head, tail = Editor.yank_buffer[0], Editor.yank_buffer[-1] 1040 | Editor.yank_buffer[0] = ( 1041 | self.content[self.cur_line][: self.col] + Editor.yank_buffer[0] 1042 | ) 1043 | Editor.yank_buffer[-1] += self.content[self.cur_line][self.col :] 1044 | ni = 1 if len(Editor.yank_buffer) <= 1 else 1 - len(Editor.yank_buffer) 1045 | self.undo_add( 1046 | self.cur_line, [self.content[self.cur_line]], KEY_NONE, ni, chain 1047 | ) 1048 | self.content[ 1049 | self.cur_line : self.cur_line + 1 1050 | ] = Editor.yank_buffer 1051 | Editor.yank_buffer[-1], Editor.yank_buffer[0] = tail, head 1052 | self.total_lines = len(self.content) 1053 | elif key == KEY_WRITE: 1054 | fname = self.line_edit( 1055 | "Save File: ", self.fname if self.is_dir is False else "", Editor.file_char 1056 | ) 1057 | if fname: 1058 | if fname != self.fname: 1059 | try: 1060 | open(fname).close() 1061 | res = self.line_edit("The file exists! Overwrite (y/N)? ", "N") 1062 | if not res or res[0].upper() != "Y": 1063 | return 1064 | except: 1065 | pass 1066 | self.put_file(fname) 1067 | self.fname = fname 1068 | self.hash = self.hash_buffer() 1069 | self.changed = "" 1070 | self.is_dir = False 1071 | elif key == KEY_UNDO: 1072 | self.undo_redo(self.undo, self.redo) 1073 | elif key == KEY_REDO: 1074 | self.undo_redo(self.redo, self.undo) 1075 | elif key == KEY_COMMENT: 1076 | if self.mark is None: 1077 | lrange = (self.cur_line, self.cur_line + 1) 1078 | else: 1079 | lrange = self.line_range() 1080 | self.undo_add( 1081 | lrange[0], self.content[lrange[0] : lrange[1]], KEY_COMMENT, lrange[1] - lrange[0] 1082 | ) 1083 | ni = len(Editor.comment_char) 1084 | for i in range(lrange[0], lrange[1]): 1085 | if self.content[i].strip() != "": 1086 | ns = self.spaces(self.content[i]) 1087 | if self.content[i][ns : ns + ni] == Editor.comment_char: 1088 | self.content[i] = ns * " " + self.content[i][ns + ni :] 1089 | else: 1090 | self.content[i] = ns * " " + Editor.comment_char + self.content[i][ns:] 1091 | elif key == KEY_REDRAW: 1092 | self.redraw(True) 1093 | elif key == KEY_PLACE: 1094 | here = (self.cur_line, self) 1095 | if here not in Editor.place_list: 1096 | if len(Editor.place_list) >= Editor.max_places: 1097 | Editor.place_list.pop(0) 1098 | Editor.place_list.append(here) 1099 | Editor.place_index = len(Editor.place_list) - 1 1100 | elif key == KEY_NEXT_PLACE or key == KEY_PREV_PLACE: 1101 | ni = len(Editor.place_list) 1102 | if ni > 0: 1103 | Editor.place_index = ( 1104 | Editor.place_index + (1 if key == KEY_NEXT_PLACE else -1) 1105 | ) % ni 1106 | here = Editor.place_list[Editor.place_index] 1107 | if here[1] == self: 1108 | self.cur_line = here[0] 1109 | self.row = Editor.height >> 1 1110 | else: 1111 | here[1].cur_line = here[0] 1112 | return here[1] 1113 | elif key == KEY_UNDO_PREV or key == KEY_UNDO_NEXT: 1114 | if len(self.undo) > 0: 1115 | self.undo_index = (self.undo_index + (1 if key == KEY_UNDO_NEXT else -1)) % len( 1116 | self.undo 1117 | ) 1118 | self.cur_line = self.undo[self.undo_index][0] 1119 | self.col = self.undo[self.undo_index][4] 1120 | elif key == KEY_UNDO_YANK: 1121 | if len(self.undo) > 0: 1122 | Editor.yank_buffer = self.undo[self.undo_index][2] 1123 | return key 1124 | def edit_loop(self): 1125 | if not self.content: 1126 | self.content = [""] 1127 | self.total_lines = len(self.content) 1128 | os.chdir(self.work_dir) 1129 | self.redraw(self.message == "") 1130 | while True: 1131 | self.display_window() 1132 | key, char = self.get_input() 1133 | self.message = "" 1134 | key = self.handle_edit_keys(key, char) 1135 | if key == KEY_QUIT: 1136 | if self.hash != self.hash_buffer(): 1137 | res = self.line_edit("File changed! Quit (y/N/f)? ", "N") 1138 | if not res or res[0].upper() == "N": 1139 | continue 1140 | if res[0].upper() == "F": 1141 | key = KEY_FORCE_QUIT 1142 | Editor.place_list = [item for item in Editor.place_list if item[1] != self] 1143 | Editor.place_index = 0 1144 | self.scroll_region(0) 1145 | self.mouse_reporting(False) 1146 | self.goto(Editor.height, 0) 1147 | self.clear_to_eol() 1148 | self.undo = [] 1149 | return key 1150 | elif key == KEY_NEXT or key == KEY_PREV or type(key) is Editor: 1151 | return key 1152 | elif key == KEY_GET: 1153 | if self.mark is not None: 1154 | self.clear_mark() 1155 | self.display_window() 1156 | return key 1157 | def packtabs(self, s): 1158 | sb = StringIO() 1159 | for i in range(0, len(s), 8): 1160 | c = s[i : i + 8] 1161 | cr = c.rstrip(" ") 1162 | if (len(c) - len(cr)) > 1: 1163 | sb.write(cr + "\t") 1164 | else: 1165 | sb.write(c) 1166 | return sb.getvalue() 1167 | def hash_buffer(self): 1168 | res = 0 1169 | for line in self.content: 1170 | res = ((res * 227 + 1) ^ hash(line)) & 0x3FFFFFFF 1171 | return res 1172 | def get_file(self, fname): 1173 | if fname: 1174 | try: 1175 | self.fname = fname 1176 | if fname in (".", "..") or (os.stat(fname)[0] & 0x4000): 1177 | os.chdir(fname) 1178 | self.work_dir = os.getcwd() 1179 | self.fname = "/" if self.work_dir == "/" else self.work_dir.split("/")[-1] 1180 | self.content = ["Directory '{}'".format(self.work_dir), ""] + sorted( 1181 | os.listdir(".") 1182 | ) 1183 | self.is_dir = True 1184 | else: 1185 | if is_micropython: 1186 | with open(fname) as f: 1187 | self.content = f.readlines() 1188 | else: 1189 | with open(fname, errors="ignore") as f: 1190 | self.content = f.readlines() 1191 | self.write_tabs = False 1192 | i = 0 1193 | for l in self.content: 1194 | self.content[i] = self.expandtabs(l.rstrip()) 1195 | i += 1 1196 | except OSError: 1197 | self.message = "Error: file '" + fname + "' may not exist" 1198 | self.hash = self.hash_buffer() 1199 | def put_file(self, fname): 1200 | tmpfile = fname + ".pyetmp" 1201 | with open(tmpfile, "w") as f: 1202 | for l in self.content: 1203 | if self.write_tabs == "y": 1204 | f.write(self.packtabs(l)) 1205 | else: 1206 | f.write(l) 1207 | f.write("\n") 1208 | try: 1209 | os.remove(fname) 1210 | except: 1211 | pass 1212 | os.rename(tmpfile, fname) 1213 | def expandtabs(self, s): 1214 | if "\t" in s: 1215 | self.write_tabs = True 1216 | sb = StringIO() 1217 | pos = 0 1218 | for c in s: 1219 | if c == "\t": 1220 | sb.write(" " * (8 - pos % 8)) 1221 | pos += 8 - pos % 8 1222 | else: 1223 | sb.write(c) 1224 | pos += 1 1225 | return sb.getvalue() 1226 | else: 1227 | return s 1228 | def pye_edit(content, tab_size=4, undo=50, io_device=None): 1229 | if io_device is None: 1230 | print("IO device not defined") 1231 | return 1232 | gc.collect() 1233 | index = 0 1234 | undo = max(4, (undo if type(undo) is int else 0)) 1235 | current_dir = os.getcwd() 1236 | if content: 1237 | slot = [] 1238 | for f in content: 1239 | slot.append(Editor(tab_size, undo, io_device)) 1240 | if type(f) == str and f: 1241 | try: 1242 | slot[index].get_file(f) 1243 | except Exception as err: 1244 | slot[index].message = "{!r}".format(err) 1245 | else: 1246 | try: 1247 | slot[index].content = [ 1248 | str(_) for _ in f 1249 | ] 1250 | except: 1251 | slot[index].content = [str(f)] 1252 | index += 1 1253 | else: 1254 | slot = [Editor(tab_size, undo, io_device)] 1255 | slot[0].get_file(current_dir) 1256 | while True: 1257 | try: 1258 | index %= len(slot) 1259 | key = slot[index].edit_loop() 1260 | if key == KEY_QUIT: 1261 | if len(slot) == 1: 1262 | break 1263 | del slot[index] 1264 | elif key == KEY_GET: 1265 | f = slot[index].line_edit("Open file: ", "", Editor.file_char) 1266 | if f is not None: 1267 | slot.append(Editor(tab_size, undo, io_device)) 1268 | index = len(slot) - 1 1269 | slot[index].get_file(f) 1270 | elif key == KEY_NEXT: 1271 | index += 1 1272 | elif key == KEY_PREV: 1273 | index -= 1 1274 | elif key == KEY_FORCE_QUIT: 1275 | break 1276 | elif key in slot: 1277 | index = slot.index(key) 1278 | except Exception as err: 1279 | slot[index].message = "{!r}".format(err) 1280 | Editor.yank_buffer = [] 1281 | os.chdir(current_dir) 1282 | return slot[0].content if (slot[0].fname == "") else slot[0].fname 1283 | try: 1284 | import usys as sys 1285 | except: 1286 | import sys 1287 | class IO_DEVICE: 1288 | def __init__(self): 1289 | try: 1290 | from micropython import kbd_intr 1291 | kbd_intr(-1) 1292 | except ImportError: 1293 | pass 1294 | if hasattr(sys.stdin, "buffer"): 1295 | self.rd_raw_fct = sys.stdin.buffer.read 1296 | else: 1297 | self.rd_raw_fct = sys.stdin.read 1298 | def wr(self, s): 1299 | sys.stdout.write(s) 1300 | def rd(self): 1301 | return sys.stdin.read(1) 1302 | def rd_raw(self): 1303 | return self.rd_raw_fct(1) 1304 | def deinit_tty(self): 1305 | try: 1306 | from micropython import kbd_intr 1307 | kbd_intr(3) 1308 | except ImportError: 1309 | pass 1310 | def get_screen_size(self): 1311 | self.wr("\x1b[999;999H\x1b[6n") 1312 | pos = "" 1313 | char = self.rd() 1314 | while char != "R": 1315 | pos += char 1316 | char = self.rd() 1317 | return [int(i, 10) for i in pos.lstrip("\n\x1b[").split(";")] 1318 | if "pye_edit" not in globals().keys(): 1319 | from pye import pye_edit 1320 | def pye(*args, tab_size=4, undo=50): 1321 | io_device = IO_DEVICE() 1322 | ret = pye_edit(args, tab_size=tab_size, undo=undo, io_device=io_device) 1323 | io_device.deinit_tty() 1324 | return ret 1325 | -------------------------------------------------------------------------------- /pye_gen.py: -------------------------------------------------------------------------------- 1 | # 2 | # Front-end for Micropython standard console IO 3 | # 4 | try: 5 | import usys as sys 6 | except: 7 | import sys 8 | 9 | 10 | class IO_DEVICE: 11 | def __init__(self): 12 | try: 13 | from micropython import kbd_intr 14 | 15 | kbd_intr(-1) 16 | except ImportError: 17 | pass 18 | if hasattr(sys.stdin, "buffer"): 19 | self.rd_raw_fct = sys.stdin.buffer.read 20 | else: 21 | self.rd_raw_fct = sys.stdin.read 22 | 23 | def wr(self, s): 24 | sys.stdout.write(s) 25 | 26 | def rd(self): 27 | return sys.stdin.read(1) 28 | 29 | def rd_raw(self): 30 | return self.rd_raw_fct(1) 31 | 32 | def deinit_tty(self): 33 | try: 34 | from micropython import kbd_intr 35 | 36 | kbd_intr(3) 37 | except ImportError: 38 | pass 39 | 40 | def get_screen_size(self): 41 | self.wr("\x1b[999;999H\x1b[6n") 42 | pos = "" 43 | char = self.rd() ## expect ESC[yyy;xxxR 44 | while char != "R": 45 | pos += char 46 | char = self.rd() 47 | return [int(i, 10) for i in pos.lstrip("\n\x1b[").split(";")] 48 | 49 | 50 | ## test, if the Editor class is already present 51 | if "pye_edit" not in globals().keys(): 52 | from pye import pye_edit 53 | 54 | 55 | def pye(*args, tab_size=4, undo=50): 56 | io_device = IO_DEVICE() 57 | ret = pye_edit(args, tab_size=tab_size, undo=undo, io_device=io_device) 58 | io_device.deinit_tty() 59 | return ret 60 | -------------------------------------------------------------------------------- /pye_lcd.py: -------------------------------------------------------------------------------- 1 | # 2 | # Wrapper for Micropython standard console IO 3 | # 4 | import sys 5 | 6 | 7 | class IO_DEVICE: 8 | def __init__(self, Editor): 9 | self.init_display() 10 | self.init_terminal() 11 | 12 | Editor.KEYMAP["\x08"] = 0x08 13 | 14 | import busio, board 15 | 16 | self.uart = busio.UART( 17 | board.TX, board.RX, baudrate=115200, timeout=0.1, receiver_buffer_size=64 18 | ) 19 | 20 | try: 21 | from micropython import kbd_intr 22 | 23 | kbd_intr(-1) 24 | except ImportError: 25 | pass 26 | 27 | def init_display(self): 28 | import fontio, displayio, terminalio, board 29 | from adafruit_st7789 import ST7789 30 | 31 | displayio.release_displays() 32 | 33 | spi = board.SPI() 34 | tft_cs = board.D12 # arbitrary, pin not used for my display 35 | tft_dc = board.D2 36 | tft_backlight = board.D4 37 | tft_reset = board.D3 38 | 39 | while not spi.try_lock(): 40 | pass 41 | spi.unlock() 42 | 43 | display_bus = displayio.FourWire( 44 | spi, 45 | command=tft_dc, 46 | chip_select=tft_cs, 47 | reset=tft_reset, 48 | baudrate=24000000, 49 | polarity=1, 50 | phase=1, 51 | ) 52 | 53 | self.xPixels = 240 # number of xPixels for the display 54 | self.yPixels = 240 # number of yPixels for the display 55 | 56 | self.display = ST7789( 57 | display_bus, 58 | width=self.xPixels, 59 | height=self.yPixels, 60 | rotation=0, 61 | rowstart=80, 62 | colstart=0, 63 | ) 64 | self.display.show(None) 65 | 66 | def init_terminal(self): 67 | from simpleTerminal import editorTerminal 68 | 69 | self.terminal = editorTerminal( 70 | self.display, displayXPixels=self.xPixels, displayYPixels=self.yPixels 71 | ) 72 | 73 | def wr(self, s): 74 | self.terminal.write(s) 75 | 76 | def rd(self): 77 | while True: 78 | myInput = self.uart.read(1) # for using uart 79 | if myInput: 80 | return myInput.decode("utf-8") 81 | 82 | def rd_raw(self): ## just to have it implemented 83 | return self.rd() 84 | 85 | def deinit_tty(self): 86 | self.uart.deinit() # clear out the UART 87 | self.display.show(None) # remove the groups from the display 88 | try: 89 | from micropython import kbd_intr 90 | 91 | kbd_intr(3) 92 | except ImportError: 93 | pass 94 | 95 | def get_screen_size(self): 96 | return self.terminal.getScreenSize() 97 | 98 | 99 | ## test, if the Editor class is already present 100 | if "pye_edit" not in globals().keys(): 101 | from pye import pye_edit, Editor 102 | 103 | 104 | def pye(*args, tab_size=4, undo=50): 105 | io_device = IO_DEVICE(Editor) 106 | ret = pye_edit(args, tab_size=tab_size, undo=undo, io_device=io_device) 107 | io_device.deinit_tty() 108 | return ret 109 | -------------------------------------------------------------------------------- /pye_ux.py: -------------------------------------------------------------------------------- 1 | # 2 | # Front-end for Linux 3 | # 4 | import os, tty, signal, termios, sys 5 | 6 | 7 | class SignalWindowResize(Exception): 8 | pass 9 | 10 | 11 | class IO_DEVICE: 12 | def __init__(self, device, key_redraw): 13 | self.org_termios = termios.tcgetattr(device) 14 | tty.setraw(device) 15 | self.sdev = device 16 | self.key_redraw = key_redraw 17 | 18 | def wr(self, s): 19 | os.write(1, s.encode("utf-8")) 20 | 21 | def rd(self): 22 | while True: 23 | try: ## The signal handler for SIGWINCH raises an exception 24 | c = os.read(self.sdev, 1) 25 | flag = c[0] 26 | while (flag & 0xC0) == 0xC0: ## utf-8 char collection 27 | c += os.read(self.sdev, 1) 28 | flag <<= 1 29 | return c.decode("utf-8") 30 | except SignalWindowResize: 31 | return chr(self.key_redraw) 32 | 33 | def rd_raw(self): 34 | return os.read(self.sdev, 1) 35 | 36 | def get_screen_size(self): 37 | if hasattr(signal, "SIGWINCH"): 38 | signal.signal(signal.SIGWINCH, signal.SIG_IGN) 39 | self.wr("\x1b[999;999H\x1b[6n") 40 | pos = "" 41 | char = self.rd() ## expect ESC[yyy;xxxR 42 | while char != "R": 43 | pos += char 44 | char = self.rd() 45 | if hasattr(signal, "SIGWINCH"): 46 | signal.signal(signal.SIGWINCH, IO_DEVICE.signal_handler) 47 | return [int(i, 10) for i in pos.lstrip("\n\x1b[").split(";")] 48 | 49 | def deinit_tty(self): 50 | termios.tcsetattr(self.sdev, termios.TCSANOW, self.org_termios) 51 | 52 | @staticmethod 53 | def signal_handler(sig, frame): 54 | raise SignalWindowResize ## raise the specific exception, which stops os.read() 55 | 56 | 57 | ## test, if the Editor class is already present 58 | if "pye_edit" not in globals().keys(): 59 | from pye_core import pye_edit, is_micropython, KEY_REDRAW, Editor 60 | 61 | Editor.match_span = 500 62 | 63 | 64 | def pye(*args, tab_size=4, undo=500): 65 | io_device = IO_DEVICE(0, KEY_REDRAW) 66 | ret = pye_edit(*args, tab_size=tab_size, undo=undo, io_device=io_device) 67 | io_device.deinit_tty() 68 | return ret 69 | 70 | 71 | if __name__ == "__main__": 72 | import stat 73 | 74 | fd_tty = 0 75 | if len(sys.argv) > 1: 76 | name = sys.argv[1:] 77 | io_device = IO_DEVICE(fd_tty, KEY_REDRAW) 78 | pye_edit(name, undo=500, io_device=io_device) 79 | else: 80 | name = "." 81 | if not is_micropython: 82 | mode = os.fstat(0).st_mode 83 | if stat.S_ISFIFO(mode) or stat.S_ISREG(mode): 84 | name = [[l.rstrip("\r\n\t ").expandtabs(8) for l in sys.stdin]] 85 | os.close(0) ## close and repopen /dev/tty 86 | fd_tty = os.open("/dev/tty", os.O_RDONLY) ## memorized, if new fd 87 | io_device = IO_DEVICE(fd_tty, KEY_REDRAW) 88 | pye_edit(name, undo=500, io_device=io_device) 89 | 90 | io_device.deinit_tty() 91 | -------------------------------------------------------------------------------- /pye_win.py: -------------------------------------------------------------------------------- 1 | # 2 | # Front-end for Micropython standard console IO 3 | # 4 | try: 5 | import usys as sys 6 | except: 7 | import sys 8 | 9 | 10 | class IO_DEVICE: 11 | def __init__(self): 12 | self.rd_raw_fct = self.rd 13 | self.peek_char = None 14 | 15 | def wr(self, s): 16 | from msvcrt import putwch 17 | 18 | for c in s: 19 | putwch(c) 20 | 21 | def rd(self): 22 | from msvcrt import getwch 23 | 24 | if self.peek_char is not None: 25 | c = self.peek_char 26 | self.peek_char = None 27 | return c 28 | c = getwch() 29 | if ord(c) == 224 or c == "\x00": # translate the keyboard escape sequences 30 | c = getwch() 31 | try: # borrowed from mpr.py 32 | self.peek_char = { 33 | "H": "A", # UP 34 | "P": "B", # DOWN 35 | "M": "C", # RIGHT 36 | "K": "D", # LEFT 37 | "G": "H", # POS1 38 | "O": "F", # END 39 | "Q": "6~", # PGDN 40 | "I": "5~", # PGUP 41 | "s": "1;5D", # CTRL-LEFT, 42 | "t": "1;5C", # CTRL-RIGHT, 43 | "\x8d": "1;5A", # CTRL-UP, 44 | "\x91": "1;5B", # CTRL-DOWN, 45 | "w": "1;5H", # CTRL-POS1 46 | "u": "1;5F", # CTRL-END 47 | "\x98": "1;3A", # ALT-UP, 48 | "\xa0": "1;3B", # ALT-DOWN, 49 | "S": "3~", # DEL, 50 | "\x93": "3;5~", # CTRL-DEL 51 | "\x94": "Z", # Ctrl-Tab = BACKTAB, 52 | }[c] 53 | except: 54 | self.peek_char = "~" # illegal code, will be ignored 55 | return "\x1b[" 56 | else: 57 | return c 58 | 59 | def rd_raw(self): 60 | return self.rd_raw_fct(1) 61 | 62 | def deinit_tty(self): 63 | pass 64 | 65 | def get_screen_size(self): 66 | self.wr("\x1b[999;999H\x1b[6n") 67 | pos = "" 68 | char = self.rd() ## expect ESC[yyy;xxxR 69 | while char != "R": 70 | pos += char 71 | char = self.rd() 72 | return [int(i, 10) for i in pos.lstrip(" \n\x1b[").split(";")] 73 | 74 | 75 | ## test, if the Editor class is already present 76 | if "pye_edit" not in globals().keys(): 77 | from pye import pye_edit, Editor, KEY_BACKSPACE 78 | 79 | Editor.KEYMAP["\x08"] = KEY_BACKSPACE 80 | Editor.match_span = 500 81 | 82 | 83 | def pye(*args, tab_size=4, undo=500): 84 | io_device = IO_DEVICE() 85 | ret = pye_edit(*args, tab_size=tab_size, undo=undo, io_device=io_device) 86 | io_device.deinit_tty() 87 | return ret 88 | 89 | 90 | if __name__ == "__main__": 91 | io_device = IO_DEVICE() 92 | if len(sys.argv) > 1: 93 | name = sys.argv[1:] 94 | pye_edit(name, undo=500, io_device=io_device) 95 | else: 96 | name = "." 97 | pye_edit(name, undo=500, io_device=io_device) 98 | 99 | io_device.deinit_tty() 100 | -------------------------------------------------------------------------------- /pye_x3.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robert-hh/Micropython-Editor/ed544c7a984be6db3d88c1682924409bae339da1/pye_x3.mpy -------------------------------------------------------------------------------- /pye_xbee.py: -------------------------------------------------------------------------------- 1 | ## 2 | ## Small python text editor based on the 3 | ## Very simple VT100 terminal text editor widget 4 | ## Copyright (c) 2015 Paul Sokolovsky (initial code) 5 | ## Copyright (c) 2015-2020 Robert Hammelrath (additional code) 6 | ## Distributed under MIT License 7 | ## Changes: 8 | ## - Ported the code to boards from micropython.org, Pycom Boards, 9 | ## sipeed boards, Adafruit Circuitpython boards (still runs on Linux or Darwin) 10 | ## - changed read keyboard function to comply with char-by-char input 11 | ## - added support for TAB, BACKTAB, SAVE, DEL and Backspace joining lines, 12 | ## Find, Replace, Goto Line, UNDO, REDO GET file, Auto-Indent, Set Flags, 13 | ## Copy/Cut & Paste, Indent, Dedent 14 | ## - Added mouse support for pointing and scrolling 15 | ## - handling tab (0x09) on reading & writing files, 16 | ## - Added a status line and single line prompts for 17 | ## Quit, Save, Find, Replace, Flags and Goto 18 | ## - moved main into a function with some optional parameters 19 | ## - Added multi-file support 20 | ## 21 | 22 | PYE_VERSION = " V2.65x " 23 | 24 | import sys 25 | import gc 26 | import uos as os 27 | from uio import StringIO 28 | 29 | KEY_NONE = const(0x00) 30 | KEY_UP = const(0x0B) 31 | KEY_DOWN = const(0x0D) 32 | KEY_LEFT = const(0x1F) 33 | KEY_RIGHT = const(0x1E) 34 | KEY_HOME = const(0x10) 35 | KEY_END = const(0x03) 36 | KEY_PGUP = const(0xFFF1) 37 | KEY_PGDN = const(0xFFF2) 38 | KEY_WORD_LEFT = const(0xFFF3) 39 | KEY_WORD_RIGHT = const(0xFFF4) 40 | KEY_SHIFT_UP = const(0xFFF5) 41 | KEY_ALT_UP = const(0xFFEA) 42 | KEY_SHIFT_DOWN = const(0xFFF6) 43 | KEY_ALT_DOWN = const(0xFFEB) 44 | KEY_SHIFT_LEFT = const(0xFFF0) 45 | KEY_SHIFT_RIGHT = const(0xFFEF) 46 | KEY_SHIFT_CTRL_LEFT = const(0xFFED) 47 | KEY_SHIFT_CTRL_RIGHT = const(0xFFEC) 48 | KEY_QUIT = const(0x11) 49 | KEY_ENTER = const(0x0A) 50 | KEY_BACKSPACE = const(0x08) 51 | KEY_DELETE = const(0x7F) 52 | KEY_DEL_WORD = const(0xFFF7) 53 | KEY_WRITE = const(0x13) 54 | KEY_TAB = const(0x09) 55 | KEY_BACKTAB = const(0x15) 56 | KEY_FIND = const(0x06) 57 | KEY_GOTO = const(0x07) 58 | KEY_MOUSE = const(0x1B) 59 | KEY_SCRLUP = const(0x1C) 60 | KEY_SCRLDN = const(0x1D) 61 | KEY_FIND_AGAIN = const(0x0E) 62 | KEY_REDRAW = const(0x05) 63 | KEY_UNDO = const(0x1A) 64 | KEY_REDO = const(0xFFEE) 65 | KEY_CUT = const(0x18) 66 | KEY_PASTE = const(0x16) 67 | KEY_COPY = const(0x04) 68 | KEY_FIRST = const(0x14) 69 | KEY_LAST = const(0x02) 70 | KEY_REPLC = const(0x12) 71 | KEY_TOGGLE = const(0x01) 72 | KEY_GET = const(0x0F) 73 | KEY_MARK = const(0x0C) 74 | KEY_NEXT = const(0x17) 75 | KEY_COMMENT = const(0xFFFC) 76 | KEY_MATCH = const(0xFFFD) 77 | KEY_INDENT = const(0xFFFE) 78 | KEY_DEDENT = const(0xFFFF) 79 | 80 | 81 | class Editor: 82 | KEYMAP = { ## Gets lengthy 83 | "\x1b[A": KEY_UP, 84 | "\x1b[1;2A": KEY_SHIFT_UP, 85 | "\x1b[1;3A": KEY_ALT_UP, 86 | "\x1b[B": KEY_DOWN, 87 | "\x1b[1;2B": KEY_SHIFT_DOWN, 88 | "\x1b[1;3B": KEY_ALT_DOWN, 89 | "\x1b[D": KEY_LEFT, 90 | "\x1b[1;2D": KEY_SHIFT_LEFT, 91 | "\x1b[1;6D": KEY_SHIFT_CTRL_LEFT, 92 | "\x1b[C": KEY_RIGHT, 93 | "\x1b[1;2C": KEY_SHIFT_RIGHT, 94 | "\x1b[1;6C": KEY_SHIFT_CTRL_RIGHT, 95 | "\x1b[H": KEY_HOME, ## in Linux Terminal 96 | "\x1bOH": KEY_HOME, ## Picocom, Minicom 97 | "\x1b[1~": KEY_HOME, ## Putty 98 | "\x1b[F": KEY_END, ## Linux Terminal 99 | "\x1bOF": KEY_END, ## Picocom, Minicom 100 | "\x1b[4~": KEY_END, ## Putty 101 | "\x1b[5~": KEY_PGUP, 102 | "\x1b[6~": KEY_PGDN, 103 | "\x1b[1;5D": KEY_WORD_LEFT, 104 | "\x1b[1;5C": KEY_WORD_RIGHT, 105 | "\x03": KEY_COPY, ## Ctrl-C 106 | "\r": KEY_ENTER, 107 | "\x7f": KEY_BACKSPACE, ## Ctrl-? (127) 108 | "\x1b[3~": KEY_DELETE, 109 | "\x1b[Z": KEY_BACKTAB, ## Shift Tab 110 | "\x19": KEY_REDO, ## Ctrl-Y 111 | "\x08": KEY_REPLC, ## Ctrl-H 112 | "\x12": KEY_REPLC, ## Ctrl-R 113 | "\x11": KEY_QUIT, ## Ctrl-Q 114 | "\n": KEY_ENTER, 115 | "\x13": KEY_WRITE, ## Ctrl-S 116 | "\x06": KEY_FIND, ## Ctrl-F 117 | "\x0e": KEY_FIND_AGAIN, ## Ctrl-N 118 | "\x07": KEY_GOTO, ## Ctrl-G 119 | "\x05": KEY_REDRAW, ## Ctrl-E 120 | "\x1a": KEY_UNDO, ## Ctrl-Z 121 | "\x09": KEY_TAB, 122 | "\x15": KEY_BACKTAB, ## Ctrl-U 123 | "\x18": KEY_CUT, ## Ctrl-X 124 | "\x16": KEY_PASTE, ## Ctrl-V 125 | "\x04": KEY_COPY, ## Ctrl-D 126 | "\x0c": KEY_MARK, ## Ctrl-L 127 | "\x00": KEY_MARK, ## Ctrl-Space 128 | "\x14": KEY_FIRST, ## Ctrl-T 129 | "\x02": KEY_LAST, ## Ctrl-B 130 | "\x01": KEY_TOGGLE, ## Ctrl-A 131 | "\x17": KEY_NEXT, ## Ctrl-W 132 | "\x0f": KEY_GET, ## Ctrl-O 133 | "\x10": KEY_COMMENT, ## Ctrl-P 134 | ## other keys 135 | "\x1b[1;5A": KEY_SCRLUP, ## Ctrl-Up 136 | "\x1b[1;5B": KEY_SCRLDN, ## Ctrl-Down 137 | "\x1b[1;5H": KEY_FIRST, ## Ctrl-Home 138 | "\x1b[1;5F": KEY_LAST, ## Ctrl-End 139 | "\x1b[3;5~": KEY_DEL_WORD, ## Ctrl-Del 140 | "\x0b": KEY_MATCH, ## Ctrl-K 141 | "\x1b[M": KEY_MOUSE, 142 | } 143 | 144 | TERMCMD = [ ## list of terminal control strings 145 | "\x1b[{row};{col}H", ## 0: Set cursor 146 | "\x1b[0K", ## 1: Clear EOL 147 | "\x1b[?25h", ## 2: Cursor ON 148 | "\x1b[?25l", ## 3: Cursor OFF 149 | "\x1b[0m", ## 4: Hilite 0 - normal text 150 | "\x1b[1;37;46m", ## 5: Hilite 1 - Entering the status line 151 | "\x1b[43m", ## 6: Hilite 2 - Highligthing Text 152 | "\x1b[?9h", ## 7: Mouse reporting on 153 | "\x1b[?9l", ## 8: Mouse reporting off 154 | "\x1bM", ## 9: Scroll one line up 155 | "\n", ## 10: Scroll one line down 156 | "\x1b[1;{stop}r", ## 11: Set lowest line of scrolling range 157 | "\x1b[r", ## 12: Scroll the full screen 158 | "\b", ## 13: backspace one character, used in line_edit 159 | ## 14: Long status line format string. 160 | "{chd}{file} Row: {row}/{total} Col: {col} {msg}", 161 | ## 15: Shorter status line format string. 162 | "{chd}{file} {row}:{col} {msg}", 163 | ] 164 | 165 | ## symbols that are shared between instances of Editor 166 | yank_buffer = [] 167 | find_pattern = "" 168 | case = "n" 169 | autoindent = "y" 170 | replc_pattern = "" 171 | comment_char = "\x23 " ## for # 172 | word_char = "_\\" ## additional characters in a word 173 | file_char = "_.-" # additional characters in a file name 174 | match_span = 50 ## number of lines to search for a bracket match 175 | 176 | def __init__(self, tab_size, undo_limit, io_device): 177 | self.top_line = self.cur_line = self.row = self.vcol = self.col = self.margin = 0 178 | self.tab_size = tab_size 179 | self.changed = "" 180 | self.hash = 0 181 | self.message = self.fname = "" 182 | self.content = [""] 183 | self.undo = [] 184 | self.undo_limit = undo_limit 185 | self.redo = [] 186 | self.mark = None 187 | self.write_tabs = "n" 188 | self.work_dir = os.getcwd() 189 | self.io_device = io_device 190 | self.wr = io_device.wr 191 | self.is_dir = False 192 | self.key_max = 0 193 | for _ in Editor.KEYMAP.keys(): 194 | self.key_max = max(self.key_max, len(_)) 195 | 196 | def goto(self, row, col): 197 | self.wr(Editor.TERMCMD[0].format(row=row + 1, col=col + 1)) 198 | 199 | def clear_to_eol(self): 200 | self.wr(Editor.TERMCMD[1]) 201 | 202 | def cursor(self, onoff): 203 | self.wr(Editor.TERMCMD[2] if onoff else Editor.TERMCMD[3]) 204 | 205 | def hilite(self, mode): 206 | if mode == 1: ## used for the status line 207 | self.wr(Editor.TERMCMD[5]) 208 | elif mode == 2: ## used for the marked area 209 | self.wr(Editor.TERMCMD[6]) 210 | else: ## plain text 211 | self.wr(Editor.TERMCMD[4]) 212 | 213 | def mouse_reporting(self, onoff): 214 | self.wr( 215 | Editor.TERMCMD[7] if onoff else Editor.TERMCMD[8] 216 | ) ## enable/disable mouse reporting 217 | 218 | def scroll_region(self, stop): 219 | self.wr( 220 | Editor.TERMCMD[11].format(stop=stop) if stop else Editor.TERMCMD[12] 221 | ) ## set scrolling range 222 | 223 | def scroll_up(self, scrolling): 224 | if Editor.TERMCMD[9]: 225 | Editor.scrbuf[scrolling:] = Editor.scrbuf[:-scrolling] 226 | Editor.scrbuf[:scrolling] = [""] * scrolling 227 | self.goto(0, 0) 228 | self.wr(Editor.TERMCMD[9] * scrolling) 229 | 230 | def scroll_down(self, scrolling): 231 | if Editor.TERMCMD[10]: 232 | Editor.scrbuf[:-scrolling] = Editor.scrbuf[scrolling:] 233 | Editor.scrbuf[-scrolling:] = [""] * scrolling 234 | self.goto(Editor.height - 1, 0) 235 | self.wr(Editor.TERMCMD[10] * scrolling) 236 | 237 | def redraw(self, flag): 238 | self.cursor(False) 239 | Editor.height, Editor.width = self.io_device.get_screen_size() 240 | Editor.height -= 1 241 | Editor.scrbuf = [(False, "\x00")] * Editor.height ## force delete 242 | self.row = min(Editor.height - 1, self.row) 243 | self.scroll_region(Editor.height) 244 | self.mouse_reporting(True) ## enable mouse reporting 245 | if flag: 246 | self.message = PYE_VERSION 247 | gc.collect() 248 | if flag: 249 | self.message += "{} Bytes Memory available".format(gc.mem_free()) 250 | self.changed = "" if self.hash == self.hash_buffer() else "*" 251 | 252 | def get_input( 253 | self, 254 | ): ## read from interface/keyboard one byte each and match against function keys 255 | while True: 256 | in_buffer = self.io_device.rd() 257 | if in_buffer == "\x1b": ## starting with ESC, must be fct 258 | while True: 259 | in_buffer += self.io_device.rd() 260 | c = in_buffer[-1] 261 | if c == "~" or (c.isalpha() and c != "O"): 262 | break 263 | if len(in_buffer) >= self.key_max: 264 | break 265 | if len(in_buffer) == 2 and c.isalpha(): ## map alt-chr onto ctrl-chr 266 | in_buffer = chr(ord(in_buffer[1]) & 0x1F) 267 | if in_buffer in Editor.KEYMAP: 268 | c = Editor.KEYMAP[in_buffer] 269 | if c != KEY_MOUSE: 270 | return c, None 271 | else: ## special for mice 272 | mouse_fct = ord(self.io_device.rd_raw()) ## read 3 more chars 273 | mouse_x = ord(self.io_device.rd_raw()) - 33 274 | mouse_y = ord(self.io_device.rd_raw()) - 33 275 | if mouse_fct == 0x61: 276 | return KEY_SCRLDN, 3 277 | elif mouse_fct == 0x60: 278 | return KEY_SCRLUP, 3 279 | else: 280 | return KEY_MOUSE, [mouse_x, mouse_y, mouse_fct] ## set the cursor 281 | elif ord(in_buffer[0]) >= 32: 282 | return KEY_NONE, in_buffer 283 | 284 | def display_window(self): ## Update window and status line 285 | ## Force cur_line and col to be in the reasonable bounds 286 | self.cur_line = min(self.total_lines - 1, max(self.cur_line, 0)) 287 | self.vcol = max(0, min(self.col, len(self.content[self.cur_line]))) 288 | ## Check if Column is out of view, and align margin if needed 289 | if self.vcol >= Editor.width + self.margin: 290 | self.margin = self.vcol - Editor.width + (Editor.width >> 2) 291 | elif self.vcol < self.margin: 292 | self.margin = max(self.vcol - (Editor.width >> 2), 0) 293 | ## if cur_line is out of view, align top_line to the given row 294 | if not (self.top_line <= self.cur_line < self.top_line + Editor.height): # Visible? 295 | self.top_line = max(self.cur_line - self.row, 0) 296 | ## in any case, align row to top_line and cur_line 297 | self.row = self.cur_line - self.top_line 298 | ## update_screen 299 | self.cursor(False) 300 | line = self.top_line 301 | if self.mark is None: 302 | flag = 0 303 | else: 304 | start_line, start_col, end_line, end_col = self.mark_range() 305 | start_col = max(start_col - self.margin, 0) 306 | end_col = max(end_col - self.margin, 0) 307 | 308 | for c in range(Editor.height): 309 | if line == self.total_lines: ## at empty bottom screen part 310 | if Editor.scrbuf[c] != (False, ""): 311 | self.goto(c, 0) 312 | self.clear_to_eol() 313 | Editor.scrbuf[c] = (False, "") 314 | else: 315 | if self.mark is not None: 316 | flag = ( 317 | (start_line <= line < end_line) 318 | + ((start_line == line) << 1) 319 | + (((end_line - 1) == line) << 2) 320 | ) 321 | l = (flag, self.content[line][self.margin : self.margin + Editor.width]) 322 | if (flag and line == self.cur_line) or l != Editor.scrbuf[ 323 | c 324 | ]: ## line changed, print it 325 | self.goto(c, 0) 326 | if flag == 0: # no mark 327 | self.wr(l[1]) 328 | elif flag == 7: # only line of a mark 329 | self.wr(l[1][:start_col]) 330 | self.hilite(2) 331 | self.wr(l[1][start_col:end_col]) 332 | self.hilite(0) 333 | self.wr(l[1][end_col:]) 334 | elif flag == 3: # first line of mark 335 | self.wr(l[1][:start_col]) 336 | self.hilite(2) 337 | self.wr(l[1][start_col:]) 338 | self.wr(" ") 339 | self.hilite(0) 340 | elif flag == 5: # last line of mark 341 | self.hilite(2) 342 | self.wr(l[1][:end_col]) 343 | self.hilite(0) 344 | self.wr(l[1][end_col:]) 345 | else: # middle line of a mark 346 | self.hilite(2) 347 | self.wr(l[1]) 348 | self.wr(" ") 349 | self.hilite(0) 350 | if len(l[1]) < Editor.width: 351 | self.clear_to_eol() 352 | Editor.scrbuf[c] = l 353 | line += 1 354 | ## display Status-Line 355 | self.goto(Editor.height, 0) 356 | self.hilite(1) 357 | self.wr( 358 | Editor.TERMCMD[14 if Editor.width > 40 else 15].format( 359 | chd=self.changed, 360 | file=self.fname, 361 | row=self.cur_line + 1, 362 | total=self.total_lines, 363 | col=self.vcol + 1, 364 | msg=self.message, 365 | )[: self.width - 1] 366 | ) 367 | self.clear_to_eol() ## once moved up for mate/xfce4-terminal issue with scroll region 368 | self.hilite(0) 369 | self.goto(self.row, self.vcol - self.margin) 370 | self.cursor(True) 371 | 372 | def spaces(self, line, pos=None): ## count spaces 373 | return ( 374 | len(line) - len(line.lstrip(" ")) 375 | if pos is None 376 | else len(line[:pos]) - len(line[:pos].rstrip(" ")) ## at line start 377 | ) 378 | 379 | def mark_range(self): 380 | if self.mark_order(self.cur_line, self.col) >= 0: 381 | return (self.mark[0], self.mark[1], self.cur_line + 1, self.col) 382 | else: 383 | return (self.cur_line, self.col, self.mark[0] + 1, self.mark[1]) 384 | 385 | def mark_order(self, line, col): 386 | return col - self.mark[1] if self.mark[0] == line else line - self.mark[0] 387 | 388 | def line_range(self): 389 | res = self.mark_range() 390 | return (res[0], res[2]) if res[3] > 0 else (res[0], res[2] - 1) 391 | 392 | def line_edit( 393 | self, prompt, default, zap=None 394 | ): ## better one: added cursor keys and backsp, delete 395 | push_msg = lambda msg: self.wr( 396 | msg + Editor.TERMCMD[13] * len(msg) 397 | ) ## Write a message and move cursor back 398 | self.goto(Editor.height, 0) 399 | self.hilite(1) 400 | self.wr(prompt) 401 | self.wr(default) 402 | self.clear_to_eol() 403 | res = default 404 | pos = len(res) 405 | del_all = True 406 | mouse_last = None 407 | while True: 408 | key, char = self.get_input() ## Get Char of Fct. 409 | if key == KEY_NONE: ## char to be inserted 410 | if len(prompt) + len(res) < self.width - 2: 411 | res = res[:pos] + char + res[pos:] 412 | self.wr(res[pos]) 413 | pos += len(char) 414 | push_msg(res[pos:]) ## update tail 415 | elif key in (KEY_ENTER, KEY_TAB): ## Finis 416 | self.hilite(0) 417 | return res 418 | elif key in (KEY_QUIT, KEY_COPY): ## Abort 419 | self.hilite(0) 420 | return None 421 | elif key == KEY_LEFT: 422 | if pos > 0: 423 | self.wr(Editor.TERMCMD[13]) 424 | pos -= 1 425 | elif key == KEY_RIGHT: 426 | if pos < len(res): 427 | self.wr(res[pos]) 428 | pos += 1 429 | elif key == KEY_HOME: 430 | self.wr(Editor.TERMCMD[13] * pos) 431 | pos = 0 432 | elif key == KEY_END: 433 | self.wr(res[pos:]) 434 | pos = len(res) 435 | elif key == KEY_DELETE: ## Delete 436 | if del_all: 437 | self.wr(Editor.TERMCMD[13] * pos) 438 | self.wr(" " * len(res)) 439 | self.wr(Editor.TERMCMD[13] * len(res)) 440 | pos = 0 441 | res = "" 442 | else: 443 | if pos < len(res): 444 | res = res[:pos] + res[pos + 1 :] 445 | push_msg(res[pos:] + " ") ## update tail 446 | elif key == KEY_BACKSPACE: ## Backspace 447 | if pos > 0: 448 | res = res[: pos - 1] + res[pos:] 449 | self.wr(Editor.TERMCMD[13]) 450 | pos -= 1 451 | push_msg(res[pos:] + " ") ## update tail 452 | elif key == KEY_PASTE: ## Get from content 453 | res += self.getsymbol(self.content[self.cur_line], self.col, zap)[ 454 | : Editor.width - pos - len(prompt) - 1 455 | ] 456 | push_msg(res[pos:]) 457 | elif key == KEY_MOUSE: 458 | if char[1] < Editor.height: 459 | self.col = char[0] + self.margin 460 | self.cur_line = char[1] + self.top_line 461 | if (self.col, self.cur_line) != mouse_last: 462 | mouse_last = (self.col, self.cur_line) 463 | self.wr(Editor.TERMCMD[13] * pos) 464 | self.wr(" " * len(res)) 465 | self.wr(Editor.TERMCMD[13] * len(res)) 466 | pos = 0 467 | res = self.getsymbol(self.content[self.cur_line], self.col, zap) 468 | push_msg(res) 469 | else: ## double click at the same place. 470 | self.hilite(0) 471 | return res 472 | del_all = False 473 | 474 | def getsymbol(self, s, pos, zap): 475 | if pos < len(s) and zap is not None: 476 | start = self.skip_while(s, pos, zap, -1) 477 | stop = self.skip_while(s, pos, zap, 1) 478 | return s[start + 1 : stop] 479 | else: 480 | return "" 481 | 482 | def issymbol(self, c, zap): 483 | return c.isalpha() or c.isdigit() or c in zap 484 | 485 | def skip_until(self, s, pos, zap, way): 486 | stop = -1 if way < 0 else len(s) 487 | while pos != stop and not self.issymbol(s[pos], zap): 488 | pos += way 489 | return pos 490 | 491 | def skip_while(self, s, pos, zap, way): 492 | stop = -1 if way < 0 else len(s) 493 | while pos != stop and self.issymbol(s[pos], zap): 494 | pos += way 495 | return pos 496 | 497 | def move_up(self): 498 | if self.cur_line > 0: 499 | self.cur_line -= 1 500 | if self.cur_line < self.top_line: 501 | self.scroll_up(1) 502 | 503 | def skip_up(self): 504 | if self.col == 0 and self.cur_line > 0: 505 | self.col = len(self.content[self.cur_line - 1]) 506 | self.move_up() 507 | return True 508 | else: 509 | return False 510 | 511 | def move_left(self): 512 | self.col = self.vcol 513 | if not self.skip_up(): 514 | self.col -= 1 515 | 516 | def move_down(self): 517 | if self.cur_line < self.total_lines - 1: 518 | self.cur_line += 1 519 | if self.cur_line == self.top_line + Editor.height: 520 | self.scroll_down(1) 521 | 522 | def skip_down(self, l): 523 | if self.col >= len(l) and self.cur_line < self.total_lines - 1: 524 | self.col = 0 525 | self.move_down() 526 | return True 527 | else: 528 | return False 529 | 530 | def move_right(self, l): 531 | if not self.skip_down(l): 532 | self.col += 1 533 | 534 | # this is the simple version of find 535 | def find_in_file(self, pattern, pos, end): 536 | Editor.find_pattern = pattern # remember it 537 | if Editor.case != "y": 538 | pattern = pattern.lower() 539 | spos = pos 540 | for line in range(self.cur_line, end): 541 | if Editor.case != "y": 542 | match = self.content[line][spos:].lower().find(pattern) 543 | else: 544 | match = self.content[line][spos:].find(pattern) 545 | if match >= 0: # Bingo! 546 | self.col = match + spos 547 | self.cur_line = line 548 | return len(pattern) 549 | spos = 0 550 | else: 551 | self.message = "No match: " + Editor.find_pattern 552 | return None 553 | 554 | def undo_add(self, lnum, text, key, span=1, chain=False): 555 | self.changed = "*" 556 | if ( 557 | len(self.undo) == 0 558 | or key == KEY_NONE 559 | or self.undo[-1][3] != key 560 | or self.undo[-1][0] != lnum 561 | ): 562 | if len(self.undo) >= self.undo_limit: ## drop oldest undo(s), if full 563 | del self.undo[0] 564 | self.undo.append([lnum, span, text, key, self.col, chain]) 565 | self.redo = [] ## clear re-do list. 566 | 567 | def undo_redo(self, undo, redo): 568 | chain = True 569 | redo_start = len(redo) 570 | while len(undo) > 0 and chain: 571 | action = undo.pop() ## get action from stack 572 | if not action[3] in (KEY_INDENT, KEY_DEDENT, KEY_COMMENT): 573 | self.cur_line = action[0] ## wrong for Bkspc of BOL 574 | self.col = action[4] 575 | if len(redo) >= self.undo_limit: ## mybe not enough 576 | del redo[0] 577 | if action[1] >= 0: ## insert or replace line 578 | redo.append( 579 | action[0:1] 580 | + [len(action[2])] 581 | + [self.content[action[0] : action[0] + action[1]]] ## safe to redo stack 582 | + action[3:] 583 | ) 584 | if action[0] < self.total_lines: 585 | self.content[action[0] : action[0] + action[1]] = action[2] # insert lines 586 | else: 587 | self.content += action[2] 588 | else: ## delete lines, restore the current line 589 | redo.append( 590 | action[0:1] 591 | + [1] 592 | + [ ## undo deletes, redo inserts 593 | self.content[action[0] : action[0] - action[1] + 1] 594 | ] 595 | + action[3:] 596 | ) 597 | del self.content[action[0] : action[0] - action[1]] 598 | self.content[action[0]] = action[2][0] # replace current line with save content 599 | chain = action[5] 600 | if (len(redo) - redo_start) > 0: ## Performed at least one action 601 | redo[-1][5] = True ## fix the chaining flags for reversed action order. 602 | redo[redo_start][5] = False 603 | self.total_lines = len(self.content) ## Reset the length and change indicator 604 | self.changed = "" if self.hash == self.hash_buffer() else "*" 605 | self.mark = None 606 | 607 | def set_mark(self): ## start the highlighting if not done yet 608 | if self.mark is None: 609 | self.mark = (self.cur_line, self.col) 610 | 611 | def yank_mark(self): # Copy marked area to the yank buffer 612 | start_row, start_col, end_row, end_col = self.mark_range() 613 | ## copy first the whole area 614 | Editor.yank_buffer = self.content[start_row:end_row] 615 | ## then remove parts that do not have to be copied. Last line first 616 | Editor.yank_buffer[-1] = Editor.yank_buffer[-1][:end_col] 617 | Editor.yank_buffer[0] = Editor.yank_buffer[0][start_col:] 618 | 619 | def delete_mark(self, yank): ## copy marked lines (opt) and delete them 620 | if yank: 621 | self.yank_mark() 622 | ## delete by composing fractional lines into the ifrst one and erase remaining lines 623 | start_row, start_col, end_row, end_col = self.mark_range() 624 | self.undo_add(start_row, self.content[start_row:end_row], KEY_NONE, 1, False) 625 | self.content[start_row] = ( 626 | self.content[start_row][:start_col] + self.content[end_row - 1][end_col:] 627 | ) 628 | if start_row + 1 < end_row: 629 | del self.content[start_row + 1 : end_row] ## delete the ramining area 630 | self.col = start_col 631 | 632 | if self.content == []: ## if all was wiped 633 | self.content = [""] ## add a line 634 | self.undo[-1][1] = 1 ## tell undo to overwrite this single line 635 | self.total_lines = len(self.content) 636 | self.cur_line = start_row 637 | self.mark = None ## unset line mark 638 | 639 | def handle_edit_keys(self, key, char): ## keys which change content 640 | l = self.content[self.cur_line] 641 | if key == KEY_NONE: ## character to be added 642 | self.col = self.vcol 643 | if self.mark is not None: 644 | self.delete_mark(False) 645 | l = self.content[self.cur_line] 646 | chain = True 647 | else: 648 | chain = False 649 | self.undo_add(self.cur_line, [l], 0x20 if char == " " else 0x41, 1, chain) 650 | self.content[self.cur_line] = l[: self.col] + char + l[self.col :] 651 | self.col += len(char) 652 | return key ## return here for a marginally faster paste 653 | elif key == KEY_SHIFT_CTRL_LEFT: 654 | self.set_mark() 655 | key = KEY_WORD_LEFT 656 | elif key == KEY_SHIFT_CTRL_RIGHT: 657 | self.set_mark() 658 | key = KEY_WORD_RIGHT 659 | elif key == KEY_MOUSE: ## Set Cursor or open file/find 660 | if char[2] == 0x22: ## right click opens find or get 661 | key = KEY_GET if self.is_dir else KEY_FIND 662 | elif char[1] < Editor.height: 663 | col = char[0] + self.margin 664 | line = char[1] + self.top_line 665 | if (col, line) == ( 666 | self.col, 667 | self.cur_line, 668 | ): ## click at the cursor -> double blick 669 | if ( 670 | self.mark is None 671 | and col < len(l) 672 | and self.issymbol(l[col], Editor.word_char) 673 | ): 674 | self.col = self.skip_while(l, col, Editor.word_char, -1) + 1 675 | self.set_mark() 676 | self.col = self.skip_while(l, self.col, Editor.word_char, 1) 677 | else: ## toggle single char mark 678 | key = KEY_MARK 679 | else: 680 | if self.mark is not None: 681 | if ( 682 | self.mark_order(self.cur_line, self.col) * self.mark_order(line, col) 683 | < 0 684 | ): 685 | self.mark = self.cur_line, self.col 686 | self.cur_line, self.col = ( 687 | line, 688 | col, 689 | ) ## start new if/elif sequence, since the value of key might have changed 690 | if key == KEY_DOWN: 691 | self.move_down() 692 | elif key == KEY_UP: 693 | self.move_up() 694 | elif key == KEY_LEFT: 695 | self.move_left() 696 | elif key == KEY_RIGHT: 697 | self.move_right(l) 698 | elif key == KEY_WORD_LEFT: 699 | self.col = self.vcol 700 | if self.skip_up(): 701 | l = self.content[self.cur_line] 702 | pos = self.skip_until(l, self.col - 1, Editor.word_char, -1) 703 | self.col = self.skip_while(l, pos, Editor.word_char, -1) + 1 704 | elif key == KEY_WORD_RIGHT: 705 | if self.skip_down(l): 706 | l = self.content[self.cur_line] 707 | pos = self.skip_until(l, self.col, Editor.word_char, 1) 708 | self.col = self.skip_while(l, pos, Editor.word_char, 1) 709 | elif key == KEY_DELETE: 710 | self.col = self.vcol 711 | if self.mark is not None: 712 | self.delete_mark(False) 713 | elif self.col < len(l): 714 | self.undo_add(self.cur_line, [l], KEY_DELETE) 715 | self.content[self.cur_line] = l[: self.col] + l[self.col + 1 :] 716 | elif (self.cur_line + 1) < self.total_lines: ## test for last line 717 | self.undo_add(self.cur_line, [l, self.content[self.cur_line + 1]], KEY_NONE) 718 | self.content[self.cur_line] = l + ( 719 | self.content.pop(self.cur_line + 1).lstrip() 720 | if Editor.autoindent == "y" and self.col > 0 721 | else self.content.pop(self.cur_line + 1) 722 | ) 723 | self.total_lines -= 1 724 | elif key == KEY_BACKSPACE: 725 | self.col = self.vcol 726 | if self.mark is not None: 727 | self.delete_mark(False) 728 | elif self.col > 0: 729 | self.undo_add(self.cur_line, [l], KEY_BACKSPACE) 730 | self.content[self.cur_line] = l[: self.col - 1] + l[self.col :] 731 | self.col -= 1 732 | elif self.cur_line > 0: # at the start of a line, but not the first 733 | self.undo_add(self.cur_line - 1, [self.content[self.cur_line - 1], l], KEY_NONE) 734 | self.col = len(self.content[self.cur_line - 1]) 735 | self.content[self.cur_line - 1] += self.content.pop(self.cur_line) 736 | self.cur_line -= 1 737 | self.total_lines -= 1 738 | elif key == KEY_DEL_WORD: 739 | if self.col < len(l): 740 | pos = self.skip_while(l, self.col, Editor.word_char, 1) 741 | pos += self.spaces(l[pos:]) 742 | if self.col < pos: 743 | self.undo_add(self.cur_line, [l], KEY_DEL_WORD) 744 | self.content[self.cur_line] = l[: self.col] + l[pos:] 745 | elif key == KEY_HOME: 746 | self.col = self.spaces(l) if self.col == 0 else 0 747 | elif key == KEY_END: 748 | ni = len(l.split(Editor.comment_char.strip())[0].rstrip()) 749 | ns = self.spaces(l) 750 | self.col = ni if self.col >= len(l) and ni > ns else len(l) 751 | elif key == KEY_PGUP: 752 | self.cur_line -= Editor.height 753 | elif key == KEY_PGDN: 754 | self.cur_line += Editor.height 755 | elif key == KEY_FIND: 756 | pat = self.line_edit("Find: ", Editor.find_pattern, "_") 757 | if pat: 758 | self.mark = None 759 | self.find_in_file(pat, self.col + 1, self.total_lines) 760 | self.row = Editor.height >> 1 761 | elif key == KEY_FIND_AGAIN: 762 | if Editor.find_pattern: 763 | self.find_in_file(Editor.find_pattern, self.col + 1, self.total_lines) 764 | self.row = Editor.height >> 1 765 | elif key == KEY_GOTO: ## goto line 766 | line = self.line_edit("Goto Line: ", "") 767 | if line: 768 | self.cur_line = int(line) - 1 769 | self.row = Editor.height >> 1 770 | elif key == KEY_FIRST: ## first line 771 | self.cur_line = 0 772 | elif key == KEY_LAST: ## last line 773 | self.cur_line = self.total_lines - 1 774 | self.row = Editor.height - 1 ## will be fixed if required 775 | elif key == KEY_TOGGLE: ## Toggle Autoindent/Search case/ Tab Size, TAB write 776 | pat = self.line_edit( 777 | "Autoindent {}, Search Case {}" 778 | ", Tabsize {}, Comment {}, Tabwrite {}: ".format( 779 | Editor.autoindent, 780 | Editor.case, 781 | self.tab_size, 782 | Editor.comment_char, 783 | self.write_tabs, 784 | ), 785 | "", 786 | ) 787 | try: 788 | res = [i.lstrip().lower() for i in pat.split(",")] 789 | if res[0]: 790 | Editor.autoindent = "y" if res[0][0] == "y" else "n" 791 | if res[1]: 792 | Editor.case = "y" if res[1][0] == "y" else "n" 793 | if res[2]: 794 | self.tab_size = int(res[2]) 795 | if res[3]: 796 | Editor.comment_char = res[3] 797 | if res[4]: 798 | self.write_tabs = "y" if res[4][0] == "y" else "n" 799 | except: 800 | pass 801 | elif key == KEY_SCRLUP: ## 802 | ni = 1 if char is None else 3 803 | if self.top_line > 0: 804 | self.top_line = max(self.top_line - ni, 0) 805 | self.cur_line = min(self.cur_line, self.top_line + Editor.height - 1) 806 | self.scroll_up(ni) 807 | elif key == KEY_SCRLDN: ## 808 | ni = 1 if char is None else 3 809 | if self.top_line + Editor.height < self.total_lines: 810 | self.top_line = min(self.top_line + ni, self.total_lines - 1) 811 | self.cur_line = max(self.cur_line, self.top_line) 812 | self.scroll_down(ni) 813 | elif key == KEY_MATCH: 814 | if self.col < len(l): ## ony within text 815 | brackets = "<{[()]}>" 816 | srch = l[self.col] 817 | i = brackets.find(srch) 818 | if i >= 0: ## found a bracket 819 | match = brackets[7 - i] ## matching bracket 820 | level = 0 821 | way = 1 if i < 4 else -1 ## set direction up/down 822 | i = self.cur_line ## set starting point 823 | c = self.col + way ## one off the current position 824 | lstop = ( 825 | min(self.total_lines, i + Editor.match_span) 826 | if way > 0 827 | else max(-1, i - Editor.match_span) 828 | ) 829 | while i != lstop: 830 | l = self.content[i] 831 | cstop = len(l) if way > 0 else -1 832 | if srch in l or match in l: 833 | while c != cstop: 834 | if l[c] == match: 835 | if level == 0: ## match found 836 | self.cur_line, self.col = i, c 837 | return key ## return here instead of ml-breaking 838 | else: 839 | level -= 1 840 | elif l[c] == srch: 841 | level += 1 842 | c += way 843 | i += way 844 | ## set starting point for the next line. 845 | ## treatment for the first and last line is implicit. 846 | c = 0 if way > 0 else len(self.content[i]) - 1 847 | self.message = "No match in {} lines".format(abs(lstop - self.cur_line)) 848 | elif key == KEY_MARK: 849 | if self.mark is None: 850 | self.set_mark() 851 | else: 852 | self.mark = None 853 | elif key == KEY_SHIFT_DOWN: 854 | self.set_mark() 855 | self.move_down() 856 | elif key == KEY_SHIFT_UP: 857 | self.set_mark() 858 | self.move_up() 859 | elif key == KEY_SHIFT_LEFT: 860 | self.set_mark() 861 | self.move_left() 862 | elif key == KEY_SHIFT_RIGHT: 863 | self.set_mark() 864 | self.move_right(l) 865 | elif key == KEY_ALT_UP: 866 | if self.mark is None: 867 | start_line = self.cur_line 868 | end_line = start_line + 1 869 | else: 870 | start_line, end_line = self.line_range() 871 | if start_line > 0: 872 | self.mark = (self.mark[0] - 1, self.mark[1]) 873 | if start_line > 0: 874 | self.undo_add( 875 | start_line - 1, 876 | self.content[start_line - 1 : end_line], 877 | KEY_NONE, 878 | end_line - start_line + 1, 879 | ) 880 | self.content[start_line - 1 : end_line - 1], self.content[end_line - 1] = ( 881 | self.content[start_line:end_line], 882 | self.content[start_line - 1], 883 | ) 884 | self.move_up() 885 | elif key == KEY_ALT_DOWN: 886 | if self.mark is None: 887 | start_line = self.cur_line 888 | end_line = start_line + 1 889 | else: 890 | start_line, end_line = self.line_range() 891 | if end_line < self.total_lines: 892 | self.mark = (self.mark[0] + 1, self.mark[1]) 893 | ## very special case: cursor at the start of the last line 894 | if self.cur_line == end_line == (self.total_lines - 1): 895 | self.move_left() 896 | if end_line < self.total_lines: 897 | self.undo_add( 898 | start_line, 899 | self.content[start_line : end_line + 1], 900 | KEY_NONE, 901 | end_line - start_line + 1, 902 | ) 903 | self.content[start_line + 1 : end_line + 1], self.content[start_line] = ( 904 | self.content[start_line:end_line], 905 | self.content[end_line], 906 | ) 907 | self.move_down() 908 | elif key == KEY_ENTER: 909 | self.col = self.vcol 910 | self.mark = None 911 | self.undo_add(self.cur_line, [l], KEY_NONE, 2) 912 | self.content[self.cur_line] = l[: self.col] 913 | ni = 0 914 | if Editor.autoindent == "y": ## Autoindent 915 | ni = min(self.spaces(l), self.col) ## query indentation 916 | self.cur_line += 1 917 | self.content[self.cur_line : self.cur_line] = [" " * ni + l[self.col :]] 918 | self.total_lines += 1 919 | self.col = ni 920 | elif key == KEY_TAB: 921 | if self.mark is None: 922 | self.col = self.vcol 923 | self.undo_add(self.cur_line, [l], KEY_TAB) 924 | ni = self.tab_size - self.col % self.tab_size ## determine spaces to add 925 | self.content[self.cur_line] = l[: self.col] + " " * ni + l[self.col :] 926 | self.col += ni 927 | else: 928 | lrange = self.line_range() 929 | self.undo_add( 930 | lrange[0], 931 | self.content[lrange[0] : lrange[1]], 932 | KEY_INDENT, 933 | lrange[1] - lrange[0], 934 | ) ## undo replaces 935 | for i in range(lrange[0], lrange[1]): 936 | if len(self.content[i]) > 0: 937 | self.content[i] = ( 938 | " " * (self.tab_size - self.spaces(self.content[i]) % self.tab_size) 939 | + self.content[i] 940 | ) 941 | elif key == KEY_BACKTAB: 942 | if self.mark is None: 943 | self.col = self.vcol 944 | ni = min( 945 | (self.col - 1) % self.tab_size + 1, self.spaces(l, self.col) 946 | ) ## determine spaces to drop 947 | if ni > 0: 948 | self.undo_add(self.cur_line, [l], KEY_BACKTAB) 949 | self.content[self.cur_line] = l[: self.col - ni] + l[self.col :] 950 | self.col -= ni 951 | else: 952 | lrange = self.line_range() 953 | self.undo_add( 954 | lrange[0], 955 | self.content[lrange[0] : lrange[1]], 956 | KEY_DEDENT, 957 | lrange[1] - lrange[0], 958 | ) ## undo replaces 959 | for i in range(lrange[0], lrange[1]): 960 | ns = self.spaces(self.content[i]) 961 | if ns > 0: 962 | self.content[i] = self.content[i][(ns - 1) % self.tab_size + 1 :] 963 | elif key == KEY_REPLC: 964 | count = 0 965 | pat = self.line_edit("Replace: ", Editor.find_pattern, "_") 966 | if pat: 967 | rpat = self.line_edit( 968 | "With: ", Editor.replc_pattern if Editor.replc_pattern else pat 969 | ) 970 | if rpat is not None: ## start with setting up loop parameters 971 | Editor.replc_pattern = rpat 972 | q = "" 973 | cur_line, cur_col = self.cur_line, self.col ## remember pos 974 | if self.mark is not None: ## Replace in Marked area 975 | (self.cur_line, self.col, end_line, end_col) = self.mark_range() 976 | else: ## replace from cur_line to end 977 | end_line = self.total_lines 978 | end_col = 999999 ## just a large number 979 | self.message = "Replace (yes/No/all/quit) ? " 980 | chain = False 981 | while True: ## and go 982 | ni = self.find_in_file(pat, self.col, end_line) 983 | if ni is not None and ( 984 | self.cur_line != (end_line - 1) or self.col < end_col 985 | ): ## Pattern found 986 | if q != "a": 987 | self.display_window() 988 | key, char = self.get_input() ## Get Char of Fct. 989 | q = char.lower() 990 | if q == "q" or key == KEY_QUIT: 991 | break 992 | elif q in ("a", "y"): 993 | self.undo_add( 994 | self.cur_line, 995 | [self.content[self.cur_line]], 996 | KEY_NONE, 997 | 1, 998 | chain, 999 | ) 1000 | self.content[self.cur_line] = ( 1001 | self.content[self.cur_line][: self.col] 1002 | + rpat 1003 | + self.content[self.cur_line][self.col + ni :] 1004 | ) 1005 | self.col += len(rpat) + (ni == 0) # ugly but short 1006 | count += 1 1007 | chain = True ## delete that line if undo for each replace is preferred. 1008 | else: ## everything else is no 1009 | self.col += 1 1010 | else: ## not found, quit 1011 | break 1012 | self.cur_line, self.col = cur_line, cur_col ## restore pos 1013 | self.message = "'{}' replaced {} times".format(pat, count) 1014 | elif key == KEY_CUT: # delete line or line(s) into buffer 1015 | if self.mark is not None: 1016 | self.delete_mark(True) 1017 | elif key == KEY_COPY: # copy line(s) into buffer 1018 | if self.mark is not None: 1019 | self.yank_mark() 1020 | self.mark = None 1021 | elif key == KEY_PASTE: ## insert buffer 1022 | if Editor.yank_buffer: 1023 | self.col = self.vcol 1024 | if self.mark is not None: 1025 | self.delete_mark(False) 1026 | chain = True ## undo this delete too when undoing paste 1027 | else: 1028 | chain = False ## just undo the paste 1029 | ## save the yank buffer state, complete the first and last line and insert it 1030 | head, tail = Editor.yank_buffer[0], Editor.yank_buffer[-1] ## save the buffer 1031 | Editor.yank_buffer[0] = ( 1032 | self.content[self.cur_line][: self.col] + Editor.yank_buffer[0] 1033 | ) 1034 | Editor.yank_buffer[-1] += self.content[self.cur_line][self.col :] 1035 | ni = 1 if len(Editor.yank_buffer) <= 1 else 1 - len(Editor.yank_buffer) 1036 | self.undo_add( 1037 | self.cur_line, [self.content[self.cur_line]], KEY_NONE, ni, chain 1038 | ) # replace 1039 | self.content[ 1040 | self.cur_line : self.cur_line + 1 1041 | ] = Editor.yank_buffer # insert lines 1042 | Editor.yank_buffer[-1], Editor.yank_buffer[0] = tail, head ## restore the buffer 1043 | 1044 | self.total_lines = len(self.content) 1045 | elif key == KEY_WRITE: 1046 | fname = self.line_edit( 1047 | "Save File: ", self.fname if self.is_dir is False else "", Editor.file_char 1048 | ) 1049 | if fname: 1050 | if fname != self.fname: ## save to a different name, confirm evtl. overwrite 1051 | try: 1052 | open(fname).close() ## if that succeeds, the file exists 1053 | res = self.line_edit("The file exists! Overwrite (y/N)? ", "N") 1054 | if not res or res[0].upper() != "Y": 1055 | return 1056 | except: 1057 | pass 1058 | self.put_file(fname) 1059 | self.fname = fname ## remember (new) name 1060 | self.hash = self.hash_buffer() 1061 | self.changed = "" 1062 | self.is_dir = False 1063 | elif key == KEY_UNDO: 1064 | self.undo_redo(self.undo, self.redo) 1065 | elif key == KEY_REDO: 1066 | self.undo_redo(self.redo, self.undo) 1067 | elif key == KEY_COMMENT: 1068 | if self.mark is None: 1069 | lrange = (self.cur_line, self.cur_line + 1) 1070 | else: 1071 | lrange = self.line_range() 1072 | self.undo_add( 1073 | lrange[0], self.content[lrange[0] : lrange[1]], KEY_COMMENT, lrange[1] - lrange[0] 1074 | ) ## undo replaces 1075 | ni = len(Editor.comment_char) 1076 | for i in range(lrange[0], lrange[1]): 1077 | if self.content[i].strip() != "": ## do not touch empty lines 1078 | ns = self.spaces(self.content[i]) 1079 | if self.content[i][ns : ns + ni] == Editor.comment_char: 1080 | self.content[i] = ns * " " + self.content[i][ns + ni :] 1081 | else: 1082 | self.content[i] = ns * " " + Editor.comment_char + self.content[i][ns:] 1083 | elif key == KEY_REDRAW: 1084 | self.redraw(True) 1085 | return key 1086 | 1087 | def edit_loop(self): ## main editing loop 1088 | if not self.content: ## ensure content 1089 | self.content = [""] 1090 | self.total_lines = len(self.content) 1091 | os.chdir(self.work_dir) 1092 | self.redraw(self.message == "") 1093 | 1094 | while True: 1095 | self.display_window() ## Update & display window 1096 | key, char = self.get_input() ## Get Char of Fct-key code 1097 | self.message = "" ## clear message 1098 | 1099 | key = self.handle_edit_keys(key, char) 1100 | if key == KEY_QUIT: 1101 | if self.hash != self.hash_buffer(): 1102 | res = self.line_edit("File changed! Quit (y/N)? ", "N") 1103 | if not res or res[0].upper() != "Y": 1104 | continue 1105 | self.scroll_region(0) 1106 | self.mouse_reporting(False) ## disable mouse reporting 1107 | self.goto(Editor.height, 0) 1108 | self.clear_to_eol() 1109 | self.undo = [] 1110 | return key 1111 | elif key == KEY_NEXT: 1112 | return key 1113 | elif key == KEY_GET: 1114 | if self.mark is not None: 1115 | self.mark = None 1116 | self.display_window() ## Update & display window 1117 | return key 1118 | 1119 | ## packtabs: replace sequence of space by tab 1120 | def packtabs(self, s): 1121 | sb = StringIO() 1122 | for i in range(0, len(s), 8): 1123 | c = s[i : i + 8] 1124 | cr = c.rstrip(" ") 1125 | if (len(c) - len(cr)) > 1: 1126 | sb.write(cr + "\t") ## Spaces at the end of a section 1127 | else: 1128 | sb.write(c) 1129 | return sb.getvalue() 1130 | 1131 | ## calculate a hash over the content 1132 | def hash_buffer(self): 1133 | res = 0 1134 | for line in self.content: 1135 | res = ((res * 17 + 1) ^ hash(line)) & 0x3FFFFFFF 1136 | return res 1137 | 1138 | ## Get the filetype due to absence of os.stat 1139 | def filetype(self, name): 1140 | for t in os.ilistdir("."): 1141 | if t[0] == name: 1142 | return t[1] 1143 | raise OSError 1144 | 1145 | ## Read file into content 1146 | def get_file(self, fname): 1147 | if fname: 1148 | try: 1149 | self.fname = fname 1150 | if fname in (".", "..") or (self.filetype(fname) & 0x4000): ## Dir 1151 | os.chdir(fname) 1152 | self.work_dir = os.getcwd() # let the os module do the normalization 1153 | self.fname = "/" if self.work_dir == "/" else self.work_dir.split("/")[-1] 1154 | self.content = ["Directory '{}'".format(self.work_dir), ""] + sorted( 1155 | os.listdir(".") 1156 | ) 1157 | self.is_dir = True 1158 | else: 1159 | with open(fname) as f: 1160 | self.content = f.readlines() 1161 | self.write_tabs = False 1162 | i = 0 1163 | for l in self.content: 1164 | self.content[i] = self.expandtabs(l.rstrip()) 1165 | i += 1 1166 | except OSError: 1167 | self.message = "Error: file '" + fname + "' may not exist" 1168 | self.hash = self.hash_buffer() 1169 | 1170 | ## write file 1171 | def put_file(self, fname): 1172 | from os import remove 1173 | 1174 | try: 1175 | remove(fname) 1176 | except: 1177 | pass 1178 | with open(fname, "w") as f: 1179 | for l in self.content: 1180 | f.write(l) 1181 | f.write("\n") 1182 | 1183 | ## expandtabs: hopefully sometimes replaced by the built-in function 1184 | def expandtabs(self, s): 1185 | if "\t" in s: 1186 | self.write_tabs = True 1187 | sb = StringIO() 1188 | pos = 0 1189 | for c in s: 1190 | if c == "\t": ## tab is seen 1191 | sb.write(" " * (8 - pos % 8)) ## replace by space 1192 | pos += 8 - pos % 8 1193 | else: 1194 | sb.write(c) 1195 | pos += 1 1196 | return sb.getvalue() 1197 | else: 1198 | return s 1199 | 1200 | 1201 | def pye_edit(content, tab_size=4, undo=50, io_device=None): 1202 | ## prepare content 1203 | ## test, if the IO class if provided 1204 | if io_device is None: 1205 | print("IO device not defined") 1206 | return 1207 | 1208 | gc.collect() ## all (memory) is mine 1209 | index = 0 1210 | undo = max(4, (undo if type(undo) is int else 0)) # minimum undo size 1211 | current_dir = os.getcwd() ## remember current dir 1212 | if content: 1213 | slot = [] 1214 | for f in content: 1215 | slot.append(Editor(tab_size, undo, io_device)) 1216 | if type(f) == str and f: ## String = non-empty Filename 1217 | try: 1218 | slot[index].get_file(f) 1219 | except Exception as err: 1220 | slot[index].message = "{!r}".format(err) 1221 | else: 1222 | try: 1223 | slot[index].content = [ 1224 | str(_) for _ in f 1225 | ] ## iterable item -> make strings and edit 1226 | except: 1227 | slot[index].content = [str(f)] 1228 | index += 1 1229 | else: 1230 | slot = [Editor(tab_size, undo, io_device)] 1231 | slot[0].get_file(".") 1232 | ## edit 1233 | while True: 1234 | try: 1235 | index %= len(slot) 1236 | key = slot[index].edit_loop() ## edit buffer 1237 | if key == KEY_QUIT: 1238 | if len(slot) == 1: ## the last man standing is kept 1239 | break 1240 | del slot[index] 1241 | elif key == KEY_GET: 1242 | f = slot[index].line_edit("Open file: ", "", Editor.file_char) 1243 | if f is not None: 1244 | slot.append(Editor(tab_size, undo, io_device)) 1245 | index = len(slot) - 1 1246 | slot[index].get_file(f) 1247 | elif key == KEY_NEXT: 1248 | index += 1 1249 | except Exception as err: 1250 | slot[index].message = "{!r}".format(err) 1251 | ## raise 1252 | ## All windows closed, clean up 1253 | Editor.yank_buffer = [] 1254 | ## close 1255 | os.chdir(current_dir) ## restore dir 1256 | return slot[0].content if (slot[0].fname == "") else slot[0].fname 1257 | -------------------------------------------------------------------------------- /shebang: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | -------------------------------------------------------------------------------- /strip.sh: -------------------------------------------------------------------------------- 1 | # !sh 2 | # 3 | cat pye_core.py pye_gen.py | sed "s/\ *#.*$//" | sed "/^$/d" >pye.py 4 | cat pye_xbee.py pye_gen.py | sed "s/\ *#.*$//" | sed "/^$/d" >pye_x3.py 5 | cat shebang pye_core.py pye_ux.py >pye 6 | chmod +x pye 7 | cat pye_core.py pye_win.py >pye_win 8 | mpy-cross -O3 -o pye.mpy pye.py 9 | mpy-cross -O3 -o pye_x3.mpy -msmall-int-bits=31 pye_x3.py 10 | # 11 | -------------------------------------------------------------------------------- /tuning_pye.py: -------------------------------------------------------------------------------- 1 | # 2 | # This is the regex version of find. 3 | def find_in_file(self, pattern, col, end): 4 | Editor.find_pattern = pattern ## remember it 5 | if Editor.case != "y": 6 | pattern = pattern.lower() 7 | try: 8 | rex = re_compile(pattern) 9 | except: 10 | self.message = "Invalid pattern: " + pattern 11 | return None 12 | start = self.cur_line 13 | if (col > len(self.content[start]) or # After EOL 14 | (pattern[0] == '^' and col != 0)): # or anchored and not at BOL 15 | start, col = start + 1, 0 # Skip to the next line 16 | for line in range(start, end): 17 | l = self.content[line][col:] 18 | if Editor.case != "y": 19 | l = l.lower() 20 | match = rex.search(l) 21 | if match: # Bingo 22 | self.cur_line = line 23 | ## Instead of match.span, a simple find has to be performed to get the cursor position. 24 | ## And '$' has to be treated separately, so look for a true EOL match first 25 | if pattern[-1:] == "$" and match.group(0)[-1:] != "$": 26 | self.col = col + len(l) - len(match.group(0)) 27 | else: 28 | self.col = col + l.find(match.group(0)) 29 | return len(match.group(0)) 30 | col = 0 31 | else: 32 | self.message = pattern + " not found (again)" 33 | return None 34 | 35 | # this is the simple version of find 36 | def find_in_file(self, pattern, pos, end): 37 | Editor.find_pattern = pattern # remember it 38 | if Editor.case != "y": 39 | pattern = pattern.lower() 40 | spos = pos 41 | for line in range(self.cur_line, end): 42 | if Editor.case != "y": 43 | match = self.content[line][spos:].lower().find(pattern) 44 | else: 45 | match = self.content[line][spos:].find(pattern) 46 | if match >= 0: # Bingo! 47 | self.col = match + spos 48 | self.cur_line = line 49 | return len(pattern) 50 | spos = 0 51 | else: 52 | self.message = "No match: " + pattern 53 | return None 54 | 55 | def line_edit(self, prompt, default, zap=None): ## better one: added cursor keys and backsp, delete 56 | push_msg = lambda msg: self.wr(msg + "\b" * len(msg)) ## Write a message and move cursor back 57 | self.goto(Editor.height, 0) 58 | self.hilite(1) 59 | self.wr(prompt) 60 | self.wr(default) 61 | self.clear_to_eol() 62 | res = default 63 | pos = len(res) 64 | while True: 65 | key, char = self.get_input() ## Get Char of Fct. 66 | if key == KEY_NONE: ## char to be inserted 67 | if len(prompt) + len(res) < self.width - 2: 68 | res = res[:pos] + char + res[pos:] 69 | self.wr(res[pos]) 70 | pos += len(char) 71 | push_msg(res[pos:]) ## update tail 72 | elif key in (KEY_ENTER, KEY_TAB): ## Finis 73 | self.hilite(0) 74 | return res 75 | elif key in (KEY_QUIT, KEY_COPY): ## Abort 76 | self.hilite(0) 77 | return None 78 | elif key == KEY_LEFT: 79 | if pos > 0: 80 | self.wr("\b") 81 | pos -= 1 82 | elif key == KEY_RIGHT: 83 | if pos < len(res): 84 | self.wr(res[pos]) 85 | pos += 1 86 | elif key == KEY_HOME: 87 | self.wr("\b" * pos) 88 | pos = 0 89 | elif key == KEY_END: 90 | self.wr(res[pos:]) 91 | pos = len(res) 92 | elif key == KEY_DELETE: ## Delete 93 | if pos < len(res): 94 | res = res[:pos] + res[pos+1:] 95 | push_msg(res[pos:] + ' ') ## update tail 96 | elif key == KEY_BACKSPACE: ## Backspace 97 | if pos > 0: 98 | res = res[:pos-1] + res[pos:] 99 | self.wr("\b") 100 | pos -= 1 101 | push_msg(res[pos:] + ' ') ## update tail 102 | elif key == KEY_PASTE: ## Get from content 103 | self.wr('\b' * pos + ' ' * len(res) + '\b' * len(res)) 104 | res = self.getsymbol(self.content[self.cur_line], self.col, zap) 105 | self.wr(res) 106 | pos = len(res) 107 | 108 | def line_edit(self, prompt, default): # simple one: only 4+1 fcts 109 | self.goto(Editor.height, 0) 110 | self.hilite(1) 111 | self.wr(prompt) 112 | self.wr(default) 113 | self.clear_to_eol() 114 | res = default 115 | while True: 116 | key, char = self.get_input() # Get Char of Fct. 117 | if key == KEY_NONE: ## character to be added 118 | if len(prompt) + len(res) < Editor.width - 2: 119 | res += char 120 | self.wr(char) 121 | elif key in (KEY_ENTER, KEY_TAB): # Finis 122 | self.hilite(0) 123 | return res 124 | elif key == KEY_QUIT: # Abort 125 | self.hilite(0) 126 | return None 127 | elif key == KEY_BACKSPACE: # Backspace 128 | if (len(res) > 0): 129 | res = res[:len(res)-1] 130 | self.wr('\b \b') 131 | elif key == KEY_DELETE: # Delete prev. Entry 132 | self.wr('\b \b' * len(res)) 133 | res = '' 134 | 135 | def expandtabs(self, s, tabsize=8): 136 | import _io 137 | if '\t' in s and tabsize > 0: 138 | sb = _io.StringIO() 139 | pos = 0 140 | for c in s: 141 | if c == '\t': # tab is seen 142 | sb.write(" " * (tabsize - pos % tabsize)) # replace by space 143 | pos += tabsize - pos % tabsize 144 | else: 145 | sb.write(c) 146 | pos += 1 147 | return sb.getvalue() 148 | else: 149 | return s 150 | 151 | def packtabs(self, s, tabsize=8): 152 | if tabsize > 0: 153 | sb = _io.StringIO() 154 | for i in range(0, len(s), tabsize): 155 | c = s[i:i + tabsize] 156 | cr = c.rstrip(" ") 157 | if c != cr: # Spaces at the end of a section 158 | sb.write(cr + "\t") # replace by tab 159 | else: 160 | sb.write(c) 161 | return sb.getvalue() 162 | else: 163 | return s 164 | 165 | def cls(self): 166 | self.wr(b"\x1b[2J") 167 | --------------------------------------------------------------------------------