pax_global_header00006660000000000000000000000064147630521060014516gustar00rootroot0000000000000052 comment=88732f79cd7f2f4534c168bb66f36e309372d19c durdraw-0.29.0/000077500000000000000000000000001476305210600132565ustar00rootroot00000000000000durdraw-0.29.0/.dockerignore000066400000000000000000000000051476305210600157250ustar00rootroot00000000000000.git durdraw-0.29.0/.github/000077500000000000000000000000001476305210600146165ustar00rootroot00000000000000durdraw-0.29.0/.github/FUNDING.yml000066400000000000000000000014551476305210600164400ustar00rootroot00000000000000# These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: samfoster # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] durdraw-0.29.0/.gitignore000066400000000000000000000000231476305210600152410ustar00rootroot00000000000000*.pyc __pycache__/ durdraw-0.29.0/CONTRIBUTING.md000066400000000000000000000036061476305210600155140ustar00rootroot00000000000000## Contributing to Durdraw That's awesome that you want to help! I thank you. The guidelines are pretty loose right now. * The main thing is, work in the "dev" branch instead of the "master" branch. The changes made to the Dev branch are eventually merged into Master for release. * Durdraw should always be able to run using the Python standard library, without any dependencies, even if some features are unavailable. That level of portability is important to me, and I think is part of its appeal. I am only willing to break it if there is a very good reason. You can still use a dependency if it is used in a way that does not prevevent Durdraw from running when the dependency is unavailable, but prefer the standard library, please. * If you implement a new feature that changes any behavior, unless it's really cool and makes a lot of sense, try to make it optional. For example, through a command-line option or runtime toggle. * If you want to help "clean up" the code to make it more legible or modular, that's great. But, I request that you let me see some of the style changes you are proposing before you dump a giant patch of changes. I don't want major changes in a style I don't enjoy. * Use Git and Github for development, discussion, pull requests, issues, proposals, etc. You can also contact the developer directly via email (see README.md CREDITS), or you can find us on Discord or IRC (see README.md for details). * If you want to contribute ANSI or ASCII Art, that's great! Please post it in the Discussions on Github and give consent if you are OK with it being used (in the program, website, Readme file, youtube videos, etc). * There is a wish list for desired features here: https://github.com/cmang/durdraw/wiki/Wish-List * Reviews, feedback, tutorials, demonstrations, etc. are also welcome. Post any art you created with Durdraw in the Discussions, because we would love to see them! durdraw-0.29.0/Dockerfile000066400000000000000000000002611476305210600152470ustar00rootroot00000000000000# syntax=docker/dockerfile:1 FROM python:3.12-alpine WORKDIR /durdraw COPY . . RUN pip install --upgrade . #RUN ./installconf.sh ENV TERM=xterm-256color ENTRYPOINT ["durdraw"] durdraw-0.29.0/LICENSE000066400000000000000000000027371476305210600142740ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2009-2025, Sam Foster Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. durdraw-0.29.0/README.md000066400000000000000000000623011476305210600145370ustar00rootroot00000000000000Durdraw ======= __ __ _| |__ __ _____ __| |_____ _____ __ __ __ / _ | | | __| _ | __| _ | | | |\ /_____|_____|__|__|_____|__|___\____|________| | \_____________________________________________\| v 0.29.0 ![durdraw-0 28-demo](https://github.com/user-attachments/assets/3bdb0c46-7f21-4514-9b48-ac00ca62e68e) ## Overview Durdraw is an ASCII, Unicode and ANSI art editor for UNIX-like systems (Linux, macOS, etc). It runs in modern Utf-8 terminals and supports frame-based animation, custom themes, 256 and 16 color modes, terminal mouse input, DOS ANSI art viewing, CP437 and Unicode mixing and conversion, HTML output, mIRC color output, and other interesting features. Durdraw is heavily inspired by classic ANSI editing software for MS-DOS and Windows, such as TheDraw, Aciddraw and Pablodraw, but with a modern Unix twist. - [Overview](#overview) - [Requirements](#requirements) - [Installation](#installation) - [Gallery](#gallery) - [Usage](#usage) - [Command Line](#command-line) - [Interactive Usage/Editing](#interactive-usageediting) - [Configuration](#configuration) - [Durfetch](#durfetch) - [FAQ](#faq) - [Other](#other) --- ## Requirements * Python 3 (3.10+ recommended) * Linux, macOS, or other Unix-like System **Optional Requirements** 1. `ansilove` For PNG and animated GIF export, please install `ansilove` (https://ansilove.org/) and make sure it is is in your path. _PNG and GIF export only works in 16-color mode for now, and only with CP437 compatible characters._ 2. `neofetch` For [durfetch](#durfetch) support, please install `neofetch` and place it in your path. ## Installation You can install `durdraw` via several methods: - [Via OS Repositories](#via-os-repositories) - [Via Source Repository](#via-source-repository) - [Via pip](#via-pip) After installing, you should be able to run `durdraw`. Press `esc-h` for help, or try `durdraw --help` for [command-line options](#command-line-usage). _If you just want to run it without installing, see [Running Without Installing](#running-without-installing). You can also install via the included Dockerfile._ ### Via OS Repositories [![Packaging status](https://repology.org/badge/vertical-allrepos/durdraw.svg)](https://repology.org/project/durdraw/versions) ### Via Source Repository 1. Download and extract, or use git to download: ```shell git clone https://github.com/cmang/durdraw.git cd durdraw ``` 2. Install or upgrade using pip: ```shell python3 -m pip install --upgrade . ``` 3. Optionally, install some themes and a sample configuration file for your local user into `~/.durdraw/`: ```shell ./installconf.sh ``` This will place durdraw.ini into `~/.durdraw/` and the themes into `~/.durdraw/themes/`. ### Via pip Alternatively, you can install the pip package pip directly (_Please note that this installation method does not include everything, i.e. the example dur files under `examples/`_, or the entrypoint scripts in the section below) ```shell # install `master` branch version: python3 -m pip install 'git+https://github.com/cmang/durdraw' # install specific version: python3 -m pip install 'git+https://github.com/cmang/durdraw@0.28.0' # install `dev` branch version: python3 -m pip install 'git+https://github.com/cmang/durdraw@dev' ``` ### Running Without Installing You can run Durdraw with: ```shell ./start-durdraw ``` To look at some included example animations: ```shell ./start-durdraw -p examples/*.dur ``` ## Gallery ### Tutorials | | | |-|-| | [![Watch the Tutorial Part 1](https://github.com/cmang/durdraw/assets/261501/ca33c81b-0559-4fc7-a49b-a11768938d3d)](https://youtu.be/vWczO0Vd_54) | [![Watch another video](https://durdraw.org/durdraw-youtube-thumbnail-with-play-button.png)](https://youtu.be/7Icf08bkJxg) | ### Screenshots | | | |-|-| | ![durdraw-xmas-example](https://github.com/cmang/durdraw/assets/261501/4137eb2d-0de0-47ca-8789-cca0c8519e91) | ![dopetrans3](https://user-images.githubusercontent.com/261501/210064369-4c416e85-12d0-47aa-b182-db5435ae0c78.gif) | | ![durdraw-screenshot](https://user-images.githubusercontent.com/261501/142838691-9eaf58b0-8a1f-4636-a41a-fe8617937d1d.gif) | ![durdraw-linux-unicode-ansi](https://user-images.githubusercontent.com/261501/161380487-ac6e2b5f-d44a-493a-ba78-9903a6a9f1ca.png) | | ![cm-doge](https://user-images.githubusercontent.com/261501/210064365-e9303bee-7842-4068-b356-cd314341098b.gif) | ![bsd-color-new](https://user-images.githubusercontent.com/261501/210064354-5c1c2adc-06a3-43c5-8e21-30b1a81db315.gif) | ## Usage ### Command Line You can play a `.dur` file or series of `.dur` (or `.ANS` or `.ASC`) files with: ```shell durdraw -p filename.dur durdraw -p file1.dur file2.dur file3.dur ... ``` Or view a downloaded ANSI artpack with: ```shell durdraw -p *.DIZ *.ASC *.ANS ``` Other command-line options: ``` usage: durdraw [-h] [-p PLAY [PLAY ...]] [-d DELAYEXIT] [-x TIMES] [--256color | --16color] [-b] [-W WIDTH] [-H HEIGHT] [-m] [--wrap WRAP] [--nomouse] [--cursor CURSOR] [--notheme] [--theme THEME] [--cp437] [--export-ansi] [-u UNDOSIZE] [--fetch] [-V] [filename] positional arguments: filename .dur or ascii file to load options: -h, --help show this help message and exit -p PLAY [PLAY ...], --play PLAY [PLAY ...] Just play .dur, .ANS or .ASC file or files, then exit -d DELAYEXIT, --delayexit DELAYEXIT Wait X seconds after playback before exiting (requires -p) -x TIMES, --times TIMES Play X number of times (requires -p) --256color Try 256 color mode --16color Try 16 color mode -b, --blackbg Use a black background color instead of terminal default -W WIDTH, --width WIDTH Set canvas width -H HEIGHT, --height HEIGHT Set canvas height -m, --max Maximum canvas size for terminal (overrides -W and -H) --wrap WRAP Number of columns to wrap lines at when loading ASCII and ANSI files (default 80) --nomouse Disable mouse support --cursor CURSOR Cursor mode (block, underscore, or pipe) --notheme Disable theme support (use default theme) --theme THEME Load a custom theme file --cp437 Display extended characters on the screen using Code Page 437 (IBM-PC/MS-DOS) encoding instead of Utf-8. (Requires CP437 capable terminal and font) (beta) --export-ansi Export loaded art to an .ansi file and exit -u UNDOSIZE, --undosize UNDOSIZE Set the number of undo history states - default is 100. More requires more RAM, less saves RAM. --fetch Replace fetch strings with Neofetch output -V, --version Show version number and exit ``` ### Interactive Usage/Editing - Use the arrow keys (or mouse) and other keys to edit, much like a text editor. - You can click highlighted areas of the screen. - You can use the "Esc" (or "Meta") key to access keyboard shortcuts and commands: ``` ____________. _________ __________ _________ _____ _______ .-\\___ / |______/ _ /.-\\___ // _ /_/ _ \_.____. \ / | |/ / | / / /:| |/ / / /Y Y Y | / / | / /| | / _ _/ || / /: _ _/ : _ | /\/ / | /:| : : Y |: /:| Y | Y | /:H7 |____ |_________|___| |_____ |____| | |____|____/\_____| .-- `-----' ----------- `------': - `-----' -- `------'----' -----------------. | | `-----------------------------------------------------------------------------' .. Art Editing ..................... .. Animation ....................... : F1-F10 - insert character : : esc-k - next frame : : esc-1 to esc-0 - same as F1-F10 : : esc-j - previous frame : : esc-space - insert draw char : : esc-p - start/stop payback : : esc-c/tab - color picker : : esc-n - clone frame : : esc-left - next fg color : : esc-N - append empty frame : : esc-right - prev fg color : : esc-d - delete frame : : esc-up - change color up : : esc-D - set frame delay : : esc-down - change color down : : esc-+/esc-- - faster/slower : : esc-' - insert line : : esc-R - set playback/edit range : : esc-; - delete line : : esc-g - go to frame # : : esc-. - insert column : : esc-M - move frame : : esc-, - delete column : : esc-{ - shift frames left : : esc-] - next character group : : esc-} - shift frames right : : esc-[ - previous character group : :..................................: : esc-S - change character set : : esc-L - replace color : .. UI/Misc ......................... : esc-y - eyedrop (pick up color) : : esc-m - main menu : : esc-P - pick up character : : esc-a - animation menu : : esc-l - color character : : esc-t - mouse tools : : shift-arrows - select for copy : : esc-z - undo : : esc-K - mark selection : : esc-r - redo : : esc-v - paste : : esc-V - view mode : :..................................: : esc-i - file/canvas info : : esc-I - character inspector : .. File Operations ................. : esc-F - search/find string : : esc-C - new/clear canvas : : ctrl-l - redraw screen : : esc-o - open : : esc-h - help : : esc-s - save : : esc-q - quit : :..................................: :..................................: .. Canvas Size ..................... : esc-" - insert line : : esc-: - delete line : : esc-> - insert column : : esc-< - delete column : :..................................: esc-j esc-k Prev Next Canvas esc-f esc-g esc-- Frame Frame Size esc-m Go to esc-+ esc-D esc-R esc-t | esc-p| | Main Frame Speed Frame Play/Edit Mouse First | Play/| Last | Menu Number | Delay Range Tools Frame | Pause| Frame | | | | | | | | | | | | | [Menu] F: 1/7 : 8 D: 0.00 R: 1/8 [Move] |< << |> >> >| [80x24] tab esc-c esc-S Pick esc-[ esc-] Charset set F1-F10 esc-[ esc-] Foreground Character or Unicode Insert Special Prev/Next Cursor Color Group Block Characters Char Group Position | | | | \ | FG:██ (1/21) [Dur..] (12,10) ANIMATION: Use the Animation Menu [Anim] or keyboard commands to insert (esc-n), delete (esc-d), move (esc-M) and edit frames. Use esc-k and esc-j to flip to the next and previous frames. The "Play" button (|> or esc-p) starts or stops playback. When the animation is playing, all changes made effect all frames within the current playback/edit Range (R: or esc-R). Change playback speed ( or Frames Per Second) with esc-+ (or esc-=) and esc--. F: shows the current frame number, and you can go to a specific frame with esc-g. BRUSHES: To make a brush, use shift-arrow or esc-K to make a selection, then press b. To use the brush, click the Mouse Tools menu (esc-t) and select Paint (P). You can now use the mouse to paint with your custom brush. ``` ### Configuration - [Example Themes](#example-themes) - [Color Options](#color-options) - [Theme Options](#theme-options) - [Custom Character Sets](#custom-character-sets) You can create a custom startup file where you can set a theme and other options. > If you did not already do so during installation, you can install a sample configuration (_See step 3 under[Installation via Source Repository](#via-source-repository)_) Here is an example `durdraw.ini` file, showing the available options: ```ini ; Durdraw 0.28.0 Configuration File [Main] ; color-mode sets the color mode to start in. Available options: 16, 256 ;color-mode: 16 ; disable-mouse disables the mouse. ;disable-mouse: True ; max-canvas automatically sets the canvas size to the terminal window size at startup. ;max-canvas: True ; Cursor mode requests a cursor type from the terminal. Available options: block, underscore, pipe ;cursor-mode: underscore ; When scroll-colors is enabled, using the mouse wheel in the canvas changes the ; foreground color instead of moving the cursor. ;scroll-colors: True [Theme] theme-16: ~/.durdraw/themes/mutedchill-16.dtheme.ini theme-256: ~/.durdraw/themes/mutedform-256.dtheme.ini [Logging] ; filepath is the path to the log file. Default is ./durdraw.log ;filepath: ./durdraw.log ; level sets the logging level. Available options: DEBUG, INFO, WARNING, ERROR, CRITICAL. Default is WARNING ;level: WARNING ; local-tz indicates if the computer's local timezone should be used when logging instead of UTC ;local-tz: False ``` The option `'theme-16'` sets the path to the theme file used in 16-color mode, and `'theme-256'` sets the theme file used for 256-color mode. You can also load a custom theme file using the `--theme` command-line argument and passing it the path to a theme file, or disable themes entirely with the `--notheme` command line option. #### Example Themes
16-color theme 256-color theme
```ini [Theme-16] name: 'Purple Drank' mainColor: 6 clickColor: 3 borderColor: 6 clickHighlightColor: 5 notificationColor: 4 promptColor: 4 ``` ```ini [Theme-256] name: 'Muted Form' mainColor: 104 clickColor: 37 borderColor: 236 clickHighlightColor: 15 notificationColor: 87 promptColor: 189 menuItemColor: 189 menuTitleColor: 159 menuBorderColor: 24 ```
#### Color Options > (_for 16-color mode - color codes numbers for 256-color mode can be found in Durdraw's 256-color selector._) | code | color | |------|---------| | 1 | black | | 2 | blue | | 3 | green | | 4 | cyan | | 5 | red | | 6 | magenta | | 7 | yellow | | 8 | white | #### Theme Options | theme option | description | |---------------------|-------------| | mainColor | the color of most text | | clickColor | the color of buttons (clickable items) | | clickHighlightColor | the color the button changes to for a moment when clicked | | borderColor | the color of the border around a drawing | | notificationColor | the color of notification messages | | promptColor | the color of user prompt messages | | menuItemColor | the color of menu items | | menuTitleColor | the color of menu titles | | menuBorderColor | the color of the border around menus | #### Custom Character Sets You can create custom character set files and place them in the ~/.durdraw/charsets folder. Character set files must have the file nme extension .ini. Durdraw will automatically scan for these files and include them in the list of character sets (esc-S). An example character set file is provided in example-charset.ini: ```ini ; Custom character set ; 🭨 🭩 🭪 🭫 🭬🭭🭮 🭯 🮚 🮛 🮜 🮝 🮞 🮟 ◤ ◥ ◢ ◣ [Character Set] name: Cool Characters encoding: utf-8 [block 1] f1: 🭨 f2: 🭩 f3: 🭪 f4: 🭫 f5: 🭬 f6: 🭭 f7: 🭮 f8: 🭯 f9: 🮚 f10: 🮛 [block 2] f1: 🮜 f2: 🮝 f3: 🮞 f4: 🮟 f5: ◤ f6: ◥ f7: ◢ f8: ◣ f9: f10: ``` ## Durfetch `durfetch` is a program which acts like a fetcher. It uses Neofetch to obtain system statistics and requires that Neofetch be found in the path. You can put keys in your `.dur` files which `durfetch` will replace with values from Neofetch. You can also use built-in example animations. Note that this feature is in beta, and is far from perfect, but it can be fun to play with. If anyone wants to improve `durfetch`, please feel free. Keys will only be replaced if there is enough room in the art for the replacement value. The following values can be used in your art and automatically interpreted by `durfetch`: ```yaml {OS} {Host} {Kernel} {Uptime} {Packages} {Shell} {Resolution} {DE} {WM} {WM Theme} {Terminal} {Terminal Font} {CPU} {GPU} {Memory} ``` The `durfetch` executable takes the following command-line parameters: ```shell usage: durfetch [-h] [-r | -l LOAD] [--linux | --bsd] [filename ...] An animated fetcher. A front-end for Durdraw and Neofetch integration. positional arguments: filename .dur ASCII and ANSI art file or files to use options: -h, --help show this help message and exit -r, --rand Pick a random animation to play -l LOAD, --load LOAD Load an internal animation --linux Show a Linux animation --bsd Show a BSD animation Available animations for -l: bsd cm-eye linux-fire linux-tux unixbox ``` Here are some `durfetch` examples: | | | |-|-| | ![tux-fetch-colors](https://github.com/user-attachments/assets/4010d18a-1b79-4594-a9cd-17234584f3c8) | ![unixy3](https://github.com/user-attachments/assets/812514d4-0216-4f41-8384-84563fa664b7) | ## Experimental Features To enable an external feature, use an ENV var listed below in front of a `durdraw` command, e.g. ```shell ENABLE_UNDO_TEMPFILES=1 durdraw animation.dur ``` The following list of features are experimental and may not work as expected: ```shell ENABLE_UNDO_TEMPFILES=1 # store undo history using python the `tmpfile` lib instead of memory ``` ## Development ### Testing To run the test, you need to have `pytest` installed. You can install it with: ```shell python3 -m pip install pytest ``` Then you can run the tests with: ```shell pytest -vv test/ ``` ## FAQ #### Q: Durdraw crashed! What do I do? A: Oh no! I am sorry and hope nothing important was lost. But you can help fix it. Please take a screenshot of the crash and post it as a bug report at https://github.com/cmang/durdraw/issues/. Please try to describe what you were trying to do when it happened, and if possible, include the name of your terminal, OS and Python version. I will do my best to try to fix it ASAP. Your terminal will probably start acting weird if Durdraw crashed. You can usually fix it by typing "reset" and pressing enter. #### Q: Don't TheDraw and some other programs already do ANSI animation? A: Yes, but traditional ANSI animation does not provide any control over timing, instead relying on terminal baud rate to govern the playback speed. This does not work well on modern systems without baud rate emulation. Durdraw gives the artist fine control over frame rate, and delays per frame. Traditional ANSI animation also updates the animation one character at a time, while Durdraw updates the animation a full frame at a time. This makes it less vulnerable to visual corruption from things like errant terminal characters, resized windows, line noise, etc. Finally, unlike TheDraw, which requires MS-DOS, Durdraw runs in modern Unicode terminals. #### Q: Can I run Durdraw in Windows? A: Short answer: It's not supported, but it seems to work fine in the Windows Subsystem for Linux (WSL), and in Docker using the provided Dockerfile. Long answer: Some versions run fine in Windows Command Prompt, Windows Terminal, etc, without WSL, but it's not tested or supported. If you want to help make Durdraw work better in Windows, please help by testing, submitting bug reports and submitting patches. #### Q: Can I run Durdraw on Amiga, MS-DOS, Classic MacOS, iOS, Android, Atari ST, etc? A: Probably not easily. Durdraw requires Python 3 and Ncurses. If your platform can support these, it will probably run. However, the file format for Durdraw movies is a plain text JSON format. It should be possible to support this format in different operating systems and in different applications. See `durformat.md` for more details on the `.dur` file format. #### Q: Does Durdraw support IBM-PC ANSI art? A: Yes! IBM-PC ANSI art popular in the "ANSI Art Scene" uses Code Page 437 character encoding, which usually needs to be translated to work with modern terminals. When Durdraw encounters these files, it will convert them to Unicode and carry on. When you save ANSI files, it will ask if you want to use CP437 or Utf-8 encoding. #### Q: I only see 8 colors in 16 color mode. Why? A: Look in your terminal setting for "Use bright colors for bold," or a similarly named option. Durdraw's 16-color mode, like many vintage terminals (including MS-DOS), uses the Bold escape codes to tell the terminal that colors are "bright." This provides compatibility with many older systems. However, some terminals do not support or enable this option by default. Additionally, your terminal decides what colors to assign to the lower 16 colors. In many terminals, Durdraw can override the default 16 color palette. To do this, click on Menu -> Settings and select VGA, Commodore 64 or ZX Spectrum colors. #### Q: Some or all of the F1-F10 keys do not work for me! What can I do? A: You can use ESC-1 through ESC-0 as a replacement for F1-F10. Some terminals will map this to Alt-1 through Alt-0. You can also use the following settings in some terminals to enable the F1-F10 keys: - **GNOME Terminal**: **Click**: Menu -> Edit -> Preferences -> General, and **uncheck** the box: - [ ] Enable the menu accelerator key (F10 by default) - **Xfce4-Terminal**: **Click**: Menu -> Edit -> Preferences -> Advanced, and **check** the 2 boxes: - [x] Disable menu shortcut key (F10 by default) - [x] Disable help window shortcut key (F1 by default) ## Other - [Links, Media \& Thanks](#links-media--thanks) - [Support](#support) - [Community](#community) - [Credits](#credits) - [Legal](#legal) ### Links, Media & Thanks Special thanks to the following individuals and organizations for featuring Durdraw in their content: - Linux Magazine - https://www.linux-magazine.com/Issues/2024/281 - Linux Voice Magazine - https://archive.org/details/LinuxVoice/Linux-Voice-Issue-015/page/n71/mode/2up - Bryan Lunduke at The Lunduke Journal - https://lunduke.locals.com/post/5327347/durdraw-like-thedraw-but-linux - Korben - https://korben.info/editeur-ansi-ascii-unicode-durdraw-creer-art-terminal.html - Jill Bryant and Venn Stone at Linux Game Cast - https://www.youtube.com/watch?v=HvZXkqg2vec&t=568s - LinuxLinks - https://www.linuxlinks.com/durdraw-ascii-unicode-ansi-art-editor/ - Harald Markus Wirth (hmw) has made a Web `.dur` Player in JavaScript: https://harald.ist.org/stubs/webdurplayer/ If you write, podcast, vlog, or create content about Durdraw, or if you simply enjoy using it, I'd love to hear from you! Please reach out to me via the GitHub project page or at samfoster@gmail.com. ### Support Thank you for considering a contribution to help sustain and enhance this project. Financial contributions help cover essential costs like development time, domain registration, and web hosting. You can donate to this project using any of these platforms: - [Paypal](https://www.paypal.com/donate/?hosted_button_id=VTPZPFMDLY4X6) - [Buymeacoffee](https://buymeacoffee.com/samfoster) - [Patreon](https://patreon.com/SamFoster) Other ways to support Durdraw include reporting bugs, providing feedback, and contributing code. Please refer to the CONTRIBUTING.md file for information and guidelines. If you need assistance or have questions about Durdraw, feel free to reach out to us on GitHub. We're happy to help! ### Community There are community discussions on Github, where people post art made with Durdraw. Check it out: https://github.com/cmang/durdraw/discussions We also have a Discord server for Durdraw users. Join us: https://discord.gg/9TrCsUrtZD If you are feeling really old school, you can try the #durdraw IRC channel on irc.libera.chat. ### Credits - Home page: http://durdraw.org - Development: https://github.com/cmang/durdraw Durdraw is what it is thanks to the following people: - Sam Foster - Creator, primary developer - Tom McKeesick - Performnace enhancements, documentation formatting - Alex Myczko - Man page, Debian ambassador - sigurdo - Cursor shapes, command-line ANSI export - yumpyy - Dockerfile - Zhenrong Wang - Documentation updates - Frederick Cambus - Documentation update - eyooooo - Filename conventions, useful feedback - HK - Beta testing, useful feedback - ANSI and ASCII artists: `cmang`, `H7`, `LDA`, `HK` ### Legal Durdraw is Copyright (c) 2009-2025 Sam Foster . All rights reserved. The BSD Daemon is Copyright 1988 by Marshall Kirk McKusick. This software is distributed under the BSD 3-Clause License. See LICENSE file for details. durdraw-0.29.0/durdraw.1000066400000000000000000000057611476305210600150210ustar00rootroot00000000000000.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. .\" I edited it. Bad boys, bad boys. -Sam .\" __ __ .\" _| |__ __ _____ __| |_____ _____ __ __ __ .\" / _ | | | __| _ | __| _ | | | |\ .\" /_____|_____|__|__|_____|__|___\____|________| | .\" \_____________________________________________\| .TH DURDRAW "1" "February 2025" "durdraw 0.29.0" "User Commands" .SH NAME durdraw \- versatile ASCII and ANSI Art text editor for drawing in terminal .SH SYNOPSIS .br .B durdraw [\-h] [\-p PLAY [PLAY ...]] [\-\-startup | \fB\-w\fR | \fB\-x\fR TIMES] [\-\-256color | \fB\-\-16color]\fR [\-b] [\-W WIDTH] [\-H HEIGHT] [\-m] [\-\-nomouse] [\-\-cursor CURSOR] [\-\-notheme] [\-\-theme THEME] [\-\-cp437] [\-\-export\-ansi] [\-u UNDOSIZE] [\-V] [filename] .SH DESCRIPTION Durdraw is an ASCII, ANSI and Unicode art editor for UNIX-like systems (Linux, macOS, etc). It runs in modern Utf-8 terminals and supports frame-based animation, custom themes, 256 and 16 color modes, terminal mouse input, DOS ANSI art viewing, CP437 and Unicode mixing and conversion, HTML output, mIRC color output, and other interesting features. .PP Durdraw is heavily inspired by classic ANSI editing software for MS-DOS and Windows, such as TheDraw, Aciddraw and Pablodraw, but with a modern Unix twist. .PP .SS "positional arguments:" .TP filename \&.dur or ascii file to load .SS "options:" .TP \fB\-h\fR, \fB\-\-help\fR show this help message and exit .TP \fB\-p\fR PLAY [PLAY ...], \fB\-\-play\fR PLAY [PLAY ...] Just play .dur file or files, then exit .TP \fB\-\-startup\fR Show startup screen .TP \fB\-w\fR, \fB\-\-wait\fR Pause at startup screen .TP \fB\-x\fR TIMES, \fB\-\-times\fR TIMES Play X number of times (requires \fB\-p\fR) .TP \fB\-\-256color\fR Try 256 color mode .TP \fB\-\-16color\fR Try 16 color mode .TP \fB\-b\fR, \fB\-\-blackbg\fR Use a black background color instead of terminal default .TP \fB\-W\fR WIDTH, \fB\-\-width\fR WIDTH Set canvas width .TP \fB\-H\fR HEIGHT, \fB\-\-height\fR HEIGHT Set canvas height .TP \fB\-m\fR, \fB\-\-max\fR Maximum canvas size for terminal (overrides \fB\-W\fR and \fB\-H\fR) .TP \fB\-\-nomouse\fR Disable mouse support .TP \fB\-\-cursor\fR CURSOR Cursor mode (block, underscore, or pipe) .TP \fB\-\-notheme\fR Disable theme support (use default theme) .TP \fB\-\-theme\fR THEME Load a custom theme file .TP \fB\-\-cp437\fR Display extended characters on the screen using Code Page 437 (IBM\-PC/MS\-DOS) encoding instead of Utf\-8. (Requires CP437 capable terminal and font) (beta) .TP \fB\-\-export\-ansi\fR Export loaded art to an .ansi file and exit .TP \fB\-u\fR UNDOSIZE, \fB\-\-undosize\fR UNDOSIZE Set the number of undo history states \- default is 100. More requires more RAM, less saves RAM. .TP \fB\-V\fR, \fB\-\-version\fR Show version number and exit .SH "SEE ALSO" .B durfetch, .B durview .SH AUTHOR Durdraw is primarily written by Sam Foster . For a full list of contributors, please see the Github page: https://github.com/cmang/durdraw durdraw-0.29.0/durdraw.ini000066400000000000000000000021031476305210600154230ustar00rootroot00000000000000; Durdraw 0.29.0 Configuration File [Main] ; color-mode sets the color mode to start in. Available options: 16, 256 ;color-mode: 16 ; disable-mouse.. disablse the mouse. ;disable-mouse: True ; max-canvas atuomatically sets the canvas size to the terminal window size at startup. ;max-canvas: True ; Cursor mode requests a cursor type from the terminal. Available options: block, underscore, pipe ;cursor-mode: underscore ; When scroll-colors is enabled, using the mouse wheel in the canvas changes the ; foreground color instead of moving the cursor. ;scroll-colors: True [Theme] ; theme-16: ~/.durdraw/themes/purpledrank-16.dtheme.ini theme-16: ~/.durdraw/themes/mutedchill-16.dtheme.ini theme-256: ~/.durdraw/themes/mutedform-256.dtheme.ini [Logging] ; filepath is the path to the log file. Default is ./durdraw.log ;filepath: ./durdraw.log ; level sets the logging level. Available options: DEBUG, INFO, WARNING, ERROR, CRITICAL. Default is WARNING ;level: WARNING ; local-tz indicates if the computer's local timezone should be used when logging instead of UTC ;local-tz: False durdraw-0.29.0/durdraw/000077500000000000000000000000001476305210600147265ustar00rootroot00000000000000durdraw-0.29.0/durdraw/__init__.py000066400000000000000000000000001476305210600170250ustar00rootroot00000000000000durdraw-0.29.0/durdraw/charsets/000077500000000000000000000000001476305210600165425ustar00rootroot00000000000000durdraw-0.29.0/durdraw/charsets/cp437-charset.ini000066400000000000000000000027141476305210600215360ustar00rootroot00000000000000; IBM-PC/MS-DOS Code Page 437 Compatible character set for Durdraw ; Order copied from TheDraw [Character Set] name: Code Page 437 (IBM-PC/MS-DOS) encoding: utf-8 [block 1] f1: ┌ f2: ┐ f3: └ f4: ┘ f5: ─ f6: │ f7: ├ f8: ┤ f9: ┴ f10: ┬ [block 2] f1: ╔ f2: ╗ f3: ╚ f4: ╝ f5: ═ f6: ║ f7: ╠ f8: ╣ f9: ╩ f10: ╦ [block 3] f1: ╒ f2: ╕ f3: ╘ f4: ╛ f5: ═ f6: │ f7: ╞ f8: ╡ f9: ╧ f10: ╤ [block 4] f1: ╓ f2: ╖ f3: ╙ f4: ╜ f5: ─ f6: ║ f7: ╟ f8: ╢ f9: ╨ f10: ╥ [block 5] f1: ┼ f2: ╬ f3: ╪ f4: ╫ f5: Φ f6: Θ f7: ¢ f8: £ f9: Ö f10: ∩ [block 6] f1: ░ f2: ▒ f3: ▓ f4: █ f5: ▀ f6: ▄ f7: ▌ f8: ▐ f9: ■ f10: · [block 7] f1: ☺ f2: ☻ f3: ♥ f4: ♦ f5: ♣ f6: ♠ f7: ≡ f8: ⌂ f9: ♫ f10: ☼ [block 8] f1: ↑ f2: ↓ f3: ▲ f4: ▼ f5: ► f6: ◄ f7: ↨ f8: ↔ f9: ¶ f10: § [block 9] f1: « f2: » f3: ≥ f4: ≤ f5: ⌐ f6: ¬ f7: ² f8: ÷ f9: ½ f10: ¼ [block 10] f1: π f2: ± f3: ⌠ f4: ⌡ f5: Ω f6: ¥ f7: Σ f8: ° f9: √ f10: ⁿ [block 11] f1: α f2: ß f3: Γ f4: σ f5: µ f6: τ f7: δ f8: ∞ f9: φ f10: ε [block 12] f1: Ç f2: ç f3: Ñ f4: ñ f5: ÿ f6: ƒ f7: ≈ f8: · f9: ¡ f10: ¿ [block 13] f1: â f2: ä f3: à f4: á f5: ª f6: å f7: Ä f8: Å f9: æ f10: Æ [block 14] f1: ê f2: ë f3: è f4: é f5: É f6: î f7: ï f8: ì f9: í f10: ₧ [block 15] f1: ô f2: ö f3: ò f4: ó f5: º f6: û f7: ü f8: ù f9: ú f10: Ü durdraw-0.29.0/durdraw/charsets/unicode-groups.xml000066400000000000000000000553021476305210600222340ustar00rootroot00000000000000 durdraw-0.29.0/durdraw/durdraw_ansiparse.py000066400000000000000000000532021476305210600210170ustar00rootroot00000000000000#!/usr/bin/env python3 # Theory of operation notes: # A code starts with '\x1B[' or ^[ and ends with a LETTER. # The ltter is usually 'm' (for Select Graphic Rendition). # Note that it may end with a letter other than 'm' for some features, # like cursor movements. For example: # ^[3A means "move cursor up 3 lines" # A code looks like this: ^[1;32m for bold color 32. ^[0m for reset. # ^[0;4;3;42m for reset (0), underline (4), italic (3), background color (42). # We have a state machine that has attributes. Color, bold, underline, italic, etc. # # References: # Escape code bible: https://en.wikipedia.org/wiki/ANSI_escape_code # ANSI Sauce (metadata) spec: https://www.acid.org/info/sauce/sauce.htm import sys import struct from itertools import count import pdb import durdraw.durdraw_movie as durmovie import durdraw.durdraw_color_curses as dur_ansilib import durdraw.durdraw_sauce as dursauce import durdraw.plugins.convert_charset as durchar import durdraw.log as log LOGGER = log.getLogger('ansiparse', level='DEBUG', override=True) def ansi_color_to_durcolor(ansiColor): colorName = ansi_color_to_durcolor_table[ansiColor] durColor = color_name_to_durcolor_table[colorName] return durColor ansi_color_to_durcolor_table = { # foreground '30': 'Black', '31': 'Red', '32': 'Green', '33': 'Yellow', '34': 'Blue', '35': 'Magenta', '36': 'Cyan', '37': 'White', # background '40': 'Black', '41': 'Red', '42': 'Green', '43': 'Yellow', '44': 'Blue', '45': 'Magenta', '46': 'Cyan', '47': 'White' } color_name_to_durcolor_table = { 'Black': 0, 'Red': 5, 'Green': 3, 'Yellow': 7, 'Blue': 2, 'Magenta': 6, 'Cyan': 4, 'White': 8, 'Black': 00, 'Red': 00, 'Green': 00, 'Yellow': 00, 'Blue': 00, 'Magenta': 00, 'Cyan': 00, 'White': 00 } def find_next_alpha(text, i): for j in count(i): if text[j].isalpha(): return j return None def get_width_and_height_of_ansi_blob(text, width=80): i = 0 # index into the file blob col_num = 0 line_num = 0 max_col = 0 while i < len(text): if i % 10_000 == 0 or i+1 == len(text): LOGGER.debug('scanning', {'i': i+1, 'total': len(text), 'pct': round((i+1)/len(text)*100, 2)}) # If there's an escape code, extract data from it if text[i:i + 2] == '\x1B[': # Match ^[ match = find_next_alpha(text, i+1) if not match: i += 1 # move on to next byte continue end_index = match # where the code ends if text[end_index] == 'A': # Move the cursor up X spaces escape_sequence = text[i + 2:end_index] if len(escape_sequence) == 0: escape_sequence = 1 move_by_amount = int(escape_sequence) line_num = line_num - move_by_amount i = end_index + 1 continue # jump the while elif text[end_index] == 'B': # Move the cursor down X spaces escape_sequence = text[i + 2:end_index] if len(escape_sequence) == 0: escape_sequence = 1 move_by_amount = int(escape_sequence) line_num += move_by_amount i = end_index + 1 continue # jump the while elif text[end_index] == 'C': # Move the cursor forward X spaces escape_sequence = text[i + 2:end_index] if len(escape_sequence) == 0: escape_sequence = 1 move_by_amount = int(escape_sequence) col_num += move_by_amount i = end_index + 1 continue # jump the while elif text[end_index] == 'D': # Move the cursor back X spaces escape_sequence = text[i + 2:end_index] if len(escape_sequence) == 0: escape_sequence = 1 move_by_amount = int(escape_sequence) col_num = col_num - move_by_amount i = end_index + 1 continue # jump the while elif text[end_index] == 'H': # Move the cursor to row/column escape_sequence = text[i + 2:end_index] escape_codes = escape_sequence.split(';') if len(escape_codes) > 1: # row ; column if escape_codes[0].isnumeric(): line_num = int(escape_codes[0]) if escape_codes[1].isnumeric(): col_num = int(escape_codes[1]) elif len(escape_codes) == 1: # row, column=1 #line_num = 1 if escape_codes[0].isnumeric(): col_num = int(escape_codes[0]) i = end_index + 1 continue # jump the while elif text[end_index] == 'J': # Clear screen # 0 or none = clear from cursor to end of screen # 1 = from cursor to top of screen # 2 = clear screen and move cursor to top left # 3 = clear entire screen and delete all lines saved in the scrollback buffer escape_sequence = text[i + 2:end_index] if len(escape_sequence) == 0: escape_sequence = '0' # default - clear from cursor to end if escape_sequence == '2': # cls, move to top left # using a sledgehammer to claer the screen #new_frame = durmovie.Frame(width, height + 1) col_num, line_num = 0, 0 i = end_index + 1 # move on to next byte continue elif text[end_index] == 's': # save current position/state saved_col_num = col_num saved_line_num = line_num elif text[end_index] == 'u': # restore saved position/state col_num = saved_col_num line_num = saved_line_num i = end_index + 1 continue # jump the while # Or, not an escape character elif text[i] == '\n': # new line (LF) line_num += 1 if col_num > max_col: max_col = col_num col_num = 0 elif text[i] == '\r': # windows style newline (CR) pass # pfft elif text[i:i + 5] == 'SAUCE' and len(text) - i == 128: # SAUCE record found i += 128 # Wee, I'm flying else: # printable character (hopefully) if col_num == width: col_num = 0 line_num += 1 character = text[i] character = text[i] col_num += 1 i += 1 #print("") width = max_col height = line_num return width, height def parse_ansi_escape_codes(text, filename = None, appState=None, caller=None, console=False, debug=False, convert_control_codes=True, maxWidth=80): """ Take an ANSI file blob, load it into a DUR frame object, return frame """ sauce = None if type(text) is bytes: text = text.decode('cp437') if filename: # If we can just pull it from the Sauce, cool sauce = dursauce.SauceParser() sauce.parse_file(filename) if sauce.sauce_found: appState.sauce = sauce #if sauce.height > 0 and sauce.width > 0: #if sauce.height == None: # sauce.height = 25 if sauce.width == None: #sauce.width = 80 sauce.width = maxWidth maxWidth = sauce.width width = sauce.width height = sauce.height #caller.notify(f"Sauce pulled: author: {sauce.author}, title: {sauce.title}, width {width}, height {height}") if sauce != None: if not sauce.height: width, height = get_width_and_height_of_ansi_blob(text) width = sauce.width if not sauce.sauce_found or width > 200 or height > 1200: # let the dodgy function guess width, height = get_width_and_height_of_ansi_blob(text) else: width, height = get_width_and_height_of_ansi_blob(text) width = max(width, maxWidth) #width = max(width, 80) height += 1 #if appState.debug: # caller.notify(f"Guessed width: {width}, height: {height}") #width = min(width, maxWidth) height = max(height, 25) if appState.wrapWidth == 80 and width > 750: # I think something is probably wrong. Bad width and/or height. # But, allow --wrap to override this check. width = 80 if height > 8500: height = 1000 #print(f"Bad height or width. Width: {width}, height: {height}") #pdb.set_trace() new_frame = durmovie.Frame(width, height + 1) #if appState.debug: # caller.notify(f"debug: maxWidth = {maxWidth}") #parsed_text = '' #color_codes = '' i = 0 # index into the file blob col_num = 0 line_num = 0 max_col = 0 default_fg_color = appState.defaultFgColor default_bg_color = appState.defaultBgColor fg_color = default_fg_color bg_color = default_bg_color bold = False saved_col_num = 0 saved_line_num = 0 saved_byte_location = 0 parse_error = False while i < len(text): if i % 10_000 == 0 or i+1 == len(text): LOGGER.debug('parsing', {'i': i+1, 'total': len(text), 'pct': round((i+1)/len(text)*100, 2)}) # If there's an escape code, extract data from it if text[i:i + 2] == '\x1B[': # Match ^[[ match = find_next_alpha(text, i+1) if not match: i += 1 # move on to next byte continue end_index = match # where the code ends if text[end_index] == 'm': # Color/SGR control code escape_sequence = text[i + 2:end_index] escape_codes = escape_sequence.split(';') codeList = [] for code in escape_codes: try: codeList.append(int(code)) except: if caller: pass #caller.notify(f"Error in byte {i}, char: {code}, line: {line_num}, col: {col_num}") if len(codeList) > 1 and appState.colorMode == "256": # 256 foreground color bg_color = default_bg_color if codeList[0] == 38 and codeList[1] == 5 and len(codeList) == 3: fg_color = codeList.pop() codeList = [fg_color] # 256 background color elif codeList[0] == 48 and codeList[1] == 5 and len(codeList) == 3: bg_color = codeList.pop() codeList = [fg_color] # Not a 256 color code - treat as 16 color for code in codeList: if code == 0: # reset fg_color = default_fg_color bg_color = default_bg_color bold = False if code == 1: # bold bold = True # In case we're still using a previous color with new attributes if fg_color < 9: # sledgehammer if bold: fg_color += 8 # 16 Colors if code > 29 and code < 38: # FG colors 0-8, or 30-37 if bold: code += 60 # 30 -> 90, etc, for DOS-style bright colors that use bold #bold = False if appState.colorMode == "256": fg_color = dur_ansilib.ansi_code_to_dur_16_color[str(code)] else: #if bold: # code += 60 # 30 -> 90, etc, for DOS-style bright colors that use bold fg_color = dur_ansilib.ansi_code_to_dur_16_color[str(code)] #if bold: # fg_color += 8 # fix for durdraw color pair stupidity if fg_color == -1 or fg_color == 0: # black fg and bright black fg fix if bold: fg_color = 9 else: fg_color = 1 #if fg_color == 8: # bright white fix # if bold: # fg_color = 16 #bold = False if fg_color < 9: # sledgehammer if bold: fg_color += 8 elif code > 39 and code < 48: # BG colors 0-8, or 40-47 if appState.colorMode == "256": #bg_color = dur_ansilib.ansi_code_to_dur_16_color[str(code)] - 1 #bg_color = 0 bg_color = dur_ansilib.ansi_code_to_dur_16_color[str(code)] - 1 if bg_color == -1: bg_color = 0 else: #if bold: # code += 60 # 30 -> 90, etc, for DOS-style bright colors that use bold bg_color = dur_ansilib.ansi_code_to_dur_16_color[str(code)] - 1 if bg_color == -1: bg_color = 0 if fg_color == -1 or fg_color == 0: # black fg and bright black fg fix if bold: fg_color = 9 else: fg_color = 1 # 256 Colors #if console: # print(str(escape_codes), end="") # Add color to color map try: new_frame.newColorMap[line_num][col_num] = [fg_color, bg_color] except Exception as E: if console: print(str(E)) print(f"line num: {line_num}") i = end_index + 1 continue # jump the while elif text[end_index] == 'A': # Move the cursor up X spaces escape_sequence = text[i + 2:end_index] if len(escape_sequence) == 0: escape_sequence = 1 move_by_amount = int(escape_sequence) line_num = line_num - move_by_amount i = end_index + 1 continue # jump the while elif text[end_index] == 'B': # Move the cursor down X spaces escape_sequence = text[i + 2:end_index] if len(escape_sequence) == 0: escape_sequence = 1 move_by_amount = int(escape_sequence) line_num += move_by_amount i = end_index + 1 continue # jump the while elif text[end_index] == 'C': # Move the cursor forward X spaces escape_sequence = text[i + 2:end_index] if len(escape_sequence) == 0: escape_sequence = 1 move_by_amount = int(escape_sequence) if col_num >= maxWidth: col_num = 0 line_num += 1 col_num += move_by_amount i = end_index + 1 continue # jump the while elif text[end_index] == 'D': # Move the cursor back X spaces escape_sequence = text[i + 2:end_index] if len(escape_sequence) == 0: escape_sequence = 1 move_by_amount = int(escape_sequence) col_num = col_num - move_by_amount i = end_index + 1 continue # jump the while elif text[end_index] == 'H': # Move the cursor to row/column escape_sequence = text[i + 2:end_index] escape_codes = escape_sequence.split(';') if len(escape_codes) > 1: # row ; column if escape_codes[0].isnumeric(): line_num = int(escape_codes[0]) if escape_codes[1].isnumeric(): col_num = int(escape_codes[1]) elif len(escape_codes) == 1: # row, column=1 #line_num = 1 if escape_codes[0].isnumeric(): col_num = int(escape_codes[0]) i = end_index + 1 continue # jump the while elif text[end_index] == 'J': # Clear screen # 0 or none = clear from cursor to end of screen # 1 = from cursor to top of screen # 2 = clear screen and move cursor to top left # 3 = clear entire screen and delete all lines saved in the scrollback buffer escape_sequence = text[i + 2:end_index] if len(escape_sequence) == 0: escape_sequence = '0' # default - clear from cursor to end if escape_sequence == '2': # cls, move to top left # using a sledgehammer to claer the screen new_frame = durmovie.Frame(width, height + 1) col_num, line_num = 0, 0 i = end_index + 1 # move on to next byte continue elif text[end_index] == 's': # save current position/state #saved_col_num = col_num #saved_line_num = line_num i = end_index + 1 # move on to next byte #saved_byte_location = i continue elif text[end_index] == 'u': # restore saved position/state #col_num = saved_col_num #line_num = saved_line_num #i = saved_byte_location i = end_index + 1 # move on to next byte #pdb.set_trace() continue else: # Some other escape code, who cares for now if appState.debug: caller.notify(f"Unknown escape character type encountered: {text[end_index]}") i = end_index + 1 # move on to next byte continue # Or, not an escape character elif text[i] == '\n': # new line (LF) line_num += 1 if col_num > max_col: max_col = col_num col_num = 0 elif text[i] == '\r': # windows style newline (CR) pass # pfft elif text[i] == '\x00': # Null byte pass elif text[i] == '\x1a': # ctl-z code, EOF, just before the SAUCE pass #elif text[i] == '\x01': # CTRL-A, SOH (start header). # # Q: Why is this in some ANSIs? A: Because it's a smiley face in CP437 # pass #elif text[i] == '\x02': # CTRL-B, STX (start text) # pass elif text[i:i + 5] == 'SAUCE' and len(text) - i == 128: # SAUCE record found i += 128 else: # printable character (hopefully) if col_num >= maxWidth: col_num = 0 line_num += 1 character = text[i] try: # Convert CP437 control codes if ord(character) < 0x19: character = chr(durchar.cp437_control_codes_to_utf8[ord(character)]) new_frame.content[line_num][col_num] = character except TypeError as E: print(f"Type error, likely on text: {E}") pdb.set_trace() except IndexError: parse_error = True if debug: caller.notify(f"Error writing content. Width: {width}, Height: {height}, line: {line_num}, col: {col_num}, char: {character}, pos: {i}") try: new_frame.newColorMap[line_num][col_num] = [fg_color, bg_color] except IndexError: parse_error = True if debug: caller.notify(f"Error writing color. Width: {width}, Height: {height}, line: {line_num}, col: {col_num}, pos: {i}") # if console: # print(character, end='') col_num += 1 i += 1 if console: print("") print(f"Lines: {line_num}, Columns: {max_col}") if parse_error and appState.debug: caller.notify(f"Possible errors detected while loading this file. It may not display correctly.") height = line_num width = max_col frame = durmovie.Frame(height, width) line_num = 0 col_num = 0 # maybe usethis for the color: dur_ansilib.ansi_code_to_dur_16_color[fg_ansi] return new_frame if __name__ == "__main__": # Example usage #file_path = 'kali.ans' #file_path = '11.ANS' file_path = '../rainbow.ans' if len(sys.argv) > 1: file_path = sys.argv[1] with open(file_path, 'r') as file: try: text_with_escape_codes = file.read() except UnicodeDecodeError: file.close() file = open(file_path, "r", encoding="cp437") #file = open(file_path, "r", encoding="big5") text_with_escape_codes = file.read() #parsed_text, fg, bg = parse_ansi_escape_codes(text_with_escape_codes) newFrame = parse_ansi_escape_codes(text_with_escape_codes, console=True) print(str(newFrame.newColorMap)) print(str(newFrame.content)) print(str(newFrame)) #print(parsed_text) #print(f"Fg: {fg}, bg: {bg}") durdraw-0.29.0/durdraw/durdraw_appstate.py000066400000000000000000000441751476305210600206640ustar00rootroot00000000000000import configparser import curses import gzip import os import pdb import pickle import subprocess import sys import threading from sys import version_info from durdraw.durdraw_options import Options import durdraw.durdraw_file as durfile import durdraw.durdraw_sauce as dursauce import durdraw.log as log class AppState(): """ run-time app state, separate from movie options (Options()) """ def __init__(self): # Check for optional dependencies #self.ansiLove = self.isAppAvail("ansilove") #self.neofetch = self.isAppAvail("neofetch") #self.PIL = self.checkForPIL() self.ansiLove = None self.neofetch = None self.PIL = None self.check_dependencies() #threading self.stop_event = threading.Event() self.bg_download_thread = None self.bg_download_executor = None # String containnig updates from threads to tell users about self.thread_update_string = None self.pool_executor = None # User friendly defeaults self.quickStart = False self.mental = False # Mental mode - enable experimental options/features self.showStartupScreen = False self.narrowWindow = False self.curOpenFileName = "" python_version = f"{version_info.major}.{version_info.minor}.{version_info.micro}" self.pyVersion = python_version self.colorMode = "256" # or 16, or possibly "none" or "true" or "rgb" (24 bit rgb "truecolor") self.fileColorMode = None self.maxColors = 256 self.iceColors = False # 16c stuff self.sixteenc_available = True # Enabled if 16colo.rs browsing is available self.sixteenc_browsing = False # Enabled if we are currently browsing 16c self.sixteenc_dizcache = {} # {"packname": dizdata} self.sixteenc_cached_years = [] # [1996, etc] self.sixteenc_api = None self.sixteenc_year = None self.sixteenc_pack = None # Durview stuff self.durview_running = False self.play_queue = [] # List of files to switch between with 'n' and 'p' in Durview and/or play mode self.play_queue_position = None # Other durdraw runtime stuff self.can_inject = False # Allow injecting color codes to override ncurses colors (for BG 256 colors) self.sleep_time = 0 # Use this as a delay for playback mode, dictated in ui_curses.py from FPS self.showBgColorPicker = False # until BG colors work in 256 color mode. (ncurses 5 color pair limits) self.scrollColors = False # When true, scroll wheel in canvas changes color instead of moving cursor self.editorRunning = True self.screenCursorMode = "default" # can be block, underscore, pipe self.renderMouseCursor = False # show Paint or Draw cursor in canvas self.validScreenCursorModes = ["default", "block", "underscore", "pipe"] self.cursorBlinks = True # lord help me, why would anyone not want this to be true? self.totalFgColors = 16 self.totalBgColors = 8 self.defaultFgColor = 7 self.defaultBgColor = 0 self.width = 80 # default canvas width/columns self.height = 23 # default canvas height/lines self.wrapWidth = 80 # Default width to wrap at when loading ASCII files (.asc/.txt) self.minWindowWidth = 40 # smaller than this, and Durdraw complains that it can't draw the UI self.full_ui_width = 80 # smaller than this, and draw the streamlined UI self.stickyColorPicker = True # true to keep color picker on screen self.colorPickerSelected = False # true when the user hits esc-c self.charEncoding = 'utf-8' # or cp437, aka ibm-pc self.unicodeBlockList = [] self.userCharSets = [] self.userCharSetFiles = {} self.characterSet = "Durdraw Default" self.showCharSetButton = False self.workingLoadDirectory = None self.fileShortPath = None self.fileLongPath = None # if self.characterSet == "Unicode Block" then Durdraw knows to use a # unicode block: #self.characterSet = "Unicode Block" self.unicodeBlock = "Braille Patterns" # placeholder during initialization self.cursorMode = "Move" # Move/Select, Draw and Color self.fetchMode = False # use neofetch, replace {variables} in dur file self.fetchData = None # a {} dict containing key:value for neofetch output. self.inferno = None self.inferno_opts = None self.playOnlyMode = False # This means viewer mode now, actually.. self.viewModeShowInfo = False # show sauce etc in view mode self.playNumberOfTimes = 0 # 0 = loop forever, default self.undoHistorySize = 100 # How far back our undo history can self.playbackRange = (1,1) self.drawChar = '$' self.brush = None self.configFile = None self.configFileLoaded = False self.configFileName = None self.customThemeFile = None self.sauce = dursauce.SauceParser() # empty sauce #self.drawChar = b'\xE2\x96\x88' self.CP438_BLOCK = chr(219) self.UTF8_BLOCK = chr(9608) self.blockChar = self.UTF8_BLOCK # Unicode block by default, --cp437 should change this self.colorPickChar = self.blockChar self.hasMouse = True # replace with equivalent curses.has_mouse() self.hasMouseScroll = True # Disable for compatibility with older Python versions <3.10 self.mouse_col = 0 self.mouse_line = 0 self.helpMov = None self.helpMov_2 = None self.hasHelpFile = False self.playingHelpScreen = False self.playingHelpScreen_2 = False # on page 2 of help screen self.durVer = None self.debug = False self.debug2 = False # extra verbose debug, eg: file loading intricates self.modified = False self.durhelp256_fullpath = None self.durhelp256_page2_fullpath = None self.durhelp16_fullpath = None self.durhelp16_page2_fullpath = None # This doesn't work yet (color pairs past 256 colors. They set, but the background color doesn't get set. #if sys.version_info >= (3, 10): # if curses.has_extended_color_support(): # Requires Ncures 6 # self.showBgColorPicker = True # until BG colors work in 256 color mode. (ncurses 5 color pair limits) self.realmaxX = 0 self.realmaxY = 0 self.topLine = 0 # the top line visible on the screen, used in refresh() for scrolling self.firstCol = 0 # leftmost visbile column, to facilitate left/right scrolling self.drawBorders = True self.durFileVer = 0 # gets set in main() from DUR_FILE_VER self.sideBarEnabled = True # to show color picker, sauce info, etc self.sideBarColumn = 0 # location, usually just right of the border #self.sideBar_minimum_width = 37 # Must have this much width to draw sidebar. Actually it's the colorBar width. self.sideInfo_minimum_width = 8 # Must have this much width beyond canvas width to draw esc-i sauce info self.sideBar_minimum_width = 5 # Must have this much width to draw sidebar. Actually it's the colorBar width. self.sideBar_minimum_width_256 = 37 # Must have this much width to draw sidebar. Actually it's the colorBar width. self.sideBar_minimum_width_16 = 12 # Must have this much width to draw sidebar. Actually it's the colorBar width. self.bottomBar_minimum_height = 10 # same as above, but for height self.bottomBar_minimum_height_256 = 10 self.bottomBar_minimum_height_16 = 4 self.colorBar_height = 8 self.sideBarShowing = False self.themesEnabled = True self.themeName = "default" self.theme_16 = { 'mainColor': 8, # grey 'clickColor': 3, # green 'clickHighlightColor': 11, # bright green 'borderColor': 8, # grey #'notifications': 89 # sick maroon 'notificationColor': 8, # grey 'promptColor': 8, # grey 'menuItemColor': 2, 'menuTitleColor': 3, 'menuBorderColor': 4, } self.theme_256 = { 'mainColor': 7, # grey 'clickColor': 2, # green 'clickHighlightColor': 10, # bright green 'borderColor': 7, # grey #'notifications': 89 # sick maroon 'notificationColor': 3, # cyan 'promptColor': 3, # cyan 'menuItemColor': 7, 'menuTitleColor': 98, 'menuBorderColor': 7, } self.theme = self.theme_16 self.log_level = 'WARNING' self.log_filepath = './durdraw.log' self.log_local_tz = False self.logger = log.getLogger('appstate') def maximize_canvas(self): term_size = os.get_terminal_size() if term_size[0] > 80: self.width = term_size[0] if term_size[1] > 24: self.height = term_size[1] - 2 def check_dependencies(self): dependency_thread = threading.Thread(target=self.thread_check_dependencies) dependency_thread.start() def thread_check_dependencies(self): # Check for optional dependencies self.ansiLove = self.isAppAvail("ansilove") self.neofetch = self.isAppAvail("neofetch") self.PIL = self.checkForPIL() def setCursorModeMove(self): self.cursorMode="Move" def setCursorModeSelect(self): self.cursorMode="Select" def setCursorModeDraw(self): self.cursorMode="Draw" def setCursorModePaint(self): self.cursorMode="Paint" def setCursorModeCol(self): self.cursorMode="Color" def setCursorModeErase(self): self.cursorMode="Erase" def setCursorModeEyedrop(self): self.cursorMode="Eyedrop" def setDurFileVer(self, durFileVer): # file format for saving. 1-4 are pickle, 5+ is JSON self.durFileVer = durFileVer def setDurVer(self, version): self.durVer = version def setDebug(self, isEnabled: bool): self.debug = isEnabled def setLogger(self, level=log.DEFAULT_LOG_LEVEL, filepath=log.DEFAULT_LOG_FILEPATH, local_tz=False): self.log_level = level self.log_filepath = filepath self.log_local_tz = local_tz self.logger = log.getLogger( 'appstate', level=self.log_level, filepath=self.log_filepath, override=True, local_tz=self.log_local_tz, ) def getLogger(self, name: str): return log.getLogger(name, level=self.log_level, filepath=self.log_filepath, local_tz=self.log_local_tz) def loadThemeList(self): """ Look for theme files in internal durdraw directory """ # durhelp256_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp-256-long.dur") # Get a list of files from the themes paths internal_theme_path = pathlib.Path(__file__).parent.joinpath("themes/") self.internal_theme_file_list = glob.glob(f"{internal_theme_path}/*.dtheme.ini") #user_theme_path = pathlib.Path(__file__).parent.joinpath("themes/") #self.user_theme_file_list = glob.glob(f"{user_theme_path}/*.dtheme.ini") # Turn lists into an index of Theme name, Theme type, and Path to theme_files = [] # populate with a list of dicts containing name=, path=, type= for filename in self.internal_theme_file_list: theme_files += filename def loadConfigFile(self): # Load configuration filea configFullPath = os.path.expanduser("~/.durdraw/durdraw.ini") configShortFile = 'durdraw.ini' configFileLocations = [configFullPath, configShortFile] configFile = configparser.ConfigParser() readConfigPaths = configFile.read(configFileLocations) if self.configFile == []: self.configFileLoaded = False return False else: self.configFileName = readConfigPaths self.configFile = configFile self.configFileLoaded = True return True def loadThemeFromConfig(self, themeMode): #pdb.set_trace() if not self.themesEnabled: return False if 'Theme' in self.configFile: themeConfig = self.configFile['Theme'] if 'theme-16' in themeConfig and themeMode == 'Theme-16': self.loadThemeFile(themeConfig['theme-16'], themeMode) if 'theme-256' in themeConfig and themeMode == 'Theme-256': self.loadThemeFile(themeConfig['theme-256'], themeMode) def getConfigOption(self, section: str, item: str): # section = something like [Main], item = something like color-mode: try: # see if section and item exist, otherwise return False configSection = self.configFile[section] configItem = configSection[item] return configItem except KeyError: return False def loadThemeFile(self, themeFilePath, themeMode): # If there is a theme set, use it #if 'Theme' in self.configFile: # Load .dtheme file #themeFullPath = os.path.expanduser(f"~/.durdraw/{themeName}.dtheme") themeFullPath = os.path.expanduser(themeFilePath) themeFileConfig = configparser.ConfigParser() themeConfigsLoaded = themeFileConfig.read(themeFullPath) #pdb.set_trace() if themeConfigsLoaded == []: return False # could not find or load the theme file else: theme = themeFileConfig[themeMode] if 'name' in theme: self.themeName = str(theme['name']) if 'mainColor' in theme: self.theme['mainColor'] = int(theme['mainColor']) if 'clickColor' in theme: self.theme['clickColor'] = int(theme['clickColor']) if 'borderColor' in theme: self.theme['borderColor'] = int(theme['borderColor']) if 'clickHighlightColor' in theme: self.theme['clickHighlightColor'] = int(theme['clickHighlightColor']) if 'notificationColor' in theme: self.theme['notificationColor'] = int(theme['notificationColor']) if 'promptColor' in theme: self.theme['promptColor'] = int(theme['promptColor']) if 'menuItemColor' in theme: self.theme['menuItemColor'] = int(theme['menuItemColor']) if 'menuTitleColor' in theme: self.theme['menuTitleColor'] = int(theme['menuTitleColor']) if 'menuBorderColor' in theme: self.theme['menuBorderColor'] = int(theme['menuBorderColor']) return True def checkForPIL(self): try: import PIL return True except ImportError: return False def isAppAvail(self, name): # looks for program 'name' in path try: devnull = open(os.devnull) subprocess.Popen([name], stdout=devnull, stderr=devnull).communicate() except OSError as e: #if e.errno == os.errno.ENOENT: # return False return False return True def loadDurFileToMov(self, fileName): """ Takes a file path, returns a movie object """ fileName = os.path.expanduser(fileName) #self.helpMov = Movie(self.opts) # initialize a new movie to work with try: f = open(fileName, 'rb') except Exception as e: return False if (f.read(2) == b'\x1f\x8b'): # gzip magic number # file == gzip compressed f.close() try: f = gzip.open(fileName, 'rb') except Exception as e: return False else: f.seek(0) try: # Load json help file #pdb.set_trace() loadedContainer = durfile.open_json_dur_file(f, self) opts = loadedContainer['opts'] mov = loadedContainer['mov'] return mov, opts except: #pass # loading json help file failed for some reason, so... return False def loadHelpFileThread(self, helpFileName): help_loading_thread = threading.Thread(target=self.loadHelpFile, args=(helpFileName,)) help_loading_thread.start() def loadHelpFile(self, helpFileName, page=1): helpFileName = os.path.expanduser(helpFileName) #self.helpMov = Movie(self.opts) # initialize a new movie to work with try: f = open(helpFileName, 'rb') except Exception as e: self.hasHelpFile = False self.helpMov = None return False if (f.read(2) == b'\x1f\x8b'): # gzip magic number # file == gzip compressed f.close() try: f = gzip.open(helpFileName, 'rb') except Exception as e: self.hasHelpFile = False self.helpMov = None self.helpMov_2 = None return False else: f.seek(0) try: # Load json help file #pdb.set_trace() loadedContainer = durfile.open_json_dur_file(f, self) if page == 1: self.helpMovOpts = loadedContainer['opts'] self.helpMov = loadedContainer['mov'] elif page == 2: self.helpMovOpts_2 = loadedContainer['opts'] self.helpMov_2 = loadedContainer['mov'] self.hasHelpFile = True return True except: #pass # loading json help file failed for some reason, so... return False #try: # Load pickle file. This should never happen anymore, so... # #self.opts = pickle.load(f) # #self.mov = pickle.load(f) # self.helpMovOpts = pickle.load(f) # self.helpMov = pickle.load(f) # self.hasHelpFile = True # return True #except Exception as e: # self.hasHelpFile = False # self.helpMov = None # return False durdraw-0.29.0/durdraw/durdraw_charsets.py000066400000000000000000000156401476305210600206520ustar00rootroot00000000000000# Load character sets from files, initialize them import configparser import pdb import os import pathlib import xml.etree.ElementTree as ET def hex_range_iterator(start_hex, end_hex): """ Iterator that takes a first and last hex number, and returns each one in succession. Used to generate character sets from unicode-provided ranges # Example usage: start_hex = "1FB00" end_hex = "1FBFF" for hex_value in hex_range_iterator(start_hex, end_hex): print(hex_value) """ start_int = int(start_hex, 16) # Convert start_hex to an integer end_int = int(end_hex, 16) # Convert end_hex to an integer current_int = start_int while current_int <= end_int: #yield format(current_int, 'X') # Convert the integer back to hexadecimal #yield hex(current_int) yield current_int current_int += 1 def parse_xml_blocks_file(xml_file_path): """ laod XML file, returns block_data object, containing all the block names and their first and last code point (hex value) """ block_data = {} # Parse the XML file tree = ET.parse(xml_file_path) root = tree.getroot() # Iterate through the 'block' elements and extract the data for block_element in root.findall('.//block'): first_cp = block_element.get('first-cp') last_cp = block_element.get('last-cp') block_name = block_element.get('name') if block_name is not None and first_cp is not None and last_cp is not None: block_data[block_name] = { 'first-cp': first_cp, 'last-cp': last_cp } return block_data # We can return a fullCharMap that looks like this: [ {'f1':\x1FB00, # 895 self.fullCharMap = [ \ # 896 # All of our unicode templates live here. Blank template: # 897 #{'f1':, 'f2':, 'f3':, 'f4':, 'f5':, 'f6':, 'f7':, 'f8':, 'f9':, 'f10':}, # 898 # 899 # block characters # 900 {'f1':9617, 'f2':9618, 'f3':9619, 'f4':9608, 'f5':9600, 'f6':9604, 'f7':9612, 'f8':9616, 'f9': 9632, 'f10':183 }, # ibm-pc looking block characters (but unicode instead of ascii) def load_unicode_block(block_name: str): """ returns a fullCharMap """ #xml_block_filename = 'unicode-groups.xml' xml_block_filename = pathlib.Path(__file__).parent.joinpath("charsets/unicode-groups.xml") block_data = parse_xml_blocks_file(xml_block_filename) if block_name in block_data: block_info = block_data[block_name] #print(f"Block: {block_name}") #print(f"First Code Point: {block_info['first-cp']}") #print(f"Last Code Point: {block_info['last-cp']}") first_cp = block_info['first-cp'] last_cp = block_info['last-cp'] charMap = [] fKeyList = {'f1':'', 'f2':'', 'f3':'', 'f4':'', 'f5':'', 'f6':'', 'f7':'', 'f8':'', 'f9':'', 'f10':''} fKeyNum = 1 # go through each code point, assign them to F1-F10, and add those f1-f10 # sets to the larger full character map for code_point in hex_range_iterator(first_cp, last_cp): #fKeyList[f'f{fKeyNum}'] = chr(code_point) #fKeyList[f'f{fKeyNum}'] = f'\U{hex(code_point)}' fKeyList[f'f{fKeyNum}'] = code_point if fKeyNum == 10: charMap.append(fKeyList.copy()) fKeyNum = 1 else: fKeyNum += 1 #print(str(fKeyList)) return charMap else: #print(f"Block '{block_name}' not found in the XML data.") # this should not happen, so... return None def get_unicode_blocks_list(): xml_block_filename = pathlib.Path(__file__).parent.joinpath("charsets/unicode-groups.xml") block_data = parse_xml_blocks_file(xml_block_filename) block_list = list(block_data.keys()) block_list = block_list[1:] # cut off Basic Latin, or 0000-007F, as 0000 is a null character return block_list def scan_charmap_folders(appState): """ Scans ~/.durdraw/charmap and $durdraw/config """ internal_charsets_path = pathlib.Path(__file__).parent.joinpath("charsets") charmap_dirs = ["~/.durdraw/charsets", internal_charsets_path] for directory in charmap_dirs: directory = os.path.expanduser(directory) if os.path.isdir(directory) and os.access(directory, os.R_OK): for filename in os.listdir(directory): if filename.endswith(".ini"): file_path = os.path.join(directory, filename) load_charmap_file(file_path, appState) def load_charmap_file(file_path: str, appState, setting=False): """ returns a fullCharMap. setting=True if switching to the character set """ # Open file from path with configparser file_path = os.path.expanduser(file_path) configFileLocations = [file_path] configFile = configparser.ConfigParser() readConfigPaths = configFile.read(configFileLocations) if configFile == []: #self.configFileLoaded = False return False name = 'custom set' encoding = 'utf-8' if 'Character Set' in configFile: charSetConfig = configFile['Character Set'] if 'name' in charSetConfig: name = charSetConfig['name'] # character set name if 'encoding' in charSetConfig: encoding = charSetConfig['encoding'] charMap = [] fKeyList = {'f1':'', 'f2':'', 'f3':'', 'f4':'', 'f5':'', 'f6':'', 'f7':'', 'f8':'', 'f9':'', 'f10':''} # Look for [block 1] [block 2] etc. in file. eg: # [block 1] # f1: '🭨' blockNum = 1 scanning = True while scanning: blockName = f'block {blockNum}' if blockName in configFile: blockConfig = configFile[blockName] # put fkeys into fkeylist fKeyNum = 1 while fKeyNum < 11: fKeyString = f'f{fKeyNum}' if fKeyString in blockConfig: try: fKeyList[fKeyString] = ord(blockConfig[fKeyString][0]) except: # empty value fKeyList[fKeyString] = ord(' ') #fKeyList[fKeyString] = blockConfig[fKeyString] #fKeyList[fKeyString] = ord('x') fKeyNum += 1 charMap.append(fKeyList.copy()) blockNum += 1 else: # ran out of blocks. Stop searching. scanning = False if len(charMap) == 0: return False else: # Loaded, so add to appState. # If the name isn't already taken, add it to the appState character set list. if name not in appState.userCharSets: appState.userCharSets.append(name) appState.userCharSetFiles.update({name: file_path}) if setting: appState.characterSet = name return charMap if __name__ == "__main__": unicodeBlocksList = get_unicode_blocks_list() print(str(unicodeBlocksList)) durdraw-0.29.0/durdraw/durdraw_color_curses.py000066400000000000000000001270131476305210600215360ustar00rootroot00000000000000import curses import time import pdb from functools import lru_cache legacy_256_to_256 = { # used when loading an old 256 color file in 256 color mode #0: 7, # white 1: 7, # white 2: 3, # cyan 3: 5, # magenta 4: 1, # blue 5: 6, # yellow/brown 6: 2, # green 7: 4, # red 8: 16, # black 9: 12, # bright red 10: 10, # bright green 11: 14, # bright yellow 12: 9, # bright blue 13: 13, # bright magenta 14: 11, # bright cyan 15: 15, # bright white 16: 8, # bright black } legacy_16_to_256 = { # used when loading an old 16 color file in 256 color mode #0: 7, # white 1: 7, # white 2: 3, # cyan 3: 5, # magenta 4: 1, # blue 5: 6, # yellow/brown 6: 2, # green 7: 4, # red 8: 16, # black 9: 15, # bright white 10: 11, # bright cyan 11: 13, # bright magenta 12: 9, # bright blue 13: 14, # bright yellow 14: 10, # bright green 15: 12, # bright red 16: 8, # bright black } legacy_16_to_16 = { # used when loading an old 16 color file in 16 color mode #0: 7, # white 1: 8, # white 2: 4, # cyan 3: 6, # magenta 4: 2, # blue 5: 7, # yellow/brown 6: 3, # green 7: 5, # red 8: 1, # black 9: 16, # bright white 10: 12, # bright cyan 11: 14, # bright magenta 12: 10, # bright blue 13: 15, # bright yellow 14: 11, # bright green 15: 13, # bright red 16: 9, # bright black 'bg': { # background colors 1: 7, 2: 3, # cyan 3: 5, # magenta 4: 1, # blue 5: 6, # yellow/brown 6: 2, # green 7: 4, # red 8: 8, # black } } color_256_to_ansi_16 = { # used when saving a 256 color file, to convert first 16 colors #0: 7, # white 1: 4, # blue 2: 2, # green 3: 6, # cyan 4: 1, # red 5: 5, # magenta 6: 3, # yellow 7: 7, # white 8: 8, # bright black 9: 12, # bright blue 10: 10, # bright green 11: 14, # bright cyan 12: 9, # bright red 13: 13, # bright magenta 14: 11, # bright yellow 15: 15, # bright white 16: 16, # black } mirc_16_to_color_16 = { 0: 0, # white 1: 1, # black 2: 2, # blue 3: 3, # green 10: 4, # cyan 5: 5, # red (brown) 6: 6, # magenta 7: 7, # yellow (orange) 15: 8, # white/grey 14: 9, # br black/dark grey 12: 10, # br blue 9: 11, # br green 11: 12, # br cyan 4: 13, # br red 13: 14, # br magenta 8: 15, # br yellow #0: 16, # br white } color_16_to_mirc_16 = { 0: 0, # white 1: 1, # black 2: 2, # blue 3: 3, # green 4: 10, # cyan 5: 5, # red (brown) 6: 6, # magenta 7: 7, # yellow (orange) 8: 15, # white/grey 9: 14, # br black/dark grey 10: 12, # br blue 11: 9, # br green 12: 11, # br cyan 13: 4, # br red 14: 13, # br magenta 15: 8, # br yellow 16: 0, # br white } color_256_to_mirc_16 = { 0: 0, # white 1: 1, # black 2: 2, # blue 3: 3, # green 4: 10, # cyan 5: 5, # red (brown) 6: 6, # magenta 7: 7, # yellow (orange) 8: 15, # white/grey 9: 14, # br black/dark grey 10: 12, # br blue 11: 9, # br green 12: 11, # br cyan 13: 4, # br red 14: 13, # br magenta 15: 8, # br yellow 16: 0, # br white } ansi_code_to_dur_16_color = { '30': 0, # black '31': 5, # red '32': 3, # green '33': 7, # yellow/brown '34': 2, # blue '35': 6, # magenta '36': 4, # cyan '37': 8, # grey/white '90': 9, # bright black? '91': 13, # bright red? '92': 11, # bright green? '93': 15, # bright yellow '94': 10, # bright blue '95': 14, # bright magenta '96': 12, # bright cyan '97': 16, # bright white '40': 0, # black '41': 5, # red '42': 3, # green '43': 7, # yellow/brown '44': 2, # blue '45': 6, # magenta '46': 4, # cyan '47': 8, # grey/white } #ansi_code_to_dur_16_color = { # '30': 0, # black # '31': 8, # white # red # '32': 7, # yellow/brown # green # '33': 6, # magenta # yellow/brown # '34': 5, # red # blue # '35': 4, # cyan # magenta # '36': 3, # green # cyan # '37': 2, # blue # grey/white #} class AnsiArtStuff(): """ Ansi specific stuff.. escape codes, any refs to code page 437, ncurses color boilerplate, etc """ def __init__(self, appState): self.appState = appState self.colorPairMap = None # fill this with dict of FG/BG -> curses pair # self.escapeFgMap = { # color numbers documented in initColorPairs() comments # ANSI escape code FG colors # regular colors, white (1) through to black (8 and 0) # Using aciddraw/pablodraw-style color order 0:"0;30", # 1: black 1:"0;30", # 1: black 2:"0;34", # 2: blue 3:"0;32", # 3: green 4:"0;36", # 4: cyan 5:"0;31", # 5: red 6:"0;35", # 6: magenta 7:"0;33", # 7: yellow/brown 8:"0;37", # 8: grey/white # bright colors 9:"1;30", # 9: dark grey/black 10:"1;34", # 10 bright blue 11:"1;32", # 11 bright green 12:"1;36", # cyan 13:"1;31", # bright red 14:"1;35", # magenta 15:"1;33", # yellow 16:"1;37", # Bright white } self.escapeFgMap_old = { # color numbers documented in initColorPairs() comments # ANSI escape code FG colors # regular colors, white (1) through to black (8 and 0) 1:"0;37", 2:"0;36", 3:"0;35", 4:"0;34", 5:"0;33", 6:"0;32", 7:"0;31", 8:"0;30", 0:"0;30", # bright colors, brwhite (9) through to brblack (16) 9:"1;37", 10:"1;36", 11:"1;35", 12:"1;34", 13:"1;33", 14:"1;32", 15:"1;31", 16:"1;30" } self.ansiGraphicsModeTokenMap = { """ For parsing ANSI graphics sequence. Eg: ^[0;32;42m is green fg (32), magenta bg (42) no bold (0), setting graphics mode (m) """ # expects a function to extract escape sequence then tokenize # based on locations of ^[, ; and ending with m. # Should this store logical color name strings instead of # durdraw color numbers? Then another map can convert names # to numbers. # * Other ANSI gotchas: Pablodraw uses Cursor Forward (C) # commands before each block of characters. Eg: ^[[27C. # places a . character at column 27 of the current line. # This == different from Durdraw, which would instead place # 27 spaces, each escaped to set the color, and then a . # character. # fg colors "37":1, "36":2, "35":3, "34":4, "33":5, "32":6, "31":7, "30":8, # bg colors "47":1, "46":2, "45":3, "44":4, "43":5, "42":6, "41":7, "40":8, # text attributes "0":"none", # non-bold white fg, black bg "1":"bold", # bright color "4":"underscore", # should we handle this? "5":"blink", # iCE color "7":"reverse", # should we handle this? "8":"concealed", } self.escapeBgMap = { 1: "44", # blue 2: "42", # green 3: "46", # cyan 4: "41", # red 5: "45", # magenta 6: "43", # yellow/brown 7: "47", # white 8: "40", # black 0: "40", # black } self.escapeBgMap_old = { 1:"47", 2:"46", 3:"45", 4:"44", 5:"43", 6:"42", 7:"41", 8:"40", 0:"40" } @lru_cache(maxsize=1024) def getColorCode24k(r, g, b): """ r, g and b must be numbers between 0-255 """ return f'\033[38;2;{r};{g};{b}m' @lru_cache(maxsize=1024) def getColorCodeIrc(self, fg, bg): """ Return a string containing the IRC color code to color the next character, for given fg/bg """ # references: https://www.mirc.com/colors.html # http://anti.teamidiot.de/static/nei/*/extended_mirc_color_proposal.html # map Durdraw -> IRC colors if self.appState.colorMode == '16': fg = color_16_to_mirc_16[fg] bg = color_16_to_mirc_16[bg] elif self.appState.colorMode == '256': fg = color_256_to_mirc_16[fg] bg = color_256_to_mirc_16[bg] # fg = color_256_to_mirc_16[fg] # bg = color_256_to_mirc_16[bg] return f'\x03{fg:02d},{bg:02d}' @lru_cache(maxsize=1024) def getColorCode256(self, fg, bg): """ Return a string containing 256-color mode ANSI escape code for given fg/bg """ if fg <= 16 and fg > 0: fg = color_256_to_ansi_16[fg] return f'\033[38;5;{fg};48;5;{bg}m' @lru_cache(maxsize=1024) def getColorCode(self, fg, bg): """ returns a string containing ANSI escape code for given fg/bg """ return f'\033[{self.escapeFgMap[fg]};{self.escapeBgMap[bg]}m' def codePage437(self): pass def convert_colormap(self, mov, conv_table): """ takes a dictionary that contains a coler mapper. Modifies the movie """ # It might be better to deepclone and return a new movie... for frame in mov.frames: for line in frame.newColorMap: for pair in line: if pair[0] in conv_table.keys(): pair[0] = conv_table[pair[0]] if 'bg' in conv_table.keys(): if pair[1] in conv_table['bg'].keys(): pair[1] = conv_table['bg'][pair[1]] #if pair[1] == 16: # pair[1] = 0 #if pair[1] in conv_table.keys(): # pair[1] = conv_table[pair[1]] def initColorPairs_general(self, fg_count=256, bg_count=16, use_order=False): # High color pairs, 256 color """ Initialize 256 color mode color pairs """ self.colorPairMap = {} # color order to match thedraw, etc. color_order = [0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15, 16] pair = 0 try: curses.use_default_colors() bg = 0 if use_order: fg_range = [0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15] else: fg_range = list(range(0, fg_count)) for bg in range(0, bg_count): for fg in fg_range: curses.init_pair(pair, fg, bg) #try: # curses.init_extended_pair(pair, fg, bg) #except Exception as E: # pdb.set_trace() self.colorPairMap.update({(fg,bg):pair}) pair += 1 self.appState.totalFgColors = fg + 1 self.appState.totalBgColors = bg + 1 self.appState.totalFgColors = fg self.appState.totalBgColors = bg # set pair 0 fg 0 bg to default color: self.colorPairMap.update({(0,0):0}) return True except Exception as E: #debug_filename = 'debugy.txt' #debug_file = open(debug_filename, "w") #debug_file.write(str(E)) #debug_file.close() return False def initColorPairs_256color(self): # High color pairs, 256 color return self.initColorPairs_general(fg_count=256, bg_count=1) def initColorPairs_256color_dead_again(self): # High color pairs, 256 color """ Initialize 256 color mode color pairs """ self.colorPairMap = {} pair = 0 try: curses.use_default_colors() bg = 0 for bg in range(-1, 16): for fg in range(0, 256): curses.init_pair(pair, fg, bg) self.colorPairMap.update({(fg,bg):pair}) pair += 1 self.appState.totalFgColors = fg + 1 self.appState.totalBgColors = bg + 1 self.appState.totalFgColors = fg self.appState.totalBgColors = bg # set pair 0 fg 0 bg to default color: self.colorPairMap.update({(0,0):0}) return True except Exception as E: #debug_filename = 'debugy.txt' #debug_file = open(debug_filename, "w") #debug_file.write(str(self.colorPairMap)) #debug_file.close() return False def initColorPairs_cga(self, trans=False): if self.appState.iceColors: self.initColorPairs_ice_colors(trans=trans) else: self.initColorPairs_cga_old2(trans=trans) #return self.initColorPairs_general(fg_count=16, bg_count=16, use_order=True) #def initColorPairs_ice_colors(self): # 16 fg colors, 16 bg colors ice colors def initColorPairs_ice_colors(self, trans=False): # 16 fg colors, 16 bg colors """ Initialize 16 fg * 16 bg, or 256 color mode color pairs """ self.colorPairMap = {} # default user-friendly color order: # black, blue, green, cyan, red, magenta, yellow, white, 16 = nothing? if trans: defaultBg = -1 else: defaultBg = curses.COLOR_BLACK defaultBg = 0 #defaultBg = curses.COLOR_BLACK color_order = [-1, 0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15, 16] bg_order = [0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15, 16] try: curses.use_default_colors() bg = 0 map_fg = 0 map_bg = 0 #for bg in range(-1, 17): # for fg in range(-1, 17): #curses.init_pair(1, curses.COLOR_BLACK, bg) # black - 0 #self.colorPairMap.update({(curses.COLOR_BLACK,-1):1}) #curses.init_pair(1, curses.COLOR_BLUE, bg) # blue- 1 #self.colorPairMap.update({(curses.COLOR_BLUE,bg):2}) #curses.init_pair(2, curses.COLOR_GREEN, bg) # green - 2 #self.colorPairMap.update({(curses.COLOR_GREEN,bg):3}) #curses.init_pair(3, curses.COLOR_CYAN, bg) # cyan - 3 #self.colorPairMap.update({(curses.COLOR_CYAN,bg):4}) #curses.init_pair(4, curses.COLOR_RED, bg) # red - 4 #self.colorPairMap.update({(curses.COLOR_RED,bg):5}) #curses.init_pair(5, curses.COLOR_MAGENTA, bg) # magenta/purple - 5 #self.colorPairMap.update({(curses.COLOR_MAGENTA,bg):6}) #curses.init_pair(6, curses.COLOR_YELLOW, bg) # brown/yellow - 6 #self.colorPairMap.update({(curses.COLOR_YELLOW,bg):7}) #curses.init_pair(7, curses.COLOR_WHITE, bg) # white - 7 (and 0) #self.colorPairMap.update({(curses.COLOR_WHITE,bg):8}) # basic ncurses colors - comments for these are durdraw internal color numbers: curses.init_pair(0, curses.COLOR_BLACK, defaultBg) # black - 0 curses.init_pair(1, curses.COLOR_BLUE, defaultBg) # blue- 1 curses.init_pair(2, curses.COLOR_GREEN, defaultBg) # green - 2 curses.init_pair(3, curses.COLOR_CYAN, defaultBg) # cyan - 3 curses.init_pair(4, curses.COLOR_RED, defaultBg) # red - 4 curses.init_pair(5, curses.COLOR_MAGENTA, defaultBg) # magenta/purple - 5 curses.init_pair(6, curses.COLOR_YELLOW, defaultBg) # brown/yellow - 6 curses.init_pair(7, curses.COLOR_WHITE, defaultBg) # white - 7 (and 0) curses.init_pair(8, 7, defaultBg) # white - 7 (and 0) self.colorPairMap = { # foreground colors, black background (0,0):1, (1,0):1, (2,0):2, (3,0):3, (4,0):4, (5,0):5, (6,0):6, (7,0):7, (8,0):8, } # redo it fro scrarch: pair = 0 #bg += 1 bg = 0 for fg in color_order: for bg in bg_order: curses.init_pair(pair, fg, bg) self.colorPairMap.update({(map_fg,map_bg):pair}) pair += 1 map_fg += 1 map_bg += 1 map_fg = 0 self.appState.totalFgColors = fg + 1 self.appState.totalBgColors = bg + 1 self.appState.totalFgColors = fg self.appState.totalBgColors = bg # set pair 0 fg 0 bg to default color: self.colorPairMap.update({(0,0):0}) return True except Exception as E: #debug_filename = 'debugy.txt' #debug_file = open(debug_filename, "w") #debug_file.write(str(self.colorPairMap)) #debug_file.close() return False def initColorPairs_256color_beta(self): # High color pairs, 256 color """ Initialize 256 color mode color pairs """ self.colorPairMap = {} pair = 1 try: curses.use_default_colors() #self.initColorPairs_cga() #pair = 58 #for bg in range(9, 127): # for fg in range(9, curses.COLORS): for bg in range(0, 16): #for fg in range(0, curses.COLORS): for fg in range(0, 255): #debug_write(str(pair)) curses.init_pair(pair, fg, bg) self.colorPairMap.update({(fg,bg):pair}) pair += 1 #self.colorPairMap.update({(fg, bg):pair}) #curses.init_pair(i + 1, i, -1) # foreground only self.appState.totalFgColors = fg self.appState.totalBgColors = bg return True except Exception as E: #debug_filename = 'debugy.txt' #debug_file = open(debug_filename, "w") #debug_file.write(str(self.colorPairMap)) #debug_file.close() return False def initColorPairs_cga_old2(self, trans=False): """ Setup ncurses color pairs for ANSI colors """ # this kind of hurts to write. wtf, ncurses. if trans: defaultBg = -1 else: defaultBg = curses.COLOR_BLACK # basic ncurses colors - comments for these are durdraw internal color numbers: curses.init_pair(1, curses.COLOR_BLACK, defaultBg) # black - 0 curses.init_pair(2, curses.COLOR_BLUE, defaultBg) # blue- 1 curses.init_pair(3, curses.COLOR_GREEN, defaultBg) # green - 2 curses.init_pair(4, curses.COLOR_CYAN, defaultBg) # cyan - 3 curses.init_pair(5, curses.COLOR_RED, defaultBg) # red - 4 curses.init_pair(6, curses.COLOR_MAGENTA, defaultBg) # magenta/purple - 5 curses.init_pair(7, curses.COLOR_YELLOW, defaultBg) # brown/yellow - 6 curses.init_pair(8, curses.COLOR_WHITE, defaultBg) # white - 7 (and 0) # black with background colors curses.init_pair(9, curses.COLOR_BLACK, curses.COLOR_BLUE) # 1,2 curses.init_pair(10, curses.COLOR_BLACK, curses.COLOR_GREEN) # 1,3 curses.init_pair(11, curses.COLOR_BLACK, curses.COLOR_CYAN) # 1,4 curses.init_pair(12, curses.COLOR_BLACK, curses.COLOR_RED) # 1,5 curses.init_pair(13, curses.COLOR_BLACK, curses.COLOR_MAGENTA) # 1,6 curses.init_pair(14, curses.COLOR_BLACK, curses.COLOR_YELLOW) # 1,7 curses.init_pair(15, curses.COLOR_BLACK, curses.COLOR_WHITE) # 1,8 # blue with background colors curses.init_pair(16, curses.COLOR_BLUE, curses.COLOR_BLUE) # 2,2 curses.init_pair(17, curses.COLOR_BLUE, curses.COLOR_GREEN) # 2,3 curses.init_pair(18, curses.COLOR_BLUE, curses.COLOR_CYAN) # 2,4 curses.init_pair(19, curses.COLOR_BLUE, curses.COLOR_RED) # 2,5 curses.init_pair(20, curses.COLOR_BLUE, curses.COLOR_MAGENTA) # 2,6 curses.init_pair(21, curses.COLOR_BLUE, curses.COLOR_YELLOW) # 2,7 curses.init_pair(22, curses.COLOR_BLUE, curses.COLOR_WHITE) # 2,7 # green with background colors curses.init_pair(23, curses.COLOR_GREEN, curses.COLOR_BLUE) # 3,1 curses.init_pair(24, curses.COLOR_GREEN, curses.COLOR_GREEN) # 3,2 curses.init_pair(25, curses.COLOR_GREEN, curses.COLOR_CYAN) # 3,3 curses.init_pair(26, curses.COLOR_GREEN, curses.COLOR_RED) # 3,4 curses.init_pair(27, curses.COLOR_GREEN, curses.COLOR_MAGENTA) # 3,5 curses.init_pair(28, curses.COLOR_GREEN, curses.COLOR_YELLOW) # 3,6 curses.init_pair(29, curses.COLOR_GREEN, curses.COLOR_WHITE) # 3,7 # cyan with background colors curses.init_pair(30, curses.COLOR_CYAN, curses.COLOR_BLUE) # 4,1 curses.init_pair(31, curses.COLOR_CYAN, curses.COLOR_GREEN) # 4,2 curses.init_pair(32, curses.COLOR_CYAN, curses.COLOR_CYAN) # 4,3 curses.init_pair(33, curses.COLOR_CYAN, curses.COLOR_RED) # 4,4 curses.init_pair(34, curses.COLOR_CYAN, curses.COLOR_MAGENTA) # 4,5 curses.init_pair(35, curses.COLOR_CYAN, curses.COLOR_YELLOW) # 4,6 curses.init_pair(36, curses.COLOR_CYAN, curses.COLOR_WHITE) # 4,7 # yellow with background colors curses.init_pair(37, curses.COLOR_RED, curses.COLOR_BLUE) # 5,1 curses.init_pair(38, curses.COLOR_RED, curses.COLOR_GREEN) # 5,2 curses.init_pair(39, curses.COLOR_RED, curses.COLOR_CYAN) # 5,3 curses.init_pair(40, curses.COLOR_RED, curses.COLOR_RED) # 5,4 curses.init_pair(41, curses.COLOR_RED, curses.COLOR_MAGENTA) # 5,5 curses.init_pair(42, curses.COLOR_RED, curses.COLOR_YELLOW) # 5,6 curses.init_pair(43, curses.COLOR_RED, curses.COLOR_WHITE) # 5,7 # green with background colors curses.init_pair(44, curses.COLOR_MAGENTA, curses.COLOR_BLUE) # 6,1 curses.init_pair(45, curses.COLOR_MAGENTA, curses.COLOR_GREEN) # 6,2 curses.init_pair(46, curses.COLOR_MAGENTA, curses.COLOR_CYAN) # 6,3 curses.init_pair(47, curses.COLOR_MAGENTA, curses.COLOR_RED) # 6,4 curses.init_pair(48, curses.COLOR_MAGENTA, curses.COLOR_MAGENTA) # 6,5 curses.init_pair(49, curses.COLOR_MAGENTA, curses.COLOR_YELLOW) # 6,6 curses.init_pair(50, curses.COLOR_MAGENTA, curses.COLOR_WHITE) # 6,7 # red with background colors curses.init_pair(51, curses.COLOR_YELLOW, curses.COLOR_BLUE) # 7,1 curses.init_pair(52, curses.COLOR_YELLOW, curses.COLOR_GREEN) # 7,2 curses.init_pair(53, curses.COLOR_YELLOW, curses.COLOR_CYAN) # 7,3 curses.init_pair(54, curses.COLOR_YELLOW, curses.COLOR_RED) # 7,4 curses.init_pair(55, curses.COLOR_YELLOW, curses.COLOR_MAGENTA) # 7,5 curses.init_pair(56, curses.COLOR_YELLOW, curses.COLOR_YELLOW) # 7,6 curses.init_pair(57, curses.COLOR_YELLOW, curses.COLOR_WHITE) # 7,7 # black with background colors curses.init_pair(58, curses.COLOR_WHITE, curses.COLOR_BLUE) # 8,1 curses.init_pair(59, curses.COLOR_WHITE, curses.COLOR_GREEN) # 8,2 curses.init_pair(60, curses.COLOR_WHITE, curses.COLOR_CYAN) # 8,3 curses.init_pair(61, curses.COLOR_WHITE, curses.COLOR_RED) # 8,4 curses.init_pair(62, curses.COLOR_WHITE, curses.COLOR_MAGENTA) # 8,5 curses.init_pair(63, curses.COLOR_WHITE, curses.COLOR_YELLOW) # 8,6 curses.init_pair(64, curses.COLOR_WHITE, curses.COLOR_WHITE) # 8,7 #curses.init_pair(64, defaultBg, curses.COLOR_RED) # 8,7 # ^ this doesn't work ?!@ ncurses pair # must be between 1 and 63 # or ncurses (const?) COLOR_PAIR - 1 # fix is: have functions to swap color map from blackfg to normal. # call that function when drawing if the fg color == black, then switch back # after each character. Or.. keep track which map we're in in a variable. self.colorPairMap = { # foreground colors, black background (0,0):1, (1,0):1, (2,0):2, (3,0):3, (4,0):4, (5,0):5, (6,0):6, (7,0):7, (8,0):8, # and again, because black == both 0 and 8. :| let's just ditch 0? (0,8):1, (1,8):1, (2,8):2, (3,8):3, (4,8):4, (5,8):5, (6,8):6, (7,8):7, (8,8):8, # white with backround colors (1,1):9, (1,2):10, (1,3):11, (1,4):12, (1,5):13, (1,6):14, (1,7):15, # cyan with backround colors (2,1):16, (2,2):17, (2,3):18, (2,4):19, (2,5):20, (2,6):21, (2,7):22, # magenta with background colors (3,1):23, (3,2):24, (3,3):25, (3,4):26, (3,5):27, (3,6):28, (3,7):29, # blue with background colors (4,1):30, (4,2):31, (4,3):32, (4,4):33, (4,5):34, (4,6):35, (4,7):36, # yellow with background colors (5,1):37, (5,2):38, (5,3):39, (5,4):40, (5,5):41, (5,6):42, (5,7):43, # green with background colors (6,1):44, (6,2):45, (6,3):46, (6,4):47, (6,5):48, (6,6):49, (6,7):50, # red with background colors (7,1):51, (7,2):52, (7,3):53, (7,4):54, (7,5):55, (7,6):56, (7,7):57, # black with background colors (8,1):58, (8,2):59, (8,3):60, (8,4):61, (8,5):62, (8,6):63, #(8,7):57, # 57 instead of 64, because we switch color maps for black # on red (8,7):64, # Again, this time with feeling (0,1):58, (0,2):59, (0,3):60, (0,4):61, (0,5):62, (0,6):63, (0,7):57, # BRIGHT COLORS 9-16 # white with backround colors (9,0):1, (9,8):1, (9,1):9, (9,2):10, (9,3):11, (9,4):12, (9,5):13, (9,6):14, (9,7):15, # cyan with backround colors (10,0):2, (10,8):2, (10,1):16, (10,2):17, (10,3):18, (10,4):19, (10,5):20, (10,6):21, (10,7):22, # magenta with background colors (11,0):3, (11,8):3, (11,1):23, (11,2):24, (11,3):25, (11,4):26, (11,5):27, (11,6):28, (11,7):29, # blue with background colors (12,0):4, (12,8):4, (12,1):30, (12,2):31, (12,3):32, (12,4):33, (12,5):34, (12,6):35, (12,7):36, # yellow with background colors (13,0):5, (13,8):5, (13,1):37, (13,2):38, (13,3):39, (13,4):40, (13,5):41, (13,6):42, (13,7):43, # green with background colors (14,0):6, (14,8):6, (14,1):44, (14,2):45, (14,3):46, (14,4):47, (14,5):48, (14,6):49, (14,7):50, # red with background colors (15,0):7, (15,8):7, (15,1):51, (15,2):52, (15,3):53, (15,4):54, (15,5):55, (15,6):56, (15,7):57, # black with background colors (16,0):8, (16,8):8, (16,1):58, (16,2):59, (16,3):60, (16,4):61, (16,5):62, (16,6):63, #(16,7):57, # 57 instead of 64, because we switch color maps for black (16,7):64, } # (fg,bg):cursespair def initColorPairs_cga_old(self): """ Setup ncurses color pairs for ANSI colors """ # this kind of hurts to write. wtf, ncurses. # basic ncurses colors - comments for these are durdraw internal color numbers: curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK) # white - 1 curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK) # cyan - 2 curses.init_pair(3, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # magenta - 3 curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) # blue - 4 curses.init_pair(5, curses.COLOR_YELLOW, curses.COLOR_BLACK) # yellow - 5 curses.init_pair(6, curses.COLOR_GREEN, curses.COLOR_BLACK) # green - 6 curses.init_pair(7, curses.COLOR_RED, curses.COLOR_BLACK) # red - 7 curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_BLACK) # black - 8 (and 0) # white with background colors curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_WHITE) # 1,1 curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_CYAN) # 1,2 curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_MAGENTA) # 1,3 curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_BLUE) # 1,4 curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_YELLOW) # 1,5 curses.init_pair(14, curses.COLOR_WHITE, curses.COLOR_GREEN) # 1,6 curses.init_pair(15, curses.COLOR_WHITE, curses.COLOR_RED) # 1,7 # cyan with background colors curses.init_pair(16, curses.COLOR_CYAN, curses.COLOR_WHITE) # 2,1 curses.init_pair(17, curses.COLOR_CYAN, curses.COLOR_CYAN) # 2,2 curses.init_pair(18, curses.COLOR_CYAN, curses.COLOR_MAGENTA) # 2,3 curses.init_pair(19, curses.COLOR_CYAN, curses.COLOR_BLUE) # 2,4 curses.init_pair(20, curses.COLOR_CYAN, curses.COLOR_YELLOW) # 2,5 curses.init_pair(21, curses.COLOR_CYAN, curses.COLOR_GREEN) # 2,6 curses.init_pair(22, curses.COLOR_CYAN, curses.COLOR_RED) # 2,7 # magenta with background colors curses.init_pair(23, curses.COLOR_MAGENTA, curses.COLOR_WHITE) # 3,1 curses.init_pair(24, curses.COLOR_MAGENTA, curses.COLOR_CYAN) # 3,2 curses.init_pair(25, curses.COLOR_MAGENTA, curses.COLOR_MAGENTA) # 3,3 curses.init_pair(26, curses.COLOR_MAGENTA, curses.COLOR_BLUE) # 3,4 curses.init_pair(27, curses.COLOR_MAGENTA, curses.COLOR_YELLOW) # 3,5 curses.init_pair(28, curses.COLOR_MAGENTA, curses.COLOR_GREEN) # 3,6 curses.init_pair(29, curses.COLOR_MAGENTA, curses.COLOR_RED) # 3,7 # blue with background colors curses.init_pair(30, curses.COLOR_BLUE, curses.COLOR_WHITE) # 4,1 curses.init_pair(31, curses.COLOR_BLUE, curses.COLOR_CYAN) # 4,2 curses.init_pair(32, curses.COLOR_BLUE, curses.COLOR_MAGENTA) # 4,3 curses.init_pair(33, curses.COLOR_BLUE, curses.COLOR_BLUE) # 4,4 curses.init_pair(34, curses.COLOR_BLUE, curses.COLOR_YELLOW) # 4,5 curses.init_pair(35, curses.COLOR_BLUE, curses.COLOR_GREEN) # 4,6 curses.init_pair(36, curses.COLOR_BLUE, curses.COLOR_RED) # 4,7 # yellow with background colors curses.init_pair(37, curses.COLOR_YELLOW, curses.COLOR_WHITE) # 5,1 curses.init_pair(38, curses.COLOR_YELLOW, curses.COLOR_CYAN) # 5,2 curses.init_pair(39, curses.COLOR_YELLOW, curses.COLOR_MAGENTA) # 5,3 curses.init_pair(40, curses.COLOR_YELLOW, curses.COLOR_BLUE) # 5,4 curses.init_pair(41, curses.COLOR_YELLOW, curses.COLOR_YELLOW) # 5,5 curses.init_pair(42, curses.COLOR_YELLOW, curses.COLOR_GREEN) # 5,6 curses.init_pair(43, curses.COLOR_YELLOW, curses.COLOR_RED) # 5,7 # green with background colors curses.init_pair(44, curses.COLOR_GREEN, curses.COLOR_WHITE) # 6,1 curses.init_pair(45, curses.COLOR_GREEN, curses.COLOR_CYAN) # 6,2 curses.init_pair(46, curses.COLOR_GREEN, curses.COLOR_MAGENTA) # 6,3 curses.init_pair(47, curses.COLOR_GREEN, curses.COLOR_BLUE) # 6,4 curses.init_pair(48, curses.COLOR_GREEN, curses.COLOR_YELLOW) # 6,5 curses.init_pair(49, curses.COLOR_GREEN, curses.COLOR_GREEN) # 6,6 curses.init_pair(50, curses.COLOR_GREEN, curses.COLOR_RED) # 6,7 # red with background colors curses.init_pair(51, curses.COLOR_RED, curses.COLOR_WHITE) # 7,1 curses.init_pair(52, curses.COLOR_RED, curses.COLOR_CYAN) # 7,2 curses.init_pair(53, curses.COLOR_RED, curses.COLOR_MAGENTA) # 7,3 curses.init_pair(54, curses.COLOR_RED, curses.COLOR_BLUE) # 7,4 curses.init_pair(55, curses.COLOR_RED, curses.COLOR_YELLOW) # 7,5 curses.init_pair(56, curses.COLOR_RED, curses.COLOR_GREEN) # 7,6 #curses.init_pair(57, curses.COLOR_RED, curses.COLOR_RED) # 7,7 # black with background colors curses.init_pair(58, curses.COLOR_BLACK, curses.COLOR_WHITE) # 8,1 curses.init_pair(59, curses.COLOR_BLACK, curses.COLOR_CYAN) # 8,2 curses.init_pair(60, curses.COLOR_BLACK, curses.COLOR_MAGENTA) # 8,3 curses.init_pair(61, curses.COLOR_BLACK, curses.COLOR_BLUE) # 8,4 curses.init_pair(62, curses.COLOR_BLACK, curses.COLOR_YELLOW) # 8,5 curses.init_pair(63, curses.COLOR_BLACK, curses.COLOR_GREEN) # 8,6 curses.init_pair(57, curses.COLOR_BLACK, curses.COLOR_RED) # 8,7 #curses.init_pair(64, curses.COLOR_BLACK, curses.COLOR_RED) # 8,7 # ^ this doesn't work ?!@ ncurses pair # must be between 1 and 63 # or ncurses (const?) COLOR_PAIR - 1 # fix is: have functions to swap color map from blackfg to normal. # call that function when drawing if the fg color == black, then switch back # after each character. Or.. keep track which map we're in in a variable. self.colorPairMap = { # foreground colors, black background (0,0):1, (1,0):1, (2,0):2, (3,0):3, (4,0):4, (5,0):5, (6,0):6, (7,0):7, (8,0):8, # and again, because black == both 0 and 8. :| let's just ditch 0? (0,8):1, (1,8):1, (2,8):2, (3,8):3, (4,8):4, (5,8):5, (6,8):6, (7,8):7, (8,8):8, # white with backround colors (1,1):9, (1,2):10, (1,3):11, (1,4):12, (1,5):13, (1,6):14, (1,7):15, # cyan with backround colors (2,1):16, (2,2):17, (2,3):18, (2,4):19, (2,5):20, (2,6):21, (2,7):22, # magenta with background colors (3,1):23, (3,2):24, (3,3):25, (3,4):26, (3,5):27, (3,6):28, (3,7):29, # blue with background colors (4,1):30, (4,2):31, (4,3):32, (4,4):33, (4,5):34, (4,6):35, (4,7):36, # yellow with background colors (5,1):37, (5,2):38, (5,3):39, (5,4):40, (5,5):41, (5,6):42, (5,7):43, # green with background colors (6,1):44, (6,2):45, (6,3):46, (6,4):47, (6,5):48, (6,6):49, (6,7):50, # red with background colors (7,1):51, (7,2):52, (7,3):53, (7,4):54, (7,5):55, (7,6):56, (7,7):57, # black with background colors (8,1):58, (8,2):59, (8,3):60, (8,4):61, (8,5):62, (8,6):63, (8,7):57, # 57 instead of 64, because we switch color maps for black # on red # BRIGHT COLORS 9-16 # white with backround colors (9,0):1, (9,8):1, (9,1):9, (9,2):10, (9,3):11, (9,4):12, (9,5):13, (9,6):14, (9,7):15, # cyan with backround colors (10,0):2, (10,8):2, (10,1):16, (10,2):17, (10,3):18, (10,4):19, (10,5):20, (10,6):21, (10,7):22, # magenta with background colors (11,0):3, (11,8):3, (11,1):23, (11,2):24, (11,3):25, (11,4):26, (11,5):27, (11,6):28, (11,7):29, # blue with background colors (12,0):4, (12,8):4, (12,1):30, (12,2):31, (12,3):32, (12,4):33, (12,5):34, (12,6):35, (12,7):36, # yellow with background colors (13,0):5, (13,8):5, (13,1):37, (13,2):38, (13,3):39, (13,4):40, (13,5):41, (13,6):42, (13,7):43, # green with background colors (14,0):6, (14,8):6, (14,1):44, (14,2):45, (14,3):46, (14,4):47, (14,5):48, (14,6):49, (14,7):50, # red with background colors (15,0):7, (15,8):7, (15,1):51, (15,2):52, (15,3):53, (15,4):54, (15,5):55, (15,6):56, (15,7):57, # black with background colors (16,0):8, (16,8):8, (16,1):58, (16,2):59, (16,3):60, (16,4):61, (16,5):62, (16,6):63, (16,7):57, # 57 instead of 64, because we switch color maps for black } # (fg,bg):cursespair def initColorPairs(self): """ Setup ncurses color pairs for ANSI colors """ # this kind of hurts to write. wtf, ncurses. # basic ncurses colors - comments for these are durdraw internal color numbers: curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK) # white - 1 curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK) # cyan - 2 curses.init_pair(3, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # magenta - 3 curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) # blue - 4 curses.init_pair(5, curses.COLOR_YELLOW, curses.COLOR_BLACK) # yellow - 5 curses.init_pair(6, curses.COLOR_GREEN, curses.COLOR_BLACK) # green - 6 curses.init_pair(7, curses.COLOR_RED, curses.COLOR_BLACK) # red - 7 curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_BLACK) # black - 8 (and 0) # white with background colors curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_WHITE) # 1,1 curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_CYAN) # 1,2 curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_MAGENTA) # 1,3 curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_BLUE) # 1,4 curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_YELLOW) # 1,5 curses.init_pair(14, curses.COLOR_WHITE, curses.COLOR_GREEN) # 1,6 curses.init_pair(15, curses.COLOR_WHITE, curses.COLOR_RED) # 1,7 # cyan with background colors curses.init_pair(16, curses.COLOR_CYAN, curses.COLOR_WHITE) # 2,1 curses.init_pair(17, curses.COLOR_CYAN, curses.COLOR_CYAN) # 2,2 curses.init_pair(18, curses.COLOR_CYAN, curses.COLOR_MAGENTA) # 2,3 curses.init_pair(19, curses.COLOR_CYAN, curses.COLOR_BLUE) # 2,4 curses.init_pair(20, curses.COLOR_CYAN, curses.COLOR_YELLOW) # 2,5 curses.init_pair(21, curses.COLOR_CYAN, curses.COLOR_GREEN) # 2,6 curses.init_pair(22, curses.COLOR_CYAN, curses.COLOR_RED) # 2,7 # magenta with background colors curses.init_pair(23, curses.COLOR_MAGENTA, curses.COLOR_WHITE) # 3,1 curses.init_pair(24, curses.COLOR_MAGENTA, curses.COLOR_CYAN) # 3,2 curses.init_pair(25, curses.COLOR_MAGENTA, curses.COLOR_MAGENTA) # 3,3 curses.init_pair(26, curses.COLOR_MAGENTA, curses.COLOR_BLUE) # 3,4 curses.init_pair(27, curses.COLOR_MAGENTA, curses.COLOR_YELLOW) # 3,5 curses.init_pair(28, curses.COLOR_MAGENTA, curses.COLOR_GREEN) # 3,6 curses.init_pair(29, curses.COLOR_MAGENTA, curses.COLOR_RED) # 3,7 # blue with background colors curses.init_pair(30, curses.COLOR_BLUE, curses.COLOR_WHITE) # 4,1 curses.init_pair(31, curses.COLOR_BLUE, curses.COLOR_CYAN) # 4,2 curses.init_pair(32, curses.COLOR_BLUE, curses.COLOR_MAGENTA) # 4,3 curses.init_pair(33, curses.COLOR_BLUE, curses.COLOR_BLUE) # 4,4 curses.init_pair(34, curses.COLOR_BLUE, curses.COLOR_YELLOW) # 4,5 curses.init_pair(35, curses.COLOR_BLUE, curses.COLOR_GREEN) # 4,6 curses.init_pair(36, curses.COLOR_BLUE, curses.COLOR_RED) # 4,7 # yellow with background colors curses.init_pair(37, curses.COLOR_YELLOW, curses.COLOR_WHITE) # 5,1 curses.init_pair(38, curses.COLOR_YELLOW, curses.COLOR_CYAN) # 5,2 curses.init_pair(39, curses.COLOR_YELLOW, curses.COLOR_MAGENTA) # 5,3 curses.init_pair(40, curses.COLOR_YELLOW, curses.COLOR_BLUE) # 5,4 curses.init_pair(41, curses.COLOR_YELLOW, curses.COLOR_YELLOW) # 5,5 curses.init_pair(42, curses.COLOR_YELLOW, curses.COLOR_GREEN) # 5,6 curses.init_pair(43, curses.COLOR_YELLOW, curses.COLOR_RED) # 5,7 # green with background colors curses.init_pair(44, curses.COLOR_GREEN, curses.COLOR_WHITE) # 6,1 curses.init_pair(45, curses.COLOR_GREEN, curses.COLOR_CYAN) # 6,2 curses.init_pair(46, curses.COLOR_GREEN, curses.COLOR_MAGENTA) # 6,3 curses.init_pair(47, curses.COLOR_GREEN, curses.COLOR_BLUE) # 6,4 curses.init_pair(48, curses.COLOR_GREEN, curses.COLOR_YELLOW) # 6,5 curses.init_pair(49, curses.COLOR_GREEN, curses.COLOR_GREEN) # 6,6 curses.init_pair(50, curses.COLOR_GREEN, curses.COLOR_RED) # 6,7 # red with background colors curses.init_pair(51, curses.COLOR_RED, curses.COLOR_WHITE) # 7,1 curses.init_pair(52, curses.COLOR_RED, curses.COLOR_CYAN) # 7,2 curses.init_pair(53, curses.COLOR_RED, curses.COLOR_MAGENTA) # 7,3 curses.init_pair(54, curses.COLOR_RED, curses.COLOR_BLUE) # 7,4 curses.init_pair(55, curses.COLOR_RED, curses.COLOR_YELLOW) # 7,5 curses.init_pair(56, curses.COLOR_RED, curses.COLOR_GREEN) # 7,6 #curses.init_pair(57, curses.COLOR_RED, curses.COLOR_RED) # 7,7 # black with background colors # black with background colors curses.init_pair(58, curses.COLOR_BLACK, curses.COLOR_WHITE) # 8,1 curses.init_pair(59, curses.COLOR_BLACK, curses.COLOR_CYAN) # 8,2 curses.init_pair(60, curses.COLOR_BLACK, curses.COLOR_MAGENTA) # 8,3 curses.init_pair(61, curses.COLOR_BLACK, curses.COLOR_BLUE) # 8,4 curses.init_pair(62, curses.COLOR_BLACK, curses.COLOR_YELLOW) # 8,5 curses.init_pair(63, curses.COLOR_BLACK, curses.COLOR_GREEN) # 8,6 curses.init_pair(57, curses.COLOR_BLACK, curses.COLOR_RED) # 8,7 #curses.init_pair(64, curses.COLOR_BLACK, curses.COLOR_RED) # 8,7 # ^ this doesn't work ?!@ ncurses pair # must be between 1 and 63 # or ncurses (const?) COLOR_PAIR - 1 # fix is: have functions to swap color map from blackfg to normal. # call that function when drawing if the fg color == black, then switch back # after each character. Or.. keep track which map we're in in a variable. self.colorPairMap = { # foreground colors, black background (0,0):1, (1,0):1, (2,0):2, (3,0):3, (4,0):4, (5,0):5, (6,0):6, (7,0):7, (8,0):8, # and again, because black == both 0 and 8. :| let's just ditch 0? (0,8):1, (1,8):1, (2,8):2, (3,8):3, (4,8):4, (5,8):5, (6,8):6, (7,8):7, (8,8):8, # white with backround colors (1,1):9, (1,2):10, (1,3):11, (1,4):12, (1,5):13, (1,6):14, (1,7):15, # cyan with backround colors (2,1):16, (2,2):17, (2,3):18, (2,4):19, (2,5):20, (2,6):21, (2,7):22, # magenta with background colors (3,1):23, (3,2):24, (3,3):25, (3,4):26, (3,5):27, (3,6):28, (3,7):29, # blue with background colors (4,1):30, (4,2):31, (4,3):32, (4,4):33, (4,5):34, (4,6):35, (4,7):36, # yellow with background colors (5,1):37, (5,2):38, (5,3):39, (5,4):40, (5,5):41, (5,6):42, (5,7):43, # green with background colors (6,1):44, (6,2):45, (6,3):46, (6,4):47, (6,5):48, (6,6):49, (6,7):50, # red with background colors (7,1):51, (7,2):52, (7,3):53, (7,4):54, (7,5):55, (7,6):56, (7,7):57, # black with background colors (8,1):58, (8,2):59, (8,3):60, (8,4):61, (8,5):62, (8,6):63, (8,7):57, # 57 instead of 64, because we switch color maps for black # on red # BRIGHT COLORS 9-16 # white with backround colors (9,0):1, (9,8):1, (9,1):9, (9,2):10, (9,3):11, (9,4):12, (9,5):13, (9,6):14, (9,7):15, # cyan with backround colors (10,0):2, (10,8):2, (10,1):16, (10,2):17, (10,3):18, (10,4):19, (10,5):20, (10,6):21, (10,7):22, # magenta with background colors (11,0):3, (11,8):3, (11,1):23, (11,2):24, (11,3):25, (11,4):26, (11,5):27, (11,6):28, (11,7):29, # blue with background colors (12,0):4, (12,8):4, (12,1):30, (12,2):31, (12,3):32, (12,4):33, (12,5):34, (12,6):35, (12,7):36, # yellow with background colors (13,0):5, (13,8):5, (13,1):37, (13,2):38, (13,3):39, (13,4):40, (13,5):41, (13,6):42, (13,7):43, # green with background colors (14,0):6, (14,8):6, (14,1):44, (14,2):45, (14,3):46, (14,4):47, (14,5):48, (14,6):49, (14,7):50, # red with background colors (15,0):7, (15,8):7, (15,1):51, (15,2):52, (15,3):53, (15,4):54, (15,5):55, (15,6):56, (15,7):57, # black with background colors (16,0):8, (16,8):8, (16,1):58, (16,2):59, (16,3):60, (16,4):61, (16,5):62, (16,6):63, (16,7):57, # 57 instead of 64, because we switch color maps for black } # (fg,bg):cursespair durdraw-0.29.0/durdraw/durdraw_file.py000066400000000000000000000321501476305210600177500ustar00rootroot00000000000000# Durdraw file operations - stuff related to open, save, etc import datetime import gzip import os import pdb import pickle import json from durdraw.durdraw_options import Options from durdraw.durdraw_movie import Movie # import durdraw.durdraw_ansiparse as ansiparse old_16_pal_to_new = { # maps old durdraw colors, found in file version 5 and lower # for 16 color 0: 0, # black 1: 7, # white/grey 2: 3, # cyan 3: 5, # magenta/purple 4: 1, # blue 5: 6, # brown/yellow 6: 2, # green 7: 4, # red 8: 0, # black 9: 15, # bright white 10: 11, # bright cyan 11: 13, # bright magenta 12: 9, # bright blue 13: 14, # bright yellow 14: 10, # bright green 15: 12, # bright red 16: 16, # bright black } old_256_pal_to_new = { # maps old durdraw colors, found in file version 5 and lower # for 16 color 0: 0, # black 1: 7, # white/grey 2: 3, # cyan 3: 5, # magenta/purple 4: 1, # blue 5: 6, # brown/yellow 6: 2, # green 7: 4, # red 8: 241, # bright black 9: 8, # bright red 10: 9, # bright green 11: 10, # bright yellow 12: 11, # bright blue 13: 12, # bright magenta 14: 13, # bright cyan 15: 14, # bright white 16: 15, # bright white } colors_to_html = { # Maps Durdraw color numbers to HTML values 0: '#000000', # black 1: '#000000', # black 2: '#0000AA', # blue 3: '#00AA00', # green 4: '#00AAAA', # cyan 5: '#AA0000', # red 6: '#AA00AA', # magenta 7: '#AA5500', # brown/dark yellow 8: '#AAAAAA', # light gray/white 9: '#555555', # dark grey 10: '#5555FF', # bright blue 11: '#55FF55', # bright green 12: '#55FFFF', # bright cyan 13: '#FF5555', # bright red 14: '#FF55FF', # bright magenta 15: '#FFFF00', # bright yellow 16: '#FFFFFF', # bright white } def write_frame_to_html_file(mov, appState, frame, file_path, gzipped=False): """ Writes a single frame to an HTML file """ colorMode = appState.colorMode if gzipped: opener = gzip.open else: opener = open with opener(file_path, 'wt') as f: movieDataHeader = '\n' movieDataHeader += '\n' movieDataHeader += '' movieDataHeader += ' \n' movieDataHeader += ' \n' movieDataHeader += ' \n' movieDataHeader += ' DurDraw Generated Output\n' movieDataHeader += ' \n' movieDataHeader += ' \n' movieDataHeader += ' \n' movieDataHeader += '
\n'

        fileContent = movieDataHeader
        #lineNum = 0
        #colNum = 0
        newColorMap = []
        for posY in range(0, mov.sizeY):
            #newColorMap.append(list())
            for posX in range(0, mov.sizeX):
                #newColorMap[posX].append(list(frame.colorMap[posY, posX]))
                #newColorMap[posX].append(frame.newColorMap[posY][posX])
                durDolor = frame.newColorMap[posY][posX]
                fgColor = frame.newColorMap[posY][posX][0]
                bgColor = frame.newColorMap[posY][posX][1]
                c = frame.content[posY][posX]
        #for line in frame.content:
        #    for c in line: 
                try:
                    #fgColor = frame.newColorMap[colNum][lineNum][0]
                    #bgColor = frame.newColorMap[colNum][lineNum][1]
                    if appState.colorMode == "16":
                        bgColor += 1
                        if bgColor == 9:    # black duplicate
                            bgColor = 0
                    if appState.colorMode == "256":
                        fgColor += 1
                        bgColor += 1
                    if fgColor in colors_to_html.keys():
                        fgHtmlColor = colors_to_html[fgColor]
                    else:
                        fgHtmlColor = '#AAAAAA'
                    if bgColor in colors_to_html.keys():
                        bgHtmlColor = colors_to_html[bgColor]
                    else:
                        bgHtmlColor = '#000000'
                    #fileContent += f"
" fileContent += f"" #fileContent += f"" #fileContent += f"" fileContent += str(c) fileContent += "" #fileContent += "
" #colNum += 1 except Exception as e: print(str(e)) pdb.set_trace() fileContent += '\n' #lineNum += 1 fileContent += '
\n' fileContent += ' \n' fileContent += '\n' f.write(fileContent) f.close() def serialize_to_json_file(opts, appState, movie, file_path, gzipped=True): """ Takes live Durdraw movie objects and serializes them out to a JSON files """ colorMode = appState.colorMode if gzipped: opener = gzip.open else: opener = open with opener(file_path, 'wt') as f: movieDataHeader = { 'formatVersion': opts.saveFileFormat, 'colorFormat': colorMode, # 16, 256 'preferredFont': 'fixed', # fixed, vga, amiga, etc. 'encoding': appState.charEncoding, 'name': '', 'artist': '', 'framerate': opts.framerate, 'sizeX': movie.sizeX, 'sizeY': movie.sizeY, 'extra': None, 'frames': None, } frameNumber = 1 fullMovie = {'DurMovie': movieDataHeader} fullMovieFrames = [] for frame in movie.frames: content = '' newFrame = [] newColorMap = [] for posX in range(0, movie.sizeX): newColorMap.append(list()) for posY in range(0, movie.sizeY): #newColorMap[posX].append(list(frame.colorMap[posY, posX])) try: newColorMap[posX].append(frame.newColorMap[posY][posX]) except Exception as E: print(E) pdb.set_trace() for line in frame.content: content = ''.join(line) newFrame.append(content) serialized_frame = { 'frameNumber': frameNumber, 'delay': frame.delay, 'contents': newFrame, 'colorMap': newColorMap, } fullMovieFrames.append(serialized_frame) frameNumber += 1 fullMovie['DurMovie']['frames'] = fullMovieFrames fullMovieJSON = json.dumps(fullMovie, indent=2) newMovieJSON = clean_up_json_output(fullMovieJSON) f.write(newMovieJSON) f.close() def clean_up_json_output(json_str): """ Take aggressively whitespaced nested JSON lists and remove some of the excessive newlines and spaces. Basically format colorMap to look nicer """ json_data = json_str json_data = json_data.replace("[\n [", "[[") json_data = json_data.replace("[\n ", "[") json_data = json_data.replace(",\n ", ",") json_data = json_data.replace("\n ],", "],") json_data = json_data.replace("],\n [", "],[") json_data = json_data.replace("\n ]", "]") json_data = json_data.replace("\n ],", "],") return json_data def get_file_mod_date_time(filename): """ Returns a string like this: 2009-10-06 10:50:01 """ t = os.path.getmtime(filename) return str(datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc))[:19] def get_dur_file_colorMode_and_charMode(f): """ Returns the color mode and encoding used by the file """ f.seek(0) try: loadedMovieData = json.load(f) except Exception as e: return False colorMode = loadedMovieData['DurMovie']['colorFormat'] try: charEncoding = loadedMovieData['DurMovie']['encoding'] except Exception as e: charEncoding = 'utf-8' # convert from file format 5 to 6 #if colorMode == 'xterm-256': # colorMode = '256' if charEncoding == 'Utf-8': charEncoding = 'utf-8' return colorMode, charEncoding def open_json_dur_file(f, appState): """ Loads json file into opts and movie objects. Takes an open file object. Returns the opts and mov objects, encased in a list object. Or return False if the open fails. """ f.seek(0) try: loadedMovieData = json.load(f) except Exception as e: return False width = loadedMovieData['DurMovie']['sizeX'] height = loadedMovieData['DurMovie']['sizeY'] colorMode = loadedMovieData['DurMovie']['colorFormat'] newOpts = Options(width=width, height=height) newOpts.framerate = loadedMovieData['DurMovie']['framerate'] newOpts.saveFileFormat = loadedMovieData['DurMovie']['formatVersion'] # load frames into a new movie object newMov = Movie(newOpts) currentFrame = 0 lineNum = 0 for frame in loadedMovieData['DurMovie']['frames']: #newMov.insertCloneFrame() newMov.addEmptyFrame() newMov.nextFrame() for line in frame['contents']: #pdb.set_trace() if lineNum < len(newMov.frames[currentFrame].content): newMov.frames[currentFrame].content[lineNum] = [c for c in line] #newMov.frames[currentFrame].content[lineNum] = [c for c in line.encode(appState.charEncoding)] #newMov.frames[currentFrame].content[lineNum] = [c for c in bytes(line).decode('cp437')] #newMov.frames[currentFrame].content = line lineNum += 1 lineNum = 0 # load color map into new movie #pdb.set_trace() for x in range(0, width): for y in range(0, height): #pdb.set_trace() colorPair = frame['colorMap'][x][y] if appState.colorMode == '16' and colorMode == '256': # set a default color when down-converting color modes: if colorPair[0] > 16: colorPair = [8,0] #if newOpts.saveFileFormat < 6: # colorPair = convert_old_color_to_new(oldColorPair) # if colorPair == None: # pdb.set_trace() newMov.frames[currentFrame].colorMap[y, x] = tuple(colorPair) newMov.frames[currentFrame].newColorMap[y][x] = colorPair # Add delay for the frame newMov.frames[currentFrame].delay = frame['delay'] currentFrame += 1 #pdb.set_trace() newMov.deleteCurrentFrame() newMov.gotoFrame(1) container = {'opts':newOpts, 'mov':newMov} return container def convert_old_color_to_new(oldPair, colorMode="16"): """ takes in a frame['colorMap'][x][y] list, returns a new list with [0] replaced by appropriately mapped # """ #newPair = oldPair newFg = oldPair[0] newBg = oldPair[1] if colorMode == "16": if oldPair[0] in old_16_pal_to_new.keys(): newFg = old_16_pal_to_new[oldPair[0]] newFg += 1 if oldPair[1] in old_16_pal_to_new.keys(): newBg = old_16_pal_to_new[oldPair[1]] #newBg += 1 if colorMode == "256": if oldPair[0] in old_256_pal_to_new.keys(): newFg = old_256_pal_to_new[oldPair[0]] newFg += 1 #if oldPair[1] in old_16_pal_to_new.keys(): # newBg = old_16_pal_to_new[oldPair[1]] newBg = oldPair[1] # 256 does not use bg color, so don't change it #pdb.set_trace() return [newFg, newBg] class DurUnpickler(pickle.Unpickler): """" Custom Unpickler to remove serialized module names (like __main__) from unpickled data and replace them with "durdraw.durdraw_movie" """ def find_class(self, module, name): #if module == "__main__": module = "durdraw.durdraw_movie" return super().find_class(module, name) durdraw-0.29.0/durdraw/durdraw_gui_manager.py000066400000000000000000000013721476305210600213110ustar00rootroot00000000000000# Does things like: Takes a mouse click command. eg: left click on X, y. # Figures out which widget got clicked (if any), tells the widget to run # its onClick # Eventually this should also do things like tell the GUI handler (curses # or kivy) to initialize from durdraw.durdraw_gui_manager_curses import GuiHandler class Gui(): def __init__(self, guiType=None, window=None): self.window = window self.handler = GuiHandler(self) self.widgets = [] self.buttons = [] def add_button(self, widget): self.buttons.append(widget) def del_button(self, widget): self.buttons.remove(widget) def got_click(self, clickType, x, y, extra=None): self.handler.got_click(clickType, x, y, extra=None) durdraw-0.29.0/durdraw/durdraw_gui_manager_curses.py000066400000000000000000000013431476305210600226730ustar00rootroot00000000000000# Takes clicks from gui_manager's Gui() objects and.. does stuff? import pdb class GuiHandler(): def __init__(self, gui): self.gui = gui self.window = gui.window self.debugLine = 30 def got_click(self, clickType, y, x, extra=None): #self.window.addstr(self.debugLine, 0, f"Clicked: {x}, {y}, {clickType}.") # if a button was clicked, tell it: for button in self.gui.buttons: if x == button.realX: # if it's on the line and \/ within width area if (y >= button.realY) and (y <= button.realY + button.width + 1): #self.window.addstr(33 + z, 0, f"Clicky") #pdb.set_trace() button.on_click() durdraw-0.29.0/durdraw/durdraw_help.py000066400000000000000000000002461476305210600177620ustar00rootroot00000000000000from importlib.resources import files def get_resource_path(module: str, name: str) -> str: """Load a resource file.""" return files(module).joinpath(name) durdraw-0.29.0/durdraw/durdraw_movie.py000066400000000000000000000445601476305210600201600ustar00rootroot00000000000000from copy import deepcopy from durdraw.durdraw_options import Options import durdraw.log as log import json import pdb import re def init_list_colorMap(width, height): """ Builds a color map consisting of a list of lists """ #return [[list([1,0]) * width] * height] colorMap = [] dummyColor = [8, 0] for h in range(0, height): colorMap.append([]) for w in range(0, width): colorMap[h].append(dummyColor) return colorMap def convert_dict_colorMap(oldMap, width, height): """ Converts the old {[x,y] = (1,0)} color map into new format: [x, y] = [1, 0] """ newMap = init_list_colorMap(width, height) for x in range(0, height): for y in range(0, width): newMap[x][y] = list(oldMap[(x, y)]) # wtf was I thinking #print(f"New color map: {newMap}") # DEBUG return newMap def convert_movie_16_to_256_color_palette(mov): """ Takes movie with old 16 color palette, converts it to look correct with 256 color palette. Returns converted movie? """ fixer = { # conversion table for the palettes # old and bad: # dim: 1 = white, 2 = cyan, 3 = purple, 4 = blue, 5 = brown, 6 = green, 7 = red, 8 = black # bright: 9 = white, 10 = cyan, 11 = purple, 12 = blue, 13 = brown, 14 = green, 15 = red, 16 = bright grey # new and good: # 9: 15, # bright white 10: 14, # bright cyan 11: 13, # bright purple 12: 12, # bright blue, 13: 11, # bright yellow 14: 10, # bright green 15: 9, # bright red 16: 242 # bright gray } for frame in mov.frames: # apply the fixer{} mapping to the frame's color map # looks something like: frame.colorMap[posY, posX] # returns a tuple, not a list. yeegh pass #for line in frame: class Frame(): """Frame class - single canvas size frame of animation. a traditional drawing. """ def __init__(self, width, height): """ Initialize frame, content[x][y] grid """ # it's a bunch of rows of ' 'characters. self.content = [] self.colorMap = {} self.newColorMap = init_list_colorMap(width, height) # [[1,0], [3, 1], ...] self.sizeX = width self.width = width self.sizeY = height self.height = height self.delay = 0 # delay == # of sec to wait at this frame. # Generate character arrays for frame contents, fill it # with ' ' (space) characters for x in range(0, height): self.content.append([]) for y in range(0, width): self.content[x].append(' ') self.initOldColorMap() #self.initColorMap() #self.newColorMap = convert_dict_colorMap(self.colorMap, width, height) self.setDelayValue(0) self.log = log.getLogger('frame') self.log.info('frame initialized', {'width': width, 'height': height}) def flip_horizontal(self): #pdb.set_trace() self.content = self.content[::-1] self.newColorMap.reverse() #for x in range(0, self.height): # #for y in range(0, self.width): # # reverse slicing trick # self.content[x] = self.content[x][::-1] # #self.content[x][0] = self.content[x][0][::-1] # self.newColorMap[x].reverse() def flip_vertical(self): for x in range(0, self.height): #for y in range(0, self.width): # reverse slicing trick self.content[x] = self.content[x][::-1] #self.content[x][0] = self.content[x][0][::-1] self.newColorMap[x].reverse() #def flip_horizontal_segment(self, startPoint, height, width, frange=None): # """ Finish writing this, use it for the alt-k select """ # for x in range(0, self.height): # self.content[x].reverse() # self.newColorMap[x].reverse() def setWidth(self, width): self.sizeX = width self.width = width return true def setHeight(self, height): self.sizeY = height self.height = height return true def setDelayValue(self, delayValue): self.delay = delayValue def initOldColorMap(self): """ Builds a dictionary mapping X/Y to a FG/BG color pair """ self.colorMap = {} for x in range(0, self.sizeY): for y in range(0, self.sizeX): self.colorMap.update( {(x,y):(1,0)} ) # tuple keypair (xy), tuple value (fg and bg) def initColorMap(self, fg=7, bg=0): """ Builds a list of lists """ return [[[fg,0] * self.sizeY] * self.sizeX] class Movie(): """ Contains an array of Frames, options to add, remove, copy them """ def __init__(self, opts): self.frameCount = 0 # total number of frames self.currentFrameNumber = 0 self.sizeX = opts.sizeX # Number of columns self.sizeY = opts.sizeY # Number of lines self.opts = opts self.frames = [] self.layers = {} # Key can be a layer #, or something special. eg: "masks self.addEmptyFrame() self.currentFrameNumber = self.frameCount self.currentFrame = self.frames[self.currentFrameNumber - 1] self.log = log.getLogger('movie') self.log.info('movie initialized', {'sizeX': self.sizeX, 'sizeY': self.sizeY}) def addFrame(self, frame): """ takes a Frame object, adds it into the movie """ self.frames.append(frame) self.frameCount += 1 return True def addEmptyFrame(self): newFrame = Frame(self.sizeX, self.sizeY) self.frames.append(newFrame) self.frameCount += 1 return True def insertCloneFrame(self): """ clone current frame after current frame """ newFrame = Frame(self.sizeX, self.sizeY) self.frames.insert(self.currentFrameNumber, newFrame) newFrame.content = deepcopy(self.currentFrame.content) newFrame.colorMap = deepcopy(self.currentFrame.colorMap) newFrame.newColorMap = deepcopy(self.currentFrame.newColorMap) self.frameCount += 1 return True def deleteCurrentFrame(self): if (self.frameCount == 1): del self.frames[self.currentFrameNumber - 1] # deleted the last frame, so make a blank one newFrame = Frame(self.sizeX, self.sizeY) self.frames.append(newFrame) self.currentFrame = self.frames[self.currentFrameNumber - 1] else: del self.frames[self.currentFrameNumber - 1] self.frameCount -= 1 if (self.currentFrameNumber != 1): self.currentFrameNumber -= 1 self.currentFrame = self.frames[self.currentFrameNumber - 1] return True def moveFramePosition(self, startPosition, newPosition): """ move the frame at startPosition to newPosition """ # use push and pop to remove and insert it fromIndex = startPosition - 1 toIndex = newPosition - 1 self.frames.insert(toIndex, self.frames.pop(fromIndex)) def gotoFrame(self, frameNumber): if frameNumber > 0 and frameNumber < self.frameCount + 1: self.currentFrameNumber = frameNumber self.currentFrame = self.frames[self.currentFrameNumber - 1] return True # succeeded else: return False # failed - invalid input def nextFrame(self): if (self.currentFrameNumber == self.frameCount): # if at last frame.. self.currentFrameNumber = 1 # cycle back to the beginning self.currentFrame = self.frames[self.currentFrameNumber - 1] # -1 bcuz frame 1 = self.frames[0] else: self.currentFrameNumber += 1 self.currentFrame = self.frames[self.currentFrameNumber - 1] def prevFrame(self): if (self.currentFrameNumber == 1): self.currentFrameNumber = self.frameCount self.currentFrame = self.frames[self.currentFrameNumber - 1] else: self.currentFrameNumber -= 1 self.currentFrame = self.frames[self.currentFrameNumber - 1] def hasMultipleFrames(self): if len(self.frames) > 1: return True else: return False def growCanvasWidth(self, growth): self.sizeX += growth self.opts.sizeX += growth #self.width += growth def shrinkCanvasWidth(self, shrinkage): self.sizeY = self.sizeY - shrinkage self.opts.sizeY = self.opts.sizeY - shrinkage #self.width = self.width - shrinkage def search_and_replace_color_pair(self, old_color, new_color, frange=None): if frange != None: # apply to all frames in range for frameNum in range(frange[0] - 1, frange[1]): #for frame in self.frames: frame = self.frames[frameNum] line_num = 0 col_num = 0 for line in frame.newColorMap: for pair in line: if pair == old_color: try: frame.newColorMap[line_num][col_num] = new_color except: pdb.set_trace() #found = True col_num += 1 line_num += 1 col_num = 0 else: # only apply to current frame frame = self.currentFrame line_num = 0 col_num = 0 for line in frame.newColorMap: for pair in line: if pair == old_color: try: frame.newColorMap[line_num][col_num] = new_color except: pdb.set_trace() #found = True col_num += 1 line_num += 1 col_num = 0 def search_and_replace_color(self, old_color :int, new_color :int): found = False for frame in self.frames: line_num = 0 for line in frame.newColorMap: for pair in line: if pair[0] == old_color: frame.newColorMap[line_num][0] = new_color found = True if pair[1] == old_color: frame.newColorMap[line_num][1] = new_color found = True line_num += 1 def search_and_replace(self, caller, search_str: str, replace_str: str): #search_list = list(search) found = False frame_num = 0 line_num = 0 for frame in self.frames: line_num = 0 for line in frame.content: line_str = ''.join(line) if search_str in line_str: # do regexp way, to keep justification match = re.search(f"{search_str}\\s*", line_str) if match: # Calculate the exact width to replace width = match.end() - match.start() # Trim or pad replace_str to fit in the calculated width replace_with = replace_str[:width].ljust(width) # Substitute in the line line_str = line_str[:match.start()] + replace_with + line_str[match.end():] else: # if that fails, do old way if len(search_str) < len(replace_str): #line_str = line_str.replace(search_str.ljust(len(replace_str)), replace_str) line_str = line_str.replace(search_str, replace_str) else: line_str = line_str.replace(search_str, replace_str.ljust(len(search_str))) # inject modified line back into frame line = list(line_str) frame.content[line_num] = line found = True line_num += 1 frame_num += 1 return found def search_for_string(self, search_str: str, caller=None): #search_list = list(search) found = False frame_num = 0 line_num = 0 for frame in self.frames: line_num = 0 for line in frame.content: line_str = ''.join(line) if search_str in line_str: column_num = line_str.index(search_str) + 1 frame_num += 1 found = True return {"line": line_num, "col": column_num, "frame": frame_num} line_num += 1 frame_num += 1 return found # should be false if execution reaches this point def change_palette_16_to_256(self): # Convert from blue to bright white by reducing their value by 1 for frame in self.frames: line_num = 0 col_num = 0 for line in frame.newColorMap: for pair in line: if pair[0] == 1: # black pair[0] = 16 elif pair[0] == 16: # bright white pair[0] = 15 elif pair[0] == 15: # bright yellow pair[0] = 14 elif pair[0] == 14: # bright purple pair[0] = 13 elif pair[0] == 13: # bright red pair[0] = 12 elif pair[0] == 12: # bright cyan pair[0] = 11 elif pair[0] == 11: # bright green pair[0] = 10 elif pair[0] == 10: # bright blue pair[0] = 9 elif pair[0] == 9: # bright black pair[0] = 8 elif pair[0] == 8: # grey pair[0] = 7 elif pair[0] == 7: # brown pair[0] = 6 elif pair[0] == 6: # purple pair[0] = 5 elif pair[0] == 5: # red pair[0] = 4 elif pair[0] == 4: # cyan pair[0] = 3 elif pair[0] == 3: # green pair[0] = 2 elif pair[0] == 2: # blue pair[0] = 1 col_num += 1 def change_palette_256_to_16(self): # Convert from blue to bright white by reducing their value by 1 for frame in self.frames: line_num = 0 col_num = 0 for line in frame.newColorMap: for pair in line: if pair[0] == 16: # black pair[0] = 1 elif pair[0] == 15: # bright white pair[0] = 16 elif pair[0] == 14: # bright yellow pair[0] = 15 elif pair[0] == 13: # bright purple pair[0] = 14 elif pair[0] == 12: # bright red pair[0] = 13 elif pair[0] == 11: # bright cyan pair[0] = 12 elif pair[0] == 10: # bright green pair[0] = 11 elif pair[0] == 9: # bright blue pair[0] = 10 elif pair[0] == 8: # bright black pair[0] = 9 elif pair[0] == 7: # grey pair[0] = 8 elif pair[0] == 6: # brown pair[0] = 7 elif pair[0] == 5: # purple pair[0] = 6 elif pair[0] == 4: # red pair[0] = 5 elif pair[0] == 3: # cyan pair[0] = 4 elif pair[0] == 2: # green pair[0] = 3 elif pair[0] == 1: # blue pair[0] = 2 col_num += 1 def contains_high_colors(self): """ Returns True if any color above 16 is used, False otherwise """ for frame in self.frames: for line in frame.newColorMap: for pair in line: if pair[0] > 16: return True return False def contains_background_colors(self): """ Return true if any background color is set other than black or default """ for frame in self.frames: for line in frame.newColorMap: for pair in line: if pair[1] > 0: return True return False def strip_backgrounds(self): """ Change all background colors to 0, or default background """ for frame in self.frames: for line in frame.newColorMap: for pair in line: pair[1] = 0 return True def strip_unprintable_characters(self): """ Remove all non-printable characters from canvas """ #frame_num = 0 for frame in self.frames: line_num = 0 col_num = 0 for line in frame.content: for char in line: if char.isprintable(): pass else: # Not a printable character, so replace it with a ' ' #line_str = line_str.replace(search_str, replace_str.ljust(len(search_str))) #line = list(line_str) frame.content[line_num][col_num] = ' ' col_num += 1 col_num = 0 line_num += 1 return True def shift_right(self): """ Shift all frames to the right, wrapping the last frame back to the front """ # a.insert(0,a.pop()) self.frames.insert(0, self.frames.pop()) return True def shift_left(self): """ Shift all frames to the left, wrapping the first frame around to the back end """ # fnord.append(fnord.pop(0)) self.frames.append(self.frames.pop(0)) return True def toJSON(self): return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) durdraw-0.29.0/durdraw/durdraw_options.py000066400000000000000000000012761476305210600205310ustar00rootroot00000000000000import json class Options(): # config, prefs, preferences, etc. Per movie. Separate from AppState options. """ Member variables are canvas X/Y size, Framerate, Video resolution, etc """ def __init__(self, width=80, height=23): # default options self.framerate = 8.0 self.sizeX = width self.sizeY = height self.saveFileFormat = 7 # save file format version number # version 4 is pickle, version 5 is JSON, version 6 saves color and # character encoding formats, Version 7 uses a new palette order for 16 color def toJSON(self): return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) durdraw-0.29.0/durdraw/durdraw_sauce.py000066400000000000000000000062161476305210600201350ustar00rootroot00000000000000#!/usr/bin/python3 import struct class SauceParser(): def __init__(self): # Open the file, look for a sauce record, # extract sauce data into a structure #self.fileName = filename # sauce data self.title = None self.author = None self.group = None self.date = None self.year = None self.month = None self.day = None self.fileType = None self.tinfo1 = None self.tinfo2 = None self.width = 80 self.height = 25 # sauce data offsets self.title_offset = 7 self.author_offset = 42 self.group_offset = 62 self.date_offset = 82 self.fileType_offset = 95 self.tinfo1_offset = 96 self.tinfo2_offset = 98 # Number of lines in the extra SAUCE comment block. 1 byte. 0 indicates no comment block is present. self.comnt_lines = 105 self.comnt_first_line = 133 # comment lines are 64 bytes long # other stuff self.sauce_blob = None self.sauce_found = False def parse_file(self, filename): try: with open(filename, 'rb') as file: file_blob = file.read() except Exception as E: return False self.parse_blob(file_blob) #return self.parse_blob(file_blob) # Nothing to return.. def parse_blob(self, file_blob): # Blob is, like. Bytes or something sauce_blob = file_blob[-128:] self.sauce_blob = sauce_blob #print(sauce_blob) if sauce_blob[:5] == b"SAUCE": self.sauce_found = True self.title = struct.unpack_from('35s', sauce_blob, offset=self.title_offset)[0] self.author = struct.unpack_from('20s', sauce_blob, offset=self.author_offset)[0] self.group = struct.unpack_from('20s', sauce_blob, offset=self.group_offset)[0] self.date = struct.unpack_from('8s', sauce_blob, offset=self.date_offset)[0].decode() self.year = self.date[:4] self.month = self.date[4:][:2] self.day = self.date[4:][2:] # turn bytes into nicer strings try: self.title = self.title.decode().rstrip(' ').strip('\x00') except UnicodeDecodeError: self.title = self.title.decode('cp437').rstrip(' ').strip('\x00') try: self.author= self.author.decode().rstrip(' ').strip('\x00') except UnicodeDecodeError: self.author= self.author.decode('cp437').rstrip(' ').strip('\x00') try: self.group= self.group.decode().rstrip(' ').strip('\x00') except UnicodeDecodeError: self.group= self.group.decode('cp437').rstrip(' ').strip('\x00') self.tinfo1 = struct.unpack_from('H', sauce_blob, offset=self.tinfo1_offset)[0] if self.tinfo1 > 1: self.width = self.tinfo1 self.tinfo2 = struct.unpack_from('H', sauce_blob, offset=self.tinfo2_offset)[0] if self.tinfo2 > 1: self.height = self.tinfo2 else: self.sauce_found = False durdraw-0.29.0/durdraw/durdraw_sixteencolors.py000066400000000000000000000133651476305210600217410ustar00rootroot00000000000000#!/usr/bin/python3 import pdb import json import urllib.request # exmaple data: # [{'year': 1990, 'packs': 5}, {'year': 1991, 'packs': 7}] # packs for 1992: ['1992', 'aaa-vol2', 'acdu0692', 'acdu0792', 'acdu0892', 'acdu0992', 'acdu1092', 'acdu1192', 'acdu1292', 'ace-r2', 'acid_a-d', 'acid_e-k', 'acid_l-r', 'acid_s-z', 'acidvga', 'air', 'allmack1', 'ansi', 'ansitoon', 'anssmp50', 'dead', 'grim-03', 'hipe', 'hype', 'icepk-08', 'icepk-09', 'icepk-10', 'icepk-11', 'icepk-12', 'image', 'jism', 'lgc-1292', 'ltd', 'mace', 'mirage', 'mirage01', 'mirage02', 'mirage03', 'mirage04', 'nc-17', 'paranoi2', 'paranoia', 'qck-pkt1', 'rpm', 'sda', 'z-md2'] # files for 1992/grim-03: {'name': 'grim-03', 'year': 1992, 'filename': 'grim-03.zip', 'month': '01', 'uri': '/pack/grim-03', 'groups': [], 'pack_file_location': '/archive/1992/grim-03.zip', 'files': [{'filename': 'CA-TUGO.ANS', 'file_location': '/ pack/grim-03/raw/CA-TUGO.ANS', 'uri': '/pack/grim-03/raw/CA-TUGO.ANS', 'fullsize': '/pack/grim-03/raw/CA-TUGO.ANS.png', 'th umbnail': '/pack/grim-03/tn/CA-TUGO.ANS.png', 'packs': {'filename': 'grim-03.zip', 'name': 'grim-03', 'uri': '/pack/grim-03 '}}, {'filename': 'CD-HOG1.EXE', 'file_location': '/pack/grim-03/raw/CD-HOG1.EXE' class SixteenColorsAPI: def __init__(self): self.api_prefix="https://api.sixteencolors.net/v0" self.api_year_prefix="/year" self.api_suffix="?rows=0" self.year_listing_data = [] # cache for /year self.cache_years() self.url_cache = {} # {"pack": {"filename": url, "filename2": url}} self.website_prefix="https://16colo.rs" def list_years(self): years_list = [] for item in self.year_listing_data: years_list.append(str(item['year'])) return years_list def cache_years(self): url = self.api_prefix + self.api_year_prefix + self.api_suffix try: with urllib.request.urlopen(url) as response: data = response.read() self.year_listing_data = json.loads(data) except urllib.request.HTTPError: return False return True def list_packs_for_year(self, year): url = self.api_prefix + self.api_year_prefix + '/' + str(year) + self.api_suffix #print(f"url: {url}") try: with urllib.request.urlopen(url) as response: data = response.read() packs_json_data = json.loads(data) # list of dicts except urllib.request.HTTPError: return False file_list = [] #print(f"{year} Packs JSON data: {packs_json_data}") for item in packs_json_data: #file_list.append(item['filename']) file_list.append(str(item['name'])) return file_list def list_files_for_pack(self, pack): url = self.api_prefix + '/pack/' + str(pack) + self.api_suffix #print(f"list files for pack url: {url}") try: with urllib.request.urlopen(url) as response: data = response.read() pack_files_json_data = json.loads(data) # list of dicts except urllib.request.HTTPError: return False file_list = [] try: for item in pack_files_json_data['files']: file_list.append(item['filename']) except TypeError: # got list instead of a dict? pdb.set_trace() return file_list def get_url_for_file(self, pack, filename): if pack in self.url_cache: if filename in self.url_cache[pack]: # cached, so return from cache return self.url_cache[pack][filename] # not cached url = self.api_prefix + '/pack/' + str(pack) + self.api_suffix #print(f"url: {url}") try: with urllib.request.urlopen(url) as response: data = response.read() pack_files_json_data = json.loads(data) # list of dicts except urllib.request.HTTPError: return False location = None for item in pack_files_json_data['files']: if item['filename'] == filename: #location = item['uri'] location = item['file_location'] #print(f"Get URL for File JSON data: {pack_files_json_data}") if location == None: url = '' else: url = self.website_prefix + urllib.parse.quote(location) if pack not in self.url_cache: self.url_cache[pack] = {} if filename not in self.url_cache[pack]: self.url_cache[pack][filename] = url return url def get_raw_file(self, pack, filename): url = self.get_url_for_file(pack, filename) #print(f"url: {url}") try: with urllib.request.urlopen(url) as response: data = response.read() except urllib.request.HTTPError: return False #return data.decode(encoding='cp437') return data #def diz_cache_for_year(year): # """ returns a dict, like {"packname": dizdata} """ if __name__=="__main__": sixteen_web = SixteenColorsAPI() base_listing = sixteen_web.list_years() print(f"base listing: {base_listing}") year = 1992 packs = sixteen_web.list_packs_for_year(year) print(f"packs for {year}: {packs}") pack = "grim-03" files = sixteen_web.list_files_for_pack(pack) print(f"files for {year}/{pack}: {files}") file = 'NE-EXEC.ANS' file_url = sixteen_web.get_url_for_file(pack, file) print(f"url for {year}/{pack}/{file}: {file_url}") #diz_cache = {} #diz_cache = diz_cache_for_year(year) # dict #print(str(diz_cache)) #ansi_data = sixteen_web.get_raw_file(pack, file) #print("Raw ansi data:") #print(ansi_data.decode(encoding='cp437')) durdraw-0.29.0/durdraw/durdraw_ui_curses.py000066400000000000000000013434061476305210600210440ustar00rootroot00000000000000# This file is where the main loop/controller lives. # It ought to have all curses code separated out into another file. import curses import curses.panel import fnmatch import glob import gzip import locale import os import os.path import pathlib import pdb import pickle import shutil import sys import subprocess import tempfile import textwrap import threading import time import urllib from concurrent.futures import ThreadPoolExecutor #from durdraw.durdraw_appstate import AppState from durdraw.durdraw_options import Options from durdraw.durdraw_color_curses import AnsiArtStuff from durdraw.durdraw_movie import Movie from durdraw.durdraw_undo import UndoManager from durdraw.durdraw_sixteencolors import SixteenColorsAPI import durdraw.durdraw_file as durfile from durdraw.durdraw_ui_widgets import StatusBar import durdraw.durdraw_gui_manager as durgui import durdraw.durdraw_movie as durmovie import durdraw.neofetcher as neofetcher import durdraw.durdraw_color_curses as dur_ansilib import durdraw.durdraw_ansiparse as dur_ansiparse import durdraw.durdraw_sauce as dursauce import durdraw.durdraw_charsets as durchar import durdraw.plugins.reverse_movie as reverse_plugin # transform_movie import durdraw.plugins.repeat_movie as repeat_plugin # transform_movie import durdraw.plugins.bounce_movie as bounce_plugin # transform_movie import durdraw.log as log class UserInterface(): # Separate view (curses) from this controller """ Draws user interface, has main UI loop. """ #def __init__(self, stdscr, app): def __init__(self, app): self.opts = Options(width=app.width, height=app.height) self.appState = app # will be filled in by main() .. run-time app state stuff self.log = self.appState.getLogger('ui_curses') self.log.info('UserInterface created') self.initCursorMode() self.clipBoard = None # frame object self.charMapNumber = 0 self.chMap = {} self.chMapString = "" self.chMap_offset = 0 self.statusBar = None self.appState.unicodeBlockList = durchar.get_unicode_blocks_list() self.initCharSet() # sometimes later options can store a char set to init - utf-8, cp437, etc. os.environ.setdefault('ESCDELAY', '10') self.appState.sixteenc_year = None self.appState.sixteenc_pack = None self.sixteenc_levels = ["root", "year", "pack"] # hierarchy to replace directory hierarchy self.sixteenc_level = 0 # 0 = "root" self.sixteenc_years = None # [] A list of all the years self.selected_item_number = 0 self.diz_caching_thread = None # initialize screen and draw the 'canvas' locale.setlocale(locale.LC_ALL, '') # set your locale self.realstdscr = curses.initscr() realmaxY,realmaxX = self.realstdscr.getmaxyx() # test size self.appState.realmaxY = realmaxY self.appState.realmaxX = realmaxX self.statusBarLineNum = realmaxY - 2 self.stdscr = curses.newwin(realmaxY, realmaxX, 0, 0) # Curses window self.window = self.stdscr if self.appState.charEncoding == 'cp437': self.stdscr.encoding = 'cp437' self.panel = curses.panel.new_panel(self.stdscr) # Panel for drawing, to sit below menus self.panel.bottom() self.panel.show() self.pressingButton = False self.playingHelpScreen = False self.pushingToClip = False # true while we are holding down mouse button to draw or erase self.metaKey = 0 self.commandMode = False self.line_1_offset = 15 self.transportOffset = 0 #self.stdscr.box() self.gui = durgui.Gui(guiType="curses", window=self.stdscr) curses.start_color() # Yeayuhhh self.ansi = AnsiArtStuff(self.appState) # obj for misc ansi-related stuff # Disable mouse scrolling for Python versions below 3.10, as they don't have # curses.BUTTON5_* self.colorbg = 0 # default bg black self.colorfg = 7 # default fg white. These are overriden by the following self.init_x_colors_misc(): if sys.version_info.major == 3: if sys.version_info.minor < 10: self.appState.hasMouseScroll = False if self.appState.colorMode == "256": if self.ansi.initColorPairs_256color(): self.init_256_colors_misc() self.appState.totalFgColors = 255 self.appState.totalBgColors = 1 else: self.appState.colorMode = "16" self.appState.maxColors = 16 self.appState.totalFgColors = 16 self.appState.totalBgColors = 8 if self.appState.colorMode == "16": self.init_16_colors_misc() if not app.quickStart and app.showStartupScreen: print(f"Color mode: {self.appState.colorMode}") time.sleep(2) try: self.colorpair = self.ansi.colorPairMap[(self.colorfg, self.colorbg)] # set ncurss color pair except: self.appState.colorMode = "16" self.appState.maxColors = 16 self.ansi.initColorPairs_cga() self.init_16_colors_misc() self.appState.loadThemeFromConfig("Theme-16") if self.appState.blackbg: self.enableTransBackground() try: self.colorpair = self.ansi.colorPairMap[(self.colorfg, self.colorbg)] # set ncurss color pair except KeyError: pdb.set_trace() self.mov = Movie(self.opts) # initialize a new movie to work with self.undo = UndoManager(self, appState = self.appState) # initialize undo/redo system self.undo.setHistorySize(self.appState.undoHistorySize) self.xy = [0, 1] # cursor position x/y - was "curs" self.playing = False #curses.raw() curses.cbreak() curses.noecho() curses.nonl() self.stdscr.keypad(1) self.realmaxY,self.realmaxX = self.realstdscr.getmaxyx() self.testWindowSize() self.statusBar = StatusBar(self, x=self.statusBarLineNum, y=0, appState=self.appState) if self.appState.colorMode == "16": self.statusBar.colorPickerButton.hide() self.statusBar.colorPicker = self.statusBar.colorPicker_16 if self.appState.playOnlyMode: self.statusBar.hide() else: for button in self.statusBar.buttons: self.gui.add_button(button) #self.setWindowTitle("Durdraw") # set a default drawing character self.appState.drawChar = chr(self.chMap['f4']) self.statusBar.drawCharPickerButton.label = self.appState.drawChar self.statusBarLineNum = self.realmaxY - 2 durchar.scan_charmap_folders(self.appState) #self.loadCharsetFile("~/src/durdraw/coolset.ini") self.setCharacterSet("Durdraw Default") def init_256_colors_misc(self): self.appState.theme = self.appState.theme_256 self.appState.loadThemeFromConfig('Theme-256') if self.appState.customThemeFile: self.appState.loadThemeFile(self.appState.customThemeFile, 'Theme-256') if self.appState.playOnlyMode == False: self.appState.loadHelpFileThread(self.appState.durhelp256_fullpath) #self.appState.loadHelpFile(self.appState.durhelp256_page2_fullpath, page=2) self.colorbg = 0 # default bg black self.colorfg = 7 # default fg white self.appState.sideBar_minimum_width = 37 self.appState.bottomBar_minimum_height = 10 if self.statusBar != None: self.statusBar.colorPickerButton.enabled = True def init_16_colors_misc(self): self.appState.colorMode = "16" self.appState.theme = self.appState.theme_16 self.appState.loadThemeFromConfig('Theme-16') if self.appState.playOnlyMode == False: self.appState.loadHelpFileThread(self.appState.durhelp16_fullpath) #self.appState.loadHelpFile(self.appState.durhelp16_page2_fullpath, page=2) if self.appState.customThemeFile: self.appState.loadThemeFile(self.appState.customThemeFile, 'Theme-256') self.colorfg = 8 # default fg white self.colorbg = 8 # default bg black self.appState.defaultFgColor = 8 if self.appState.iceColors: self.colorfg = 7 # default fg white self.colorbg = 0 # default bg black self.appState.defaultFgColor = 7 # phase 2 self.ansi.initColorPairs_cga() #self.ansi.initColorPairs_ice_colors() self.appState.sideBar_minimum_width = 12 self.appState.bottomBar_minimum_height = 5 if self.statusBar != None: self.statusBar.colorPickerButton.enabled = False def toggleMouse(self): """ enable and disable mouse """ self.appState.hasMouse = not self.appState.hasMouse # flip true/false if self.appState.hasMouse: self.initMouse() else: self.disableMouse() def disableMouse(self): print('\033[?1003l') # disable mouse reporting curses.mousemask(0) def initMouse(self): print('\033[?1003h') # enable mouse tracking with the XTERM API curses.mousemask(1) # click response without drag support curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) #print('\033[?1003h') # enable mouse tracking with the XTERM API # https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking def initCursorMode(self): # Set cursor shape to block - this is now a command line option. #sys.stdout.write(f"\x1b[1 q") # 1 block blink # 2 block no blink # 3 underscore blink # 4 underscore no blink # 5 pipe blink # 6 pipe no blink if self.appState.screenCursorMode == "default": return elif self.appState.screenCursorMode == "block": sys.stdout.write("\x1b[1 q") elif self.appState.screenCursorMode == "underscore": sys.stdout.write(f"\x1b[3 q") elif self.appState.screenCursorMode == "pipe": sys.stdout.write(f"\x1b[5 q") sys.stdout.write("\n") def enableMouseReporting(self): # Use xterm API to report location of mouse cursor print('\033[?1003h') # enable mouse tracking with the XTERM API self.hardRefresh() #curses.mousemask(1) #curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) def disableMouseReporting(self): print('\033[?1003l') # disable mouse reporting curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) self.hardRefresh() def enableTransBackground(self): curses.use_default_colors() self.reloadLowColorPairs() def reloadLowColorPairs(self): if self.appState.colorMode == '16': self.ansi.initColorPairs_cga(trans=True) else: #curses.init_pair(1, 16, -1) # black curses.init_pair(1, 4, -1) # blue curses.init_pair(2, 2, -1) # green curses.init_pair(3, 6, -1) # cyan curses.init_pair(4, 1, -1) # red curses.init_pair(5, 5, -1) # magenta curses.init_pair(6, 3, -1) # yellow curses.init_pair(7, 7, -1) # white curses.init_pair(8, 8, -1) # dark grey/bright black curses.init_pair(9, 12, -1) # bright blue curses.init_pair(10, 10, -1) # bright green curses.init_pair(11, 14, -1) # bright cyan curses.init_pair(12, 9, -1) # bright red curses.init_pair(13, 13, -1) # bright magenta curses.init_pair(14, 11, -1) # bright yellow curses.init_pair(15, 15, -1) # bright white def resetColorsToDefault(self): #curses.use_default_colors() self.enableTransBackground() def enableTrueVGAColors(self): curses.use_default_colors() # red intense = 1000 # hex FF low = 0 med = self.rgb_color_to_ncurses_color(85) # hex 55 high = self.rgb_color_to_ncurses_color(170) # hex AA # ncurses init_color takes: # Color #, R, G, B, as 0-1000 instead of 0-255 values. curses.init_color(1, high, low, low) # red curses.init_color(2, low, high, low) # green curses.init_color(3, high, med, low) # yellow/brown #AA5500 curses.init_color(4, low, low, high) # blue curses.init_color(5, high, low, high) # magenta curses.init_color(6, low, high, high) # cyan curses.init_color(7, high, high, high) # white # bright VGA colors curses.init_color(8, med, med, med) # bright black curses.init_color(9, intense, med, med) # red curses.init_color(10, med, intense, med) # green curses.init_color(11, intense, intense, low) # yelmed/brown #AA5500 curses.init_color(12, med, med, intense) # blue curses.init_color(13, intense, med, intense) # magenta curses.init_color(14, med, intense, intense) # cyan curses.init_color(15, intense, intense, intense) # white self.reloadLowColorPairs() def enableTrueSpeccyColors(self): curses.use_default_colors() # red intense = 1000 # hex FF low = 0 med = self.rgb_color_to_ncurses_color(85) # hex 55 high = self.rgb_color_to_ncurses_color(216) # hex d8 # ncurses init_color takes: # Color #, R, G, B, as 0-1000 instead of 0-255 values. curses.init_color(1, high, low, low) # red curses.init_color(2, low, high, low) # green curses.init_color(3, high, high, low) # yellow/brown #AA5500 curses.init_color(4, low, low, high) # blue curses.init_color(5, high, low, high) # magenta curses.init_color(6, low, high, high) # cyan curses.init_color(7, high, high, high) # white # bright VGA colors curses.init_color(8, low, low, low) # bright black curses.init_color(9, intense, low, low) # red curses.init_color(10, low, intense, low) # green curses.init_color(11, intense, intense, low) # yellow/brown #AA5500 curses.init_color(12, low, low, intense) # blue curses.init_color(13, intense, low, intense) # magenta curses.init_color(14, low, intense, intense) # cyan curses.init_color(15, intense, intense, intense) # white self.reloadLowColorPairs() def enableTrueC64Colors(self): # Colors from https://www.c64-wiki.com/wiki/Color curses.use_default_colors() # red intense = 1000 # hex FF low = 0 med = self.rgb_color_to_ncurses_color(85) # hex 55 high = self.rgb_color_to_ncurses_color(216) # hex d8 fc = self.rgb_color_to_ncurses_color # fix color # ncurses init_color takes: # Color #, R, G, B, as 0-1000 instead of 0-255 values. curses.init_color(1, fc(136), low, low) # red #880000 curses.init_color(2, low, fc(204), fc(85)) # green #00CC55 curses.init_color(3, fc(102), fc(68), 0) # brown #664400 curses.init_color(4, low, low, fc(170)) # blue #0000AA curses.init_color(5, fc(204), fc(68), fc(204)) # violet/purple #CC44CC curses.init_color(6, fc(170), intense, fc(238)) # cyan #AAFFEE curses.init_color(7, fc(119), fc(119), fc(119)) # grey 2 # bright VGA colors curses.init_color(8, fc(51), fc(51), fc(51)) # dark grey/grey 1 curses.init_color(9, intense, fc(119), fc(119)) # light red #FF7777 curses.init_color(10, fc(170), intense, fc(102)) # light green # AAFF66 curses.init_color(11, fc(238), fc(238), fc(119)) # yellow #EEEE77 curses.init_color(12, low, fc(136), intense) # light blue #0088FF curses.init_color(13, fc(187), fc(187), fc(187)) # LIGHT GREY/grey 3! #BBBBBB curses.init_color(14, fc(221), fc(136), fc(85)) # Orange #DD8855 curses.init_color(15, intense, intense, intense) # white #FFFFFF self.reloadLowColorPairs() def map_rescale_value(self, value, from_min, from_max, to_min, to_max): """ Converts number from one scale/range to another """ # Calculate the percentage of value between from_min and from_max percentage = (value - from_min) / (from_max - from_min) # Map the percentage to the range between to_min and to_max mapped_value = percentage * (to_max - to_min) + to_min return mapped_value def rgb_color_to_ncurses_color(self, value): """ Takes range 0-255, converts to 0-1000 range """ ncurses_color_value = int(self.map_rescale_value(value, 0, 255, 0, 1000)) return ncurses_color_value def setWindowTitle(self, title): if title == "": title = f"durdraw" else: title = f"durdraw - {title}" sys.stdout.write(f"\x1b]2;{title}\x07") sys.stdout.write(f'\33]0;{title}\a') sys.stdout.flush() def getPlaybackRange(self): """ ask for playback range presses cmd-r """ self.clearStatusLine() self.move(self.mov.sizeY, 0) self.stdscr.nodelay(0) # wait for input when calling getch goodResponse = False while goodResponse == False: self.promptPrint("Start frame for playback [currently %i of %i, 0 for All]: " % \ (self.appState.playbackRange[0], self.mov.frameCount)) curses.echo() lowRange = self.stdscr.getstr() if lowRange.isdigit(): lowRange = int(lowRange) if lowRange >= 0 and lowRange <= self.mov.frameCount: goodResponse = True else: self.notify("Must be a number between %i and %i" % (1, self.mov.frameCount)) goodResponse = False else: self.notify("Playback range not changed.") curses.noecho() if self.playing: self.stdscr.nodelay(1) # don't wait for input when calling getch return False if lowRange == 0: self.setPlaybackRange(1, self.mov.frameCount) curses.noecho() if self.playing: self.stdscr.nodelay(1) # don't wait for input when calling getch return True goodResponse = False self.clearStatusLine() while goodResponse == False: self.promptPrint("End frame for playback [currently %i of %i]: " % \ (self.appState.playbackRange[0], self.mov.frameCount)) curses.echo() highRange = self.stdscr.getstr() if highRange.isdigit(): highRange = int(highRange) if highRange >= lowRange and highRange <= self.mov.frameCount: goodResponse = True else: self.notify("Must be a number between %i and %i" % (lowRange, self.mov.frameCount)) goodResponse = False else: self.notify("Playback range not changed.") curses.noecho() if self.playing: self.stdscr.nodelay(1) # don't wait for input when calling getch return False self.setPlaybackRange(lowRange, highRange) curses.noecho() if self.playing: self.stdscr.nodelay(1) # don't wait for input when calling getch return True def setPlaybackRange(self, start, stop): self.appState.playbackRange = (start, stop) def gotoFrameGetInput(self): self.clearStatusLine() self.move(self.mov.sizeY, 0) self.stdscr.nodelay(0) # wait for input when calling getch goodResponse = False while goodResponse == False: self.promptPrint("Enter frame to go to [currently %i of %i]: " % \ (self.mov.currentFrameNumber, self.mov.frameCount)) curses.echo() newFrameNumber = self.stdscr.getstr() if newFrameNumber.isdigit(): newFrameNumber = int(newFrameNumber) if newFrameNumber >= 0 and newFrameNumber <= self.mov.frameCount: goodResponse = True else: self.notify("Must be a number between %i and %i" % (1, self.mov.frameCount)) goodResponse = False else: self.notify("Current frame number not changed.") curses.noecho() if self.playing: self.stdscr.nodelay(1) # don't wait for input when calling getch return False if goodResponse: self.mov.gotoFrame(newFrameNumber) curses.noecho() def setAppState(self, appState): """ Takes the app state (running config options) and makes any changes needed to the UI/etc to honor that app state """ self.appState = appState # set undo history size self.undo.setHistorySize(self.appState.undoHistorySize) def setFgColor(self, fg): self.colorfg = fg try: self.colorpair = self.ansi.colorPairMap[(self.colorfg, self.colorbg)] except KeyError: try: # If we're trying to use an unreigstered color pair, strip # out the background and try again. self.colorbg = 0 self.colorpair = self.ansi.colorPairMap[(self.colorfg, self.colorbg)] except KeyError: self.notify(f"There was an error setting the color. fg: {self.colorfg}, bg: {self.colorbg}. Please file a bug report explaining how you got to this error.") def setBgColor(self, bg): self.colorbg = bg self.colorpair = self.ansi.colorPairMap[(self.colorfg, self.colorbg)] def switchTo16ColorMode(self): self.switchToColorMode("16") def switchTo256ColorMode(self): if self.appState.maxColors < 256: self.notify("Unable to set 256 colors. Check your terminal configuration?") else: self.statusBar.drawCharPickerButton.show() self.switchToColorMode("256") def switchToColorMode(self, newMode: str): """ newMode, eg: '16' or '256' """ self.appState.inferno = None if newMode == "16": #self.statusBar.colorPicker.hide() self.appState.colorMode = "16" self.ansi.initColorPairs_cga() self.init_16_colors_misc() self.mov.change_palette_256_to_16() self.appState.loadThemeFromConfig("Theme-16") self.statusBar.colorPickerButton.hide() #self.statusBar.charSetButton.hide() #if self.statusBar.colorPickerEnabled: # self.statusBar.enableColorPicker() if self.appState.blackbg: self.enableTransBackground() # switch to 16 color sidebar picker self.statusBar.colorPicker.hide() self.statusBar.colorPicker = self.statusBar.colorPicker_16 self.appState.totalFgColors = 16 self.appState.totalBgColors = 8 #self.statusBar.colorPicker.show() if newMode == "256": self.appState.colorMode = "256" self.ansi.initColorPairs_256color() self.init_256_colors_misc() self.mov.change_palette_16_to_256() self.appState.loadThemeFromConfig("Theme-256") self.statusBar.colorPickerButton.show() self.appState.totalFgColors = 255 self.appState.totalBgColors = 1 if self.appState.blackbg: self.enableTransBackground() #self.statusBar.charSetButton.show() #if not self.statusBar.colorPickerEnabled: # self.statusBar.disableColorPicker() # switch to 256 color sidebar picker self.statusBar.colorPicker.hide() #self.statusBar.colorPicker_bg_16.hide() self.statusBar.colorPicker = self.statusBar.colorPicker_256 #self.statusBar.colorPicker.show() # Show color picker if needed realmaxY,realmaxX = self.realstdscr.getmaxyx() if self.appState.sideBarEnabled and not self.playing: # Sidebar not showing, but enabled. Check and see if the window is wide enough if realmaxX > self.mov.sizeX + self.appState.sideBar_minimum_width: self.appState.sideBarShowing = True #self.notify("Wide. Showing color picker.") #if self.appState.colorMode == "256": self.statusBar.colorPicker.show() #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.show() # Window is too narrow, but tall enough to show more stuff on the bottom. elif realmaxY - self.appState.bottomBar_minimum_height > self.mov.sizeY: #if self.appState.colorMode == "256": self.statusBar.colorPicker.show() #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.show() def nextFgColor(self): """ switch to next fg color, cycle back to beginning at max """ if self.appState.colorMode == "256": if self.colorfg < 255: newColor = self.colorfg + 1 else: newColor = 1 elif self.appState.colorMode == "16": if self.appState.iceColors: hiColor = 15 else: hiColor = 16 if self.colorfg < hiColor: newColor = self.colorfg + 1 else: newColor = 1 self.setFgColor(newColor) #if self.appState.colorMode == "256": self.statusBar.colorPicker.handler.updateFgPicker() def prevFgColor(self): """ switch to prev fg color, cycle around to end if at beginning """ if self.colorfg > 1: newColor = self.colorfg - 1 else: if self.appState.colorMode == "16": if self.appState.iceColors: newColor = 15 else: newColor = 16 elif self.appState.colorMode == "256": newColor = 255 else: newColor = 16 # default to 16 color self.setFgColor(newColor) #if self.appState.colorMode == "256": # self.statusBar.colorPicker.handler.updateFgPicker() self.statusBar.colorPicker.handler.updateFgPicker() def nextBgColor(self): """ switch to the next bg color, cycle around if at beginning """ if self.appState.colorMode == "256": lowColor = 0 #hiColor = 255 hiColor = 0 if self.colorbg < hiColor: newColor = self.colorbg + 1 else: newColor = lowColor self.setBgColor(newColor) elif self.appState.colorMode == "16": if self.appState.iceColors: lowColor = 0 hiColor = 15 else: lowColor = 1 hiColor = 8 if self.colorbg < hiColor: newColor = self.colorbg + 1 if (self.colorfg == 7 and self.colorbg == 7) or (self.colorfg == 15 and self.colorbg == 7): # skip over red on red newColor = self.colorbg + 1 else: newColor = lowColor # no ice colors - only 8 bg colors #elif self.colorbg < 8: # newColor = self.colorbg + 1 # if (self.colorfg == 7 and self.colorbg == 7) or (self.colorfg == 15 and self.colorbg == 7): # skip over red on red # newColor = self.colorbg + 1 #else: # newColor = 1 self.setBgColor(newColor) def prevBgColor(self): """ switch to prev bg color, cycle around to end if at beginning """ if self.appState.colorMode == "256": lowColor = 0 #hiColor = 255 hiColor = 0 if self.colorbg > lowColor: newColor = self.colorbg - 1 else: newColor = hiColor self.setBgColor(newColor) else: if self.appState.iceColors: lowColor = 0 hiColor = 15 else: lowColor = 1 hiColor = 8 if self.colorbg > lowColor: newColor = self.colorbg - 1 #if self.colorfg == 7 and self.colorbg == 7 or self.colorfg == 15 and self.colorbg == 7: # skip over red on red # newColor = self.colorbg - 1 else: newColor = hiColor self.setBgColor(newColor) def cursorOff(self): try: curses.curs_set(0) # turn off cursor except curses.error: pass # .. if terminal supports it. def cursorOn(self): try: curses.curs_set(1) # turn on cursor except curses.error: pass # .. if terminal supports it. def addstr(self, y, x, string, attr=None): # addstr(y, x, str[, attr]) and addstr(str[, attr]) """ Wraps ncurses addstr in a try;except, prevents addstr from crashing cureses if it fails """ if not attr: try: self.stdscr.addstr(y, x, string.encode(self.appState.charEncoding, 'replace')) #if self.appState.charEncoding == 'utf-8': # self.stdscr.addstr(y, x, string.encode('utf-8')) #else: # self.stdscr.addstr(y, x, string) except UnicodeEncodeError: # Replace non-ascii characters with ' ' string = string.encode('ascii', 'replace').decode('ascii').replace('?', ' ') self.stdscr.addstr(y, x, string) except curses.error: self.testWindowSize() else: try: self.stdscr.addstr(y, x, string.encode(self.appState.charEncoding, 'replace'), attr) #if self.appState.charEncoding == 'utf-8': # self.stdscr.addstr(y, x, strng.encode('utf-8'), attr) #else: # self.stdscr.addstr(y, x, string, attr) except UnicodeEncodeError: string = string.encode('ascii', 'replace').decode('ascii').replace('?', ' ') self.stdscr.addstr(y, x, string, attr) except curses.error: self.testWindowSize() def move(self, y, x): realLine = y - self.appState.topLine realCol = x - self.appState.firstCol try: self.stdscr.move(realLine, realCol) except curses.error: self.testWindowSize() def notify(self, message, pause=False, wait_time=2500): self.cursorOff() self.clearStatusLine() self.addstr(self.statusBarLineNum, 0, message, curses.color_pair(self.appState.theme['notificationColor'])) self.stdscr.refresh() if pause: if self.playing: self.stdscr.nodelay(0) # wait for input when calling getch self.stdscr.getch() self.stdscr.nodelay(1) # do not wait for input when calling getch else: self.stdscr.getch() if not pause: curses.napms(wait_time) curses.flushinp() self.clearStatusLine() self.cursorOn() self.stdscr.refresh() def testWindowSize(self): """Test to see if window == too small for program to operate, and go into small window mode if necessary""" realmaxY, realmaxX = self.realstdscr.getmaxyx() # test size if realmaxY != self.realmaxY or realmaxX != self.realmaxX: self.resizeHandler() self.realmaxY, self.realmaxX = realmaxY, realmaxX #while self.realmaxX < 80 and not self.appState.playOnlyMode: while self.realmaxX < self.appState.minWindowWidth and not self.appState.playOnlyMode: self.smallWindowMode() # go into small window loop.stdscr def smallWindowMode(self): """Clear the screen, draw a small message near 0,0 that the window == too small. Keep doing so until the screen == resized larger.""" self.stdscr.clear() self.stdscr.refresh() self.realmaxY,self.realmaxX = self.realstdscr.getmaxyx() #while self.realmaxX < self.mov.sizeX: while self.realmaxX < self.appState.minWindowWidth: try: self.addstr(0, 0, "Terminal is too small for the UI.") self.addstr(1, 0, f"Please enlarge to {self.appState.minWindowWidth} columns or larger, or press 'q' to quit") except: # if window is too small for the message ^ pass c = self.stdscr.getch() if c == 113: # 113 == 'q' self.verySafeQuit() self.realmaxY,self.realmaxX = self.realstdscr.getmaxyx() self.stdscr.refresh() time.sleep(0.02) self.stdscr.refresh() self.stdscr.clear() #self.testWindowSize() def backspace(self): if self.xy[1] > 1: self.undo.push() self.xy[1] = self.xy[1] - 1 if self.playing: self.insertChar(ord(' '), fg=self.appState.defaultFgColor, bg=self.appState.defaultBgColor, frange=self.appState.playbackRange) else: self.insertChar(ord(' '), fg=self.appState.defaultFgColor, bg=self.appState.defaultFgColor) self.xy[1] = self.xy[1] - 1 def deleteKeyPop(self, frange=None): fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor if self.xy[1] > 0: self.undo.push() if frange: # framge range for frameNum in range(frange[0] - 1, frange[1]): self.mov.frames[frameNum].content[self.xy[0]].pop(self.xy[1] - 1) # line & add a blank self.mov.frames[frameNum].content[self.xy[0]].append(' ') # at the end of each line. self.mov.frames[frameNum].newColorMap[self.xy[0]].pop(self.xy[1] - 1) # line & add a blank self.mov.frames[frameNum].newColorMap[self.xy[0]].append([fg,bg]) # at the end of each line. else: self.mov.currentFrame.content[self.xy[0]].pop(self.xy[1] - 1) self.mov.currentFrame.content[self.xy[0]].append(' ') self.mov.currentFrame.newColorMap[self.xy[0]].pop(self.xy[1] - 1) self.mov.currentFrame.newColorMap[self.xy[0]].append([fg,bg]) # at the end of each line. def reverseDelete(self, frange=None): fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor if self.xy[1] > 0: self.undo.push() if frange: # framge range for frameNum in range(frange[0] - 1, frange[1]): self.mov.frames[frameNum].content[self.xy[0]].pop(self.xy[1] - 1) # line & add a blank self.mov.frames[frameNum].content[self.xy[0]].insert(0, ' ') # at the end of each line. self.mov.frames[frameNum].newColorMap[self.xy[0]].pop(self.xy[1] - 1) # line & add a blank self.mov.frames[frameNum].newColorMap[self.xy[0]].insert(0, [fg,bg]) # at the end of each line. else: self.mov.currentFrame.content[self.xy[0]].pop(self.xy[1] - 1) self.mov.currentFrame.content[self.xy[0]].insert(0, ' ') self.mov.currentFrame.newColorMap[self.xy[0]].pop(self.xy[1] - 1) self.mov.currentFrame.newColorMap[self.xy[0]].insert(0, [fg,bg]) # at the end of each line. def insertColor(self, fg=1, bg=0, frange=None, x=None, y=None, pushUndo=True): """ Sets the color for an x/y location on the current frame, or a range of frames """ if pushUndo: # push onto the clipboard stack self.undo.push() if x == None: x = self.xy[1] if y == None: y = self.xy[0] if frange: for fn in range(frange[0] - 1, frange[1]): self.mov.frames[fn].newColorMap[y][x - 1] = [fg, bg] else: self.mov.currentFrame.newColorMap[y][x - 1] = [fg, bg] #self.mov.currentFrame.newColorMap[self.xy[0]][self.xy[1] - 1] = [self.colorfg, self.colorbg] def insertChar(self, c, fg=1, bg=0, frange=None, x=None, y=None, moveCursor = False, pushUndo=True): """ insert character at current location, move cursor to the right (unless at the edge of canvas) """ if pushUndo: # push onto the clipboard stack self.undo.push() if x == None: x = self.xy[1] moveCursor = True if y == None: y = self.xy[0] if frange: # frame range for fn in range(frange[0] - 1, frange[1]): try: self.mov.frames[fn].content[y][x - 1] = chr(c) #self.mov.frames[fn].colorMap.update( # {(y,x - 1):(fg,bg)} ) self.mov.frames[fn].newColorMap[y][x - 1] = [fg, bg] except Exception as E: self.notify(f"There was an internal error: {E}", pause=True) self.notify(f"Frame: {fn}, x: {x}, y: {y}, fg: {fg}, bg: {bg}") self.notify(f"Please save your work and restart Durdraw. Sorry for the inconvenience.") break if x < self.mov.sizeX and moveCursor: self.move_cursor_right() #self.xy[1] = self.xy[1] + 1 else: self.mov.currentFrame.content[y][x - 1] = chr(c) #self.mov.currentFrame.colorMap.update( # {(y,x - 1):(fg,bg)} ) self.mov.currentFrame.newColorMap[y][x - 1] = [fg, bg] if x < self.mov.sizeX and moveCursor: self.move_cursor_right() #self.xy[1] = self.xy[1] + 1 def pickUpDrawingChar(self, col, line): # Sets the drawing chaaracter to the character under teh cusror. # esc-P self.appState.drawChar = self.mov.currentFrame.content[line][col] self.statusBar.drawCharPickerButton.label = self.appState.drawChar def eyeDrop(self, col, line): old_fg = self.colorfg old_bg = self.colorbg fg, bg = self.mov.currentFrame.newColorMap[line][col] fg, bg = self.mov.currentFrame.newColorMap[line][col] try: self.setFgColor(fg) self.setBgColor(bg) except: self.setFgColor(old_fg) self.setBgColor(old_bg) self.statusBar.colorPicker.handler.updateFgPicker() def clearCanvasPrompt(self): self.clearCanvas(prompting=True) def clearCanvas(self, prompting = False): clearing = True # assume we are clearing unless we prompt and say "n" if prompting: self.stdscr.nodelay(0) # wait for input when calling getch self.clearStatusLine() self.promptPrint("Are you sure you want to clear the canvas? (Y/N) " ) while prompting: time.sleep(0.01) c = self.stdscr.getch() if c == 121: # 121 = y clearing = True prompting = False elif c == 110: # 110 = n clearing = False prompting = False self.clearStatusLine() if clearing: #self.undo.push() # so we can undo this operation self.mov = Movie(self.opts) # initialize a new movie self.setPlaybackRange(1, self.mov.frameCount) self.undo = UndoManager(self, appState = self.appState) # reset undo system self.appState.sauce = dursauce.SauceParser() # empty sauce self.appState.curOpenFileName = None self.move_cursor_topleft() self.stdscr.clear() self.hardRefresh() def searchForStringPrompt(self): self.stdscr.nodelay(0) # wait for input when calling getch self.promptPrint("Enter string to search: ") curses.echo() search_string = self.stdscr.getstr().decode('utf-8') curses.noecho() search_result = self.mov.search_for_string(search_string) if search_result == False: self.notify("No results found.") else: line = search_result["line"] column = search_result["col"] frame_num = search_result["frame"] self.mov.gotoFrame(frame_num) self.move_cursor_to_line_and_column(line, column) if self.playing: elf.stdscr.nodelay(1) def showCharInspector(self): line = self.xy[0] col = self.xy[1] - 1 character = self.mov.currentFrame.content[line][col] fg = self.mov.currentFrame.newColorMap[line][col][0] bg = self.mov.currentFrame.newColorMap[line][col][1] charType = self.appState.charEncoding if charType == "utf-8": charValue = "U+" + str(hex(ord(character)))[2:] # aye chihuahua else: charValue = ord(character) # ascii/cp437 inspectorString = f"Fg: {fg}, Bg: {bg}, Char: {character}, {charType} value: {charValue}" try: ibmpc_value = str(ord(character.encode('cp437'))) inspectorString = inspectorString + f", cp437 value: {ibmpc_value}" except: pass self.notify(inspectorString, pause=True) def clickedChMap(self, mouseX, mouseY): # Find the character the user clicked, then set it # This isn't very wide-character friendly yet. char_offset = mouseX - self.chMap_offset char_number = int(char_offset / 3.1 + 1) # lol, madness if self.appState.debug: self.notify(f"Clicked character area: {str([mouseX, mouseY])}, F{char_number}") chMapKey = f"f{char_number}" self.appState.drawChar = chr(self.chMap[chMapKey]) def clickedInfoButton(self): realmaxY,realmaxX = self.realstdscr.getmaxyx() # test size if realmaxX < self.mov.sizeX + self.appState.sideInfo_minimum_width: # I'm not wide enough self.showFileInformation(notify=True) else: self.toggleShowFileInformation() #self.notify(f"realmaxX: {realmaxX}, self.mov.sizeX: {self.mov.sizeX}, self.appState.sideBar_minimum_width: {self.appState.sideBar_minimum_width}", pause=True) def toggleShowFileInformation(self): self.appState.viewModeShowInfo = not self.appState.viewModeShowInfo self.stdscr.clear() def toggleDebug(self): self.appState.debug = not self.appState.debug def toggleWideWrapping(self): #if self.appState.wrapWidth < 120: if self.appState.wrapWidth == 80: self.appState.wrapWidth = 900 else: self.appState.wrapWidth = 80 self.notify(f"Line wrapping for loading ANSIs set to: {self.appState.wrapWidth}") def toggleInjecting(self): self.appState.can_inject = not self.appState.can_inject def toggleIceColors(self): self.appState.iceColors = not self.appState.iceColors self.ansi.initColorPairs_cga() self.init_16_colors_misc() def showFileInformation(self, notify = False): fileInfoColumn = self.mov.sizeX + 2 # wrap info lines # textwrap.wrap(text, width=70, **kwargs) fileInfoWidth = max(2, self.appState.realmaxX - fileInfoColumn) #infoStringList = textwrap.wrap(infoString, width=fileInfoWidth) # eventually show a pop-up window with editable sauce info fileName = self.appState.curOpenFileName author = self.appState.sauce.author title = self.appState.sauce.title group = self.appState.sauce.group date = self.appState.sauce.date year = self.appState.sauce.year month = self.appState.sauce.month day = self.appState.sauce.day sixteenc_year = self.appState.sixteenc_year sixteenc_pack = self.appState.sixteenc_pack colorMode = self.appState.colorMode infoString = '' infoStringList = [] #self.stdscr.nodelay(0) # wait for input when calling getch if fileName: #infoStringList.append(f"File: {fileName}", width=fileInfoWidth) infoStringList += textwrap.wrap(f"File: {fileName}", width=fileInfoWidth, subsequent_indent=' ') if self.appState.fileShortPath: # extract folder name from full path folderName = self.appState.fileShortPath #infoStringList.append(f"Folder: {folderName}") infoStringList += textwrap.wrap(f"Location: {folderName}", width=fileInfoWidth, subsequent_indent=' ') if title: infoStringList += textwrap.wrap(f"Title: {title}", width=fileInfoWidth, subsequent_indent=' ') if author: infoStringList += textwrap.wrap(f"Artist: {author}", width=fileInfoWidth, subsequent_indent=' ') if group: infoStringList += textwrap.wrap(f"Group: {group}", width=fileInfoWidth, subsequent_indent=' ') if date: infoStringList += textwrap.wrap(f"Date: {year}/{month}/{day}", width=fileInfoWidth, subsequent_indent=' ') # 16c if self.appState.sixteenc_browsing: infoStringList += textwrap.wrap(f"16c Pack: {sixteenc_pack}", width=fileInfoWidth, subsequent_indent=' ') infoStringList += textwrap.wrap(f"16c Year: {sixteenc_year}",width=fileInfoWidth, subsequent_indent=' ') infoStringList.append(f"Width: {self.mov.sizeX}") infoStringList.append(f"Height: {self.mov.sizeY}") infoStringList.append(f"Color mode: {colorMode}") if self.appState.fileColorMode: infoStringList.append(f"File color mode: {self.appState.fileColorMode}") infoStringList += textwrap.wrap(f"Playing: {self.playing} ", width=fileInfoWidth, subsequent_indent=' ') if len(infoStringList) > 0: infoString = '\n '.join(infoStringList) wideViwer = False if notify: notifyString = f"file: {fileName}, title: {title}, author: {author}, group: {group}, width: {self.mov.sizeX}, height: {self.mov.sizeY}" self.notify(notifyString, pause=True) elif self.appState.viewModeShowInfo: # check and see if the window is wide enough for a nice side sauce wideViewer = False realmaxY,realmaxX = self.realstdscr.getmaxyx() if realmaxX + self.appState.firstCol >= self.mov.sizeX + self.appState.sideInfo_minimum_width: wideViewer = True #fileInfoColumn = self.mov.sizeX + 2 #fileInfoColumn = self.mov.sizeX + 4 #fileInfoColumn = realmaxX - self.appState.sideBar_minimum_width - 1 # show me the sauce fileInfoColor = self.appState.theme['promptColor'] if wideViewer: # in a nice list on the right lineNum = 3 for infoItem in infoStringList: # truncate to prevent writing last end of line (and wrapping) #maxLength = realmaxX - fileInfoColumn #itemString = infoItem[maxLength:] itemString = infoItem self.addstr(lineNum, fileInfoColumn, itemString, fileInfoColor) lineNum += 1 else: self.addstr(self.realmaxY - 1, 0, infoString, curses.color_pair(fileInfoColor)) def showTransformer(self): """ Let the user pick transformations: Bounce, Repeat, Reverse """ self.clearStatusLine() prompting = True while prompting: self.promptPrint("[B]ounce, [R]epeat, or Re[v]erse?") c = self.stdscr.getch() if c in [98]: # b - bounce |> -> |><| prompting = False self.transform_bounce() if c in [114]: # r - repeat |> -> |>|> prompting = False self.transform_repeat() if c in [118]: # v - reverse |> -> <| prompting = False self.transform_reverse() if c in [27]: # escape - cancel prompting = False def transform_bounce(self): """ |> -> |><| Clone all the frames in the range so they repeat once, reversing the 2nd half """ self.undo.push() self.mov = bounce_plugin.transform_movie(self.mov) self.clearStatusLine() self.mov.nextFrame() self.mov.prevFrame() self.setPlaybackRange(1, self.mov.frameCount) #self.hardRefresh() def transform_repeat(self): """ |> -> |>|> Clone all the frames in the range so they repeat once """ self.undo.push() self.mov = repeat_plugin.transform_movie(self.mov) self.clearStatusLine() self.mov.nextFrame() self.mov.prevFrame() self.setPlaybackRange(1, self.mov.frameCount) #self.hardRefresh() def transform_reverse(self): """ |> -> <| """ self.undo.push() self.mov = reverse_plugin.transform_movie(self.mov) self.clearStatusLine() self.mov.nextFrame() self.mov.prevFrame() self.hardRefresh() def moveCurrentFrame(self): self.undo.push() prompting = True self.clearStatusLine() startPosition = self.mov.currentFrameNumber newPosition = self.mov.currentFrameNumber while prompting: time.sleep(0.01) self.promptPrint("Use left/right to move frame, press Enter when done or Esc to cancel. (%i/%i)" % (newPosition, self.mov.frameCount)) c = self.stdscr.getch() if c in [98, curses.KEY_LEFT]: # move frame left one position (use pop and push) if newPosition == 1: # if at first newPosition = self.mov.frameCount # go to end else: newPosition -= 1 elif c in [102, curses.KEY_RIGHT]: if newPosition == self.mov.frameCount: # if at last newPosition = 1 # go back to beginning else: newPosition += 1 elif c in [13, curses.KEY_ENTER]: # enter - save new position self.mov.moveFramePosition(startPosition, newPosition) self.mov.gotoFrame(newPosition) prompting = False elif c in [27]: # escape - cancel self.undo.undo() prompting = False def increaseFPS(self): if self.opts.framerate != 50: # max 50fps self.opts.framerate += 1 def decreaseFPS(self): if self.opts.framerate != 1.0: # min 1fps self.opts.framerate -= 1 def showScrollingHelpScreen(self): self.appState.drawBorders = False wasPlaying = self.playing # push, to pop when we're done oldTopLine = self.appState.topLine # dito oldFirstCol = self.appState.firstCol # dito self.statusBar.colorPicker.hide() #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.hide() self.appState.sideBarShowing = False self.appState.topLine = 0 self.appState.firstCol = 0 self.playing = False self.stdscr.nodelay(1) # do not wait for input when calling getch last_time = time.time() self.cursorOff() self.playingHelpScreen = True self.appState.playingHelpScreen = True new_time = time.time() helpMov = self.appState.helpMov #if page == 1: # self.appState.sleep_time = (1000.0 / self.appState.helpMovOpts.framerate) / 1000.0 #elif page == 2: # self.appState.sleep_time = (1000.0 / self.appState.helpMovOpts_2.framerate) / 1000.0 self.appState.sleep_time = (1000.0 / self.appState.helpMovOpts.framerate) / 1000.0 helpMov.gotoFrame(1) self.stdscr.clear() self.clearStatusLine() #self.promptPrint("* Press the any key (or click) to continue *") clickColor = self.appState.theme['clickColor'] promptColor = self.appState.theme['promptColor'] #self.addstr(self.statusBarLineNum - 1, 0, "You can use ALT or META instead of ESC. Everything this color is clickable", curses.color_pair(promptColor)) #self.addstr(self.statusBarLineNum - 1, 51, "this color", curses.color_pair(clickColor) | curses.A_BOLD) #self.addstr(self.statusBarLineNum - 1, 65, "clickable", curses.color_pair(clickColor) | curses.A_BOLD) helpMov.search_and_replace(self, '{ver}', f"{self.appState.durVer}") helpMov.search_and_replace(self, '{colormode}', f"{self.appState.colorMode}") helpMov.search_and_replace(self, "{charmode}", f"{self.appState.charEncoding}") helpMov.search_and_replace(self, "{pyver}", f"{self.appState.pyVersion}") while self.playingHelpScreen: self.move(self.xy[0], self.xy[1]) self.refresh() #self.addstr(self.statusBarLineNum + 1, 0, "Up/Down, Pgup/Pgdown, Home/End or Mouse Wheel to scroll. Enter or Esc to exit.", curses.color_pair(promptColor)) <- now done in self.refresh() #self.addstr(self.statusBarLineNum - 1, 51, "this color", curses.color_pair(clickColor) | curses.A_BOLD) mouseState = False c = self.stdscr.getch() if c == curses.KEY_MOUSE: # get some mouse input if available try: _, mouseX, mouseY, _, mouseState = curses.getmouse() except: pass if c in [curses.KEY_UP, ord('k')]: # scroll up if self.appState.topLine > 0: self.appState.topLine = self.appState.topLine - 1 elif c in [curses.KEY_DOWN, ord('j')]: # scroll down if self.appState.topLine + self.realmaxY < helpMov.sizeY: # wtf? self.appState.topLine += 1 elif c in [339, curses.KEY_PPAGE, ord('u'), ord('b'), ord('<')]: # page up, and vim keys self.appState.topLine = self.appState.topLine - self.realmaxY + 3 if self.appState.topLine < 0: self.appState.topLine = 0 elif c in [338, curses.KEY_NPAGE, ord(' '), ord('d'), ord('f'), ord('>')]: # page down, and vi keys self.appState.topLine += self.realmaxY - 3 # go down 25 lines or whatever if self.appState.topLine > helpMov.sizeY - self.realmaxY: self.appState.topLine = helpMov.sizeY - self.realmaxY elif c in [339, curses.KEY_HOME]: # 339 = home self.appState.topLine = 0 elif c in [338, curses.KEY_END]: # 338 = end self.appState.topLine = helpMov.sizeY - self.realmaxY + 2 #elif c != -1: # -1 means no keys are pressed. #elif c == curses.KEY_ENTER: elif c in [10, 13, curses.KEY_ENTER, 27, ord('q')]: # 27 == escape key self.playingHelpScreen = False else: if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 try: curses.BUTTON5_PRESSED except: curses.BUTTON5_PRESSED = 0 try: curses.BUTTON4_PRESSED except: curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up if self.appState.topLine > 0: self.appState.topLine = self.appState.topLine - 1 elif mouseState & curses.BUTTON5_PRESSED: # wheel down if self.appState.topLine + self.realmaxY < helpMov.sizeY: # wtf? self.appState.topLine += 1 new_time = time.time() frame_delay = helpMov.currentFrame.delay if frame_delay > 0: realDelayTime = frame_delay else: realDelayTime = self.appState.sleep_time if new_time >= (last_time + realDelayTime): # Time to update the frame? If so... last_time = new_time # draw animation if helpMov.currentFrameNumber == helpMov.frameCount: helpMov.gotoFrame(1) else: helpMov.nextFrame() else: time.sleep(0.008) # to keep from sucking up cpu if not wasPlaying: self.stdscr.nodelay(0) # back to wait for input when calling getch self.cursorOn() self.appState.playingHelpScreen = False self.appState.playingHelpScreen_2 = False self.playingHelpScreen = False self.stdscr.clear() # pop old state self.playing = wasPlaying self.appState.topLine = oldTopLine self.appState.firstCol = oldFirstCol self.appState.drawBorders = True def showAnimatedHelpScreen(self, page=1): self.appState.drawBorders = False wasPlaying = self.playing # push, to pop when we're done oldTopLine = self.appState.topLine # dito oldFirstCol = self.appState.firstCol # dito self.appState.topLine = 0 self.appState.firstCol = 0 self.playing = False self.stdscr.nodelay(1) # do not wait for input when calling getch last_time = time.time() self.cursorOff() self.playingHelpScreen = True self.appState.playingHelpScreen = True if page == 2: self.appState.playingHelpScreen_2 = True else: self.appState.playingHelpScreen_2 = False new_time = time.time() if page == 2: helpMov = self.appState.helpMov_2 else: helpMov = self.appState.helpMov if page == 1: self.appState.sleep_time = (1000.0 / self.appState.helpMovOpts.framerate) / 1000.0 elif page == 2: self.appState.sleep_time = (1000.0 / self.appState.helpMovOpts_2.framerate) / 1000.0 helpMov.gotoFrame(1) self.stdscr.clear() self.clearStatusLine() self.promptPrint("* Press the any key (or click) to continue *") clickColor = self.appState.theme['clickColor'] promptColor = self.appState.theme['promptColor'] self.addstr(self.statusBarLineNum - 1, 0, "You can use ALT or META instead of ESC. Everything this color is clickable", curses.color_pair(promptColor)) self.addstr(self.statusBarLineNum - 1, 51, "this color", curses.color_pair(clickColor) | curses.A_BOLD) self.addstr(self.statusBarLineNum - 1, 65, "clickable", curses.color_pair(clickColor) | curses.A_BOLD) while self.playingHelpScreen: self.move(self.xy[0], self.xy[1]) self.refresh() if page == 2: self.addstr(12, 51, f"{self.appState.durVer}", curses.color_pair(promptColor)) self.addstr(13, 54, f"{self.appState.colorMode}", curses.color_pair(promptColor)) self.addstr(14, 62, f"{self.appState.charEncoding}", curses.color_pair(promptColor)) c = self.stdscr.getch() if c != -1: # -1 means no keys are pressed. self.playingHelpScreen = False new_time = time.time() frame_delay = helpMov.currentFrame.delay if frame_delay > 0: realDelayTime = frame_delay else: realDelayTime = self.appState.sleep_time if new_time >= (last_time + realDelayTime): # Time to update the frame? If so... last_time = new_time # draw animation if helpMov.currentFrameNumber == helpMov.frameCount: helpMov.gotoFrame(1) else: helpMov.nextFrame() else: time.sleep(0.008) # to keep from sucking up cpu if not wasPlaying: self.stdscr.nodelay(0) # back to wait for input when calling getch self.cursorOn() self.appState.playingHelpScreen = False self.appState.playingHelpScreen_2 = False self.playingHelpScreen = False self.stdscr.clear() self.playing = wasPlaying self.appState.topLine = oldTopLine self.appState.firstCol = oldFirstCol self.appState.drawBorders = True def showDurViewHelp(self): helpLines = [ "Up, k, mouse wheel - Scroll up", "Down, j, mouse wheel - Scroll down", "Left, h - Scroll left", "Right, l - Scroll right", "PgUp, u - Scroll up one page", "PgDown, d - Scroll down one page", "Home - Scroll to top", "End - Scroll to bottom", "+, = - Increase animation speed", "- - Decrease animation speed", "i - Show file information", "v - Set VGA Terminal Colors", "Enter, Esc - Back to file list", "?, h, F1 - Help", "q - Exit Viewer" ] popup = self.popupNotification(pop_items = helpLines) def popupNotification(self, pop_items = ["Just a notification."]): width = len(max(pop_items, key=len)) height = len(pop_items) realmaxY,realmaxX = self.realstdscr.getmaxyx() topLine = realmaxY - height #self.notify(f"topLine: {topLine}, realmaxY: {realmaxY}, height: {height}, width: {width}", pause=True) # draw top border for col in range(0, width + 1): self.addstr(topLine - 1, col, ".", curses.color_pair(self.appState.theme['borderColor'])) self.addstr(topLine - 1, col + 1, " ", curses.color_pair(self.appState.theme['borderColor'])) for line in range(0, height): self.addstr(topLine + line, 0, " " * width, curses.color_pair(self.appState.theme['menuItemColor'])) self.addstr(topLine + line, 0, pop_items[line], curses.color_pair(self.appState.theme['menuItemColor'])) self.addstr(topLine + line, width, ": ", curses.color_pair(self.appState.theme['borderColor'])) self.stdscr.refresh() self.stdscr.nodelay(0) # wait for input when calling getch c = self.stdscr.getch() self.stdscr.nodelay(1) # do not wait for input when calling getch self.clearStatusLine() def justPass(self): """ Dummy callback for menu item repurposing """ pass def showViewerHelp(self): """ Show the help screen for the player/viewer mode """ #helpString = "Up/down Pgup/Pgdown Home/end - Scroll, i - File Info. -/+ - Speed. q - Exit Viewer" #self.notify(helpString, pause=True) self.showDurViewHelp() self.cursorOff() def apply_neofetch_keys(self): """ Called by the user at Runtime to search/replace inside the app. """ neofetch = self.appState.isAppAvail("neofetch") if neofetch: self.undo.push() self.appState.fetchData = neofetcher.run() # populate fetchData with NeoFetch data ... self.replace_neofetch_keys() # ... so this can work. self.refresh() else: self.notify("Neofetch not found in path. Please install it and try again.") return False def replace_neofetch_keys(self): """ Find all the Neofetch {keys} in the drawing, and replace them with Neofetch populated data """ for key in neofetcher.neo_keys: dur_key = '{' + key + '}' self.mov.search_and_replace(self, dur_key, self.appState.fetchData[key]) def handlePlayOnlyModeInput(self, c): mouseState = False if c == curses.KEY_MOUSE: # to support mouse wheel scrolling try: _, mouseX, mouseY, _, mouseState = curses.getmouse() except: pass realmaxY,realmaxX = self.realstdscr.getmaxyx() if mouseState == curses.BUTTON1_CLICKED: pass #self.showFileInformation() elif mouseState == curses.BUTTON1_DOUBLE_CLICKED: # It's as if we'd pressed enter to exit the viewer mode. self.playing = False self.appState.topLine = 0 if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 try: curses.BUTTON5_PRESSED except: curses.BUTTON5_PRESSED = 0 try: curses.BUTTON4_PRESSED except: curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up if self.appState.topLine > 0: self.appState.topLine = self.appState.topLine - 1 elif mouseState & curses.BUTTON5_PRESSED: # wheel down if self.appState.topLine + self.realmaxY < self.mov.sizeY: # wtf? self.appState.topLine += 1 elif c in [339, curses.KEY_PPAGE, ord('u'), ord('b')]: # page up, and vim keys self.appState.topLine = self.appState.topLine - self.realmaxY + 3 if self.appState.topLine < 0: self.appState.topLine = 0 elif c in [338, curses.KEY_NPAGE, ord(' '), ord('d'), ord('f')]: # page down, and vi keys if self.mov.sizeY > self.realmaxY - 3: # if the ansi is larger than a page... self.appState.topLine += self.realmaxY - 3 # go down 25 lines or whatever if self.appState.topLine > self.mov.sizeY - self.realmaxY: self.appState.topLine = self.mov.sizeY - self.realmaxY # prevent a ghost image on any blank lines at the bottom: #self.stdscr.clear() #self.refresh() elif c in [339, curses.KEY_HOME]: # 339 = home self.appState.topLine = 0 elif c in [338, curses.KEY_END]: # 338 = end self.appState.topLine = self.mov.sizeY - self.realmaxY + 2 elif c in [curses.KEY_LEFT, ord('h')]: # left - scroll left self.scroll_viewer_left() elif c in [curses.KEY_RIGHT, ord('l')]: # right - scroll right self.scroll_viewer_right() elif c in [ord('v')]: # v - enable VGA colors self.enableTrueVGAColors() #elif c in [ord('H')]: # H = scroll all the way left (like home in editor) # self.xy[1] #elif c in [ord('L')]: # L = scroll all the way right (like end in editor) # self.xy[1] = self.mov.sizeX if c in [61, 43]: # = and + - fps up self.increaseFPS() self.appState.sleep_time = (1000.0 / self.opts.framerate) / 1000.0 elif c in [45]: # - (minus) - fps down self.decreaseFPS() self.appState.sleep_time = (1000.0 / self.opts.framerate) / 1000.0 if c in [ord('q'), ord('Q')]: self.playing = False self.appState.topLine = 0 if not self.appState.editorRunning: self.verySafeQuit() elif c in [27, 10, 13, curses.KEY_ENTER]: # 27 = esc, 10 = LF, 13 = CR self.playing = False self.appState.topLine = 0 elif c in [ord('?')]: self.showViewerHelp() elif c in [ord('i'), ord('I')]: # toggle showing info. True/false swap: self.appState.viewModeShowInfo = not self.appState.viewModeShowInfo if self.appState.viewModeShowInfo: self.showFileInformation() else: self.stdscr.clear() self.hardRefresh() self.refresh() #self.showFileInformation() elif c in [curses.KEY_DOWN, ord('j')]: if self.appState.topLine + self.realmaxY < self.mov.sizeY: # wtf? self.appState.topLine += 1 elif c in [curses.KEY_UP, ord('k')]: if self.appState.topLine > 0: self.appState.topLine = self.appState.topLine - 1 elif c == 12: # ctrl-l - harder refresh self.stdscr.clear() self.hardRefresh() c = None def startPlaying(self, mov=None, opts=None): """ Start playing the animation - start a "game" style loop, make FPS by drawing if current time == greater than a delta plus the time the last frame was drawn. """ tempMovie = None tempOpts = None if mov != None: # playing a movie other than the main or help file tempMovie = self.mov self.mov = mov if opts != None: tempOpts = self.opts self.opts = opts self.commandMode = False oldTopLine = self.appState.topLine oldFirstCol = self.appState.firstCol cursorMode = self.appState.cursorMode if not self.statusBar.toolButton.hidden: self.statusBar.toolButton.draw() self.statusBar.toolButton.hide() if not self.statusBar.animButton.hidden: self.statusBar.animButton.draw() self.statusBar.animButton.hide() self.drawStatusBar() self.stdscr.nodelay(1) # do not wait for input when calling getch last_time = time.time() #self.statusBar.drawCharPickerButton.hide() if self.appState.playOnlyMode: self.statusBar.colorPicker.hide() #self.statusBar.colorPicker_bg_16.hide() self.appState.sideBarShowing = False self.statusBar.hide() self.cursorOff() self.setWindowTitle(self.appState.curOpenFileName) self.playing = True self.metaKey = 0 if self.appState.playOnlyMode: self.appState.drawBorders = False if not self.appState.playOnlyMode: # mode, show extra stuff. self.drawStatusBar() playedTimes = 1 new_time = time.time() # see how many milliseconds we have to sleep for # then divide by 1000.0 since time.sleep() uses seconds self.appState.sleep_time = (1000.0 / self.opts.framerate) / 1000.0 self.mov.gotoFrame(self.appState.playbackRange[0]) mouseX, mouseY = 0, 0 while self.playing: # catch keyboard input - to change framerate or stop pnlaying animation # get keyboard input, returns -1 if none available self.move(self.xy[0], self.xy[1]) # Here refreshScreen=False because we will self.stdscr.refresh() below, after drawing the status bar (to avoid flicker) self.refresh(refreshScreen=False) if self.appState.viewModeShowInfo: self.showFileInformation() if not self.appState.playOnlyMode: self.drawStatusBar() self.move(self.xy[0], self.xy[1] - 1) # reposition cursor c = self.stdscr.getch() # handle resize resized = False realmaxY,realmaxX = self.realstdscr.getmaxyx() if self.appState.realmaxY != realmaxY: resized = True self.appState.realmaxY = realmaxY if self.appState.realmaxX != realmaxX: resized = True self.appState.realmaxX = realmaxX if resized: pass #self.notify("Debug: resized") #self.appState.topLine = 0 #self.appState.firstCol = 0 #debugstring = f"self.appState.realmaxY: {self.appState.realmaxY}, self.appState.realmaxX: {self.appState.realmaxX}, topLine: {self.appState.topLine}, firstCol: {self.appState.firstCol}" #self.addstr(self.statusBarLineNum - 1, 0, debugstring) self.stdscr.refresh() if c == 27: if self.appState.durview_running and self.appState.playOnlyMode: # This block captures Esc before the "UI for Play Only mode" # part below, so I'm putting this here. self.playing = False self.appState.topLine = 0 return True self.metaKey = 1 self.pressingButton = False if self.pushingToClip: self.pushingToClip = False self.disableMouseReporting() self.commandMode = True c = self.stdscr.getch() # normal esc # Clear out any canvas state as needed for command mode. For example... # If we think the mouse button is pressed.. stop thinking that. # In other words, un-stick the mouse button in case it's stuck: if self.metaKey == 1 and not self.appState.playOnlyMode and c != curses.ERR: # esc self.pressingButton = False #if cursorMode != "Draw" and cursorMode != "Paint": # print('\033[?1003l') # disable mouse reporting # self.hardRefresh() # curses.mousemask(1) # curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if self.pushingToClip: self.pushingToClip = False if c == 91: c = self.stdscr.getch() # alt-arrow does this in this mrxvt 5.x build if c in [61, 43]: # esc-= and esc-+ - fps up self.increaseFPS() self.appState.sleep_time = (1000.0 / self.opts.framerate) / 1000.0 elif c in [45]: # esc-- (alt minus) - fps down self.decreaseFPS() self.appState.sleep_time = (1000.0 / self.opts.framerate) / 1000.0 elif c in [98, curses.KEY_LEFT]: # alt-left - prev bg color (in 16) if self.appState.colorMode == "16": self.prevBgColor() elif self.appState.colorMode == "256": self.prevFgColor() #self.statusBar.colorPicker.handler.move_down_256() c = None elif c in [102, curses.KEY_RIGHT]: # alt-right - next bg color if self.appState.colorMode == "16": self.nextBgColor() elif self.appState.colorMode == "256": self.nextFgColor() #self.statusBar.colorPicker.handler.move_down_256() c = None elif c in [curses.KEY_DOWN, "\x1b\x1b\x5b\x42"]: # alt-down - prev fg color if self.appState.colorMode == "16": self.prevFgColor() elif self.appState.colorMode == "256": self.statusBar.colorPicker.handler.move_down_256() c = None elif c == curses.KEY_UP: # alt-up - next fg color if self.appState.colorMode == "16": self.nextFgColor() elif self.appState.colorMode == "256": self.statusBar.colorPicker.handler.move_up_256() c = None elif c == 91 or c == 339: # alt-[ previous character set. apparently this doesn't work self.prevCharSet() # during playback, c == -1, so 339 is alt-pgup, as a backup elif c == 93 or c == 338: # alt-] (93) or aplt-pagdown next character set self.nextCharSet() elif c == 83: # alt-S - pick a character set self.showCharSetPicker() self.stdscr.nodelay(1) # do not wait for input when calling getch elif c == 46: # alt-. - insert column self.addCol(frange=self.appState.playbackRange) elif c == 44: # alt-, - erase/pop current column self.delCol(frange=self.appState.playbackRange) elif c == 39: # alt-' - insert line self.addLine(frange=self.appState.playbackRange) elif c == 59: # alt-; - erase line self.delLine(frange=self.appState.playbackRange) elif c == 105: # alt-i - File/Canvas Information self.clickedInfoButton() elif c == 109 or c == 102: # alt-m or alt-f - load menu #self.statusBar.menuButton.on_click() self.commandMode = False self.openMenu("File") elif c == 99: # alt-c - color picker self.commandMode = False #if self.appState.colorMode == "256": # self.statusBar.colorPickerButton.on_click() #self.statusBar.colorPickerButton.on_click() self.selectColorPicker() elif c == ord(' '): # alt-space - insert drawing character drawChar = self.appState.drawChar x_param = self.xy[1] y_param = self.xy[0] self.insertChar(ord(drawChar), fg=self.colorfg, bg=self.colorbg, x=x_param, y=y_param, moveCursor=True, pushUndo=True, frange=self.appState.playbackRange) elif c == 122: # alt-z = undo self.clickedUndo() elif c == 114: # alt-r = redo self.clickedRedo() elif c == 82: # alt-R = set playback range self.getPlaybackRange() elif c in [112]: # alt-p - stop playing self.stopPlaying() elif c == 111: # alt-o - open self.stopPlaying() self.openFromMenu() # as if we clicked menu->open elif c in [104, 63]: # alt-h - help self.showHelp() c = None elif c == 113: # alt-q - quit if self.appState.durview_running: self.stopPlaying() self.appState.editorRunning = False return else: self.safeQuit() self.stdscr.nodelay(1) c = None elif c in [ord('1')]: # esc-1 copy of F1 - insert extended character self.insertChar(self.chMap['f1'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None self.hardRefresh() elif c in [ord('2')]: # esc-2 copy of F2 - insert extended character self.insertChar(self.chMap['f2'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None self.hardRefresh() elif c in [ord('3')]: # F3 - insert extended character self.insertChar(self.chMap['f3'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None self.hardRefresh() elif c in [ord('4')]: # F4 - insert extended character self.insertChar(self.chMap['f4'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None self.hardRefresh() elif c in [ord('5')]: # F5 - insert extended character self.insertChar(self.chMap['f5'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None self.hardRefresh() elif c in [ord('6')]: # F6 - insert extended character self.insertChar(self.chMap['f6'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None self.hardRefresh() elif c in [ord('7')]: # F7 - insert extended character self.insertChar(self.chMap['f7'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None self.hardRefresh() elif c in [ord('8')]: # F8 - insert extended character self.insertChar(self.chMap['f8'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None self.hardRefresh() elif c in [ord('9')]: # F9 - insert extended character self.insertChar(self.chMap['f9'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None self.hardRefresh() elif c in [ord('0')]: # F10 - insert extended character self.insertChar(self.chMap['f10'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None self.hardRefresh() else: if self.appState.debug: self.notify("keystroke: %d" % c) # alt-unknown self.commandMode = 0 self.metaKey = 0 c = None elif c != -1: # -1 means no keys are pressed. # up or down to change framerate, otherwise stop playing #if self.appState.durview_running: # Extra UI for DurView # if c in [ord('e')]: # e - open file in editor # self.openEditorFromDurview() # c = None if self.appState.playOnlyMode: # UI for Play-only mode self.handlePlayOnlyModeInput(c) else: if c == curses.KEY_MOUSE: # Remember, we are playing here try: _, mouseX, mouseY, _, mouseState = curses.getmouse() except: pass realmaxY,realmaxX = self.realstdscr.getmaxyx() # enable mouse tracking only when the button is pressed if mouseState == curses.BUTTON1_CLICKED: if self.pressingButton: self.pressingButton = False if cursorMode != "Draw" and cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if self.pushingToClip: self.pushingToClip = False if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 try: curses.BUTTON5_PRESSED except: curses.BUTTON5_PRESSED = 0 try: curses.BUTTON4_PRESSED except: curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up self.move_cursor_up() elif mouseState & curses.BUTTON5_PRESSED: # wheel down self.move_cursor_down() if mouseState & curses.BUTTON1_PRESSED: #if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX: # in edit area if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX \ and mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum: if not self.pressingButton: self.pressingButton = True print('\033[?1003h') # enable mouse tracking with the XTERM APIP self.hardRefresh() else: self.pressingButton = False if cursorMode != "Draw" and cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if self.pushingToClip: self.pushingToClip = False else: if self.pressingButton: self.pressingButton = False if self.pushingToClip: self.pushingToClip = False #if self.appState.cursorMode != "Draw": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if self.pressingButton or mouseState == curses.BUTTON1_CLICKED: # self.playing == True self.gui.got_click("Click", mouseX, mouseY) if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX \ and mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum: # we clicked in edit area, so move the cursor self.xy[1] = mouseX + 1 # set cursor position self.xy[0] = mouseY + self.appState.topLine elif mouseX < realmaxX and mouseY in [self.statusBarLineNum, self.statusBarLineNum+1]: # we clicked on the status bar while playing. if mouseY == self.statusBarLineNum: # clicked upper bar offset = 6 # making room for the menu bar tOffset = realmaxX - (realmaxX - self.transportOffset) + 6 if not self.appState.narrowWindow: if mouseX in [tOffset, tOffset + 1]: # clicked pause button self.clickHighlight(tOffset, "||") self.stopPlaying() elif mouseX == 12 + offset: # clicked FPS down self.clickHighlight(12 + offset, "<") self.decreaseFPS() self.appState.sleep_time = (1000.0 / self.opts.framerate) / 1000.0 elif mouseX == 16 + offset: # clicked FPS up self.clickHighlight(16 + offset, ">") self.increaseFPS() self.appState.sleep_time = (1000.0 / self.opts.framerate) / 1000.0 elif mouseY == self.statusBarLineNum+1: # clicked bottom bar if not self.appState.narrowWindow: if mouseX in range(4,20): fg = mouseX - 3 self.setFgColor(fg) elif mouseX in range(25,33): bg = mouseX - 24 self.setBgColor(bg) elif mouseX == self.chMap_offset + len(self.chMapString): # clicked next character set self.clickHighlight(self.chMap_offset + len(self.chMapString), ">", bar='bottom') self.nextCharSet() elif mouseX == self.chMap_offset - 1: # clicked previous character set self.clickHighlight(self.chMap_offset - 1, "<", bar='bottom') self.prevCharSet() if self.appState.debug: self.notify("bottom bar. " + str([mouseX, mouseY])) elif self.appState.debug: self.notify("clicked. " + str([mouseX, mouseY])) elif c == curses.KEY_LEFT: # left - move cursor right a character self.move_cursor_left() elif c == curses.KEY_RIGHT: # right - move cursor right self.move_cursor_right() elif c == curses.KEY_UP: # up - move cursor up self.move_cursor_up() elif c == curses.KEY_DOWN: # down - move curosr down self.move_cursor_down() elif c in [339, curses.KEY_PPAGE]: # page up self.move_cursor_pgup() elif c in [338, curses.KEY_NPAGE]: # page down self.move_cursor_pgdown() elif c in [339, curses.KEY_HOME]: # 339 = home self.xy[1] = 1 elif c in [338, curses.KEY_END]: # 338 = end self.xy[1] = self.mov.sizeX elif c in [10, 13, curses.KEY_ENTER]: # enter (10 if we self.move_cursor_enter() # don't do curses.nonl()) elif c in [263, 127]: # backspace self.backspace() elif c in [9, 353]: # 9 = tab, 353 = shift-tab #if self.appState.colorMode == "256": # self.statusBar.colorPickerButton.on_click() self.statusBar.colorPickerButton.on_click() elif c in [330]: # delete self.deleteKeyPop(frange=self.appState.playbackRange) elif c in [383]: # shift-delete - delete from opposite direction self.reverseDelete(frange=self.appState.playbackRange) elif c in [1, curses.KEY_HOME]: # ctrl-a or home self.xy[1] = 1 elif c in [5, curses.KEY_END]: # ctrl-e or end self.xy[1] = self.mov.sizeX elif c in [curses.KEY_F1]: # F1 - insert extended character self.insertChar(self.chMap['f1'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None elif c in [curses.KEY_F2]: # F2 - insert extended character self.insertChar(self.chMap['f2'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None elif c in [curses.KEY_F3]: # F3 - insert extended character self.insertChar(self.chMap['f3'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None elif c in [curses.KEY_F4]: # F4 - insert extended character self.insertChar(self.chMap['f4'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None elif c in [curses.KEY_F5]: # F5 - insert extended character self.insertChar(self.chMap['f5'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None elif c in [curses.KEY_F6]: # F6 - insert extended character self.insertChar(self.chMap['f6'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None elif c in [curses.KEY_F7]: # F7 - insert extended character self.insertChar(self.chMap['f7'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None elif c in [curses.KEY_F8]: # F8 - insert extended character self.insertChar(self.chMap['f8'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None elif c in [curses.KEY_F9]: # F9 - insert extended character self.insertChar(self.chMap['f9'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None elif c in [curses.KEY_F10]: # F10 - insert extended character self.insertChar(self.chMap['f10'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) c = None elif c != None and c <= 128 and c >= 32: # normal printable character self.insertChar(c, fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) shouldDraw = False while True: if self.appState.playOnlyMode: self.handlePlayOnlyModeInput(self.stdscr.getch()) new_time = time.time() frame_delay = self.mov.currentFrame.delay if frame_delay > 0: realDelayTime = frame_delay else: realDelayTime = self.appState.sleep_time # self.appState.sleep_time == (1000.0 / self.opts.framerate) / 1000.0 time.sleep(0.009) # to keep from sucking up cpu if new_time >= (last_time + realDelayTime): # Time to update the frame? If so... shouldDraw = True break if not self.appState.playOnlyMode: break if not self.playing: break if shouldDraw: last_time = new_time # draw animation if not self.appState.playOnlyMode: if self.mov.currentFrameNumber == self.appState.playbackRange[1]: self.mov.gotoFrame(self.appState.playbackRange[0]) else: self.mov.nextFrame() else: self.mov.nextFrame() if not self.appState.playOnlyMode: # if we're not in play-only # mode, show extra stuff. self.drawStatusBar() else: if self.appState.playNumberOfTimes > 0: # if we're playing x times if self.mov.currentFrameNumber == self.mov.frameCount: # and on the last frame if playedTimes < self.appState.playNumberOfTimes: playedTimes += 1 else: # we've played the desired number of times. self.playing = False if tempMovie != None: # Need to switch back to main movie self.mov = tempMovie if tempOpts != None: self.opts = tempOpts self.appState.topLine = oldTopLine self.appState.firstCol = oldFirstCol self.stdscr.nodelay(0) # back to wait for input when calling getch self.cursorOn() def stopPlaying(self): self.playing = False self.statusBar.toolButton.show() self.statusBar.animButton.show() if self.appState.cursorMode == "Draw" or self.appState.cursorMode == "Paint": self.statusBar.drawCharPickerButton.show() self.enableMouseReporting() def genCharSet(self, firstChar): # firstChar is a unicode number newSet = {} newChar = firstChar for x in range(1,11): # 1-10 keyStr = 'f%i' % x newSet.update({keyStr:newChar}) newChar += 1 return newSet def initCharSet(self): # we can have nextCharSet and PrevCharSet to switch between chars in set # Can also add encoding= paramater for different encodings, eg: ascii, utf-8, etc. self.charMapNumber = 0 if self.appState.characterSet == "Durdraw Default": self.fullCharMap = [ \ # All of our unicode templates live here. Blank template: #{'f1':, 'f2':, 'f3':, 'f4':, 'f5':, 'f6':, 'f7':, 'f8':, 'f9':, 'f10':}, # block characters {'f1':9617, 'f2':9618, 'f3':9619, 'f4':9608, 'f5':9600, 'f6':9604, 'f7':9612, 'f8':9616, 'f9':9632, 'f10':183 }, # ibm-pc looking block characters (but unicode instead of ascii) {'f1':9601, 'f2':9602, 'f3':9603, 'f4':9604, 'f5':9605, 'f6':9606, 'f7': 9607, 'f8':9608, 'f9' :9600, 'f10':0x2594 }, # more block elements - mostly bottom half fills {'f1':0x2588, 'f2':0x2589, 'f3':0x258A, 'f4':0x258B, 'f5':0x258C, 'f6':0x258D, 'f7':0x258E, 'f8':0x258F, 'f9':0x2590, 'f10':0x2595}, # partial left and right fills # geometric shapes #{'f1':0x25dc, 'f2':0x25dd, 'f3':0x25de, 'f4':0x25df, 'f5':0x25e0, 'f6':0x25e1, 'f7':0x25e2, 'f8':0x25e3, 'f9':0x25e4, 'f10':0x25e5}, # little curves and triangles {'f1':0x25e2, 'f2':0x25e3, 'f3':0x25e5, 'f4':0x25e4, 'f5':0x25c4, 'f6':0x25ba, 'f7':0x25b2, 'f8':0x25bc, 'f9':0x25c0, 'f10':0x25b6 }, # little curves and triangles #{'f1':'🮜', 'f2':'🮝', 'f3':'🮞', 'f4':'🮟', 'f5':0x25c4, 'f6':0x25ba, 'f7':0x25b2, 'f8':0x25bc, 'f9':0x25c0, 'f10':0x25b6 }, # little curves and triangles # terminal graphic characters {'f1':9622, 'f2':9623, 'f3':9624, 'f4':9625, 'f5':9626, 'f6':9627, 'f7':9628, 'f8':9629, 'f9':9630, 'f10':9631 }, # terminal graphic characters #{'f1':0x1FB9C, 'f2':0x1FB9D, 'f3':0x1FB9F, 'f4':0x1FB9E, 'f5':0x1FB9A, 'f6':0x1FB9B, 'f7':0x1FB65, 'f8':0x1FB5A, 'f9':0x1FB4B, 'f10':0x1FB40}, # legacy computing smooth terminal mosaic characters - newer versions of unicode Triangles and shit.. not sure why it isn't working #{'f1':9581, 'f2':9582, 'f3':9583, 'f4':9584, 'f5':9585, 'f6':9586, 'f7':9587, 'f8':9472, 'f9':9474, 'f10':9532 }, # character cell arcs, aka curved pipes {'f1':9581, 'f2':9582, 'f3':9584, 'f4':9583, 'f5':9472, 'f6':9474, 'f7':9585, 'f8':9586, 'f9':9587, 'f10':9532 }, # character cell arcs, aka curved pipes #{'f1':130032, 'f2':0x1FBF2, 'f3':0x1FBF3, 'f4':0x1FBF4, 'f5':0x1FBF5, 'f6':0x1FBF6, 'f7':0x1FBF7, 'f8':0x1FBF8, 'f9':0x1FBF9, 'f10':0x1FBF0}, # lcd/led-style digits #{'f1':0x1FB8C, 'f2':ord('🭀'), 'f3':0x1FBF3, 'f4':0x1FBF4, 'f5':0x1FBF5, 'f6':0x1FBF6, 'f7':0x1FBF7, 'f8':0x1FBF8, 'f9':0x1FBF9, 'f10':0x1FBF0}, # lcd/led-style digits ] # generate some unicode sets via offset self.fullCharMap.append(self.genCharSet(0x25a0)) # geometric shapes self.fullCharMap.append(self.genCharSet(0x25e6)) # more geometric shapes self.fullCharMap.append(self.genCharSet(0x25c6)) # geometrics - diamond and circles self.fullCharMap.append(self.genCharSet(0x02ef)) # UPA modifiers self.fullCharMap.append(self.genCharSet(0x02c2)) # UPA modifiers self.fullCharMap.append(self.genCharSet(0x2669)) # music symbols self.fullCharMap.append(self.genCharSet(0xFF66)) # half-width kanji letters self.fullCharMap.append(self.genCharSet(0xFF70)) # half-width kanji letters self.fullCharMap.append(self.genCharSet(0xFF7a)) # half-width kanji letters self.fullCharMap.append(self.genCharSet(0xFF84)) # half-width kanji letters self.fullCharMap.append(self.genCharSet(0xFF8e)) # half-width kanji letters #self.fullCharMap.append(self.genCharSet(0xFF98)) # half-width kanji letters #self.fullCharMap.append(self.genCharSet(0x1F603)) # smiley emojis self.fullCharMap.append(self.genCharSet(0x2801)) # braile a-j self.fullCharMap.append(self.genCharSet(0x2805)) # braile k-t self.fullCharMap.append(self.genCharSet(0x2825)) # braile u+ self.fullCharMap.append(self.genCharSet(0x2b2c)) # ellipses self.chMap = self.fullCharMap[self.charMapNumber] # Map a dict of F1-f10 to character values #if self.appState.charEncoding == 'cp437': # # ibm-pc/cp437 ansi block character # self.chMap = {'f1':176, 'f2':177, 'f3':178, 'f4':219, 'f5':223, 'f6':220, 'f7':221, 'f8':222, 'f9':254, 'f10':250 } # self.fullCharMap = [ self.chMap ] # self.appState.colorPickChar = self.appState.CP438_BLOCK # ibm-pc/cp437 ansi block character # self.appState.blockChar = self.appState.CP438_BLOCK # self.appState.drawChar = self.appState.CP438_BLOCK elif self.appState.characterSet == "Unicode Block": self.setUnicodeBlock(block=self.appState.unicodeBlock) self.chMap = self.fullCharMap[self.charMapNumber] self.appState.colorPickChar = self.appState.UTF8_BLOCK # ibm-pc/cp437 ansi block character self.appState.blockChar = self.appState.UTF8_BLOCK self.appState.drawChar = self.appState.UTF8_BLOCK self.refreshCharMap() def loadCharsetFile(self, file_path: str): fullCharMap = durchar.load_charmap_file(file_path, self.appState, setting=True) if not fullCharMap: self.notify(f"Error loading charmap file: {file_path}") return False self.fullCharMap = fullCharMap self.charMapNumber = 0 self.chMap = self.fullCharMap[self.charMapNumber] self.refreshCharMap() def setCharacterSet(self, set_name): """ Set a Durdraw character set (not a Unicode block name) """ self.appState.characterSet = set_name miniSetName = f"{self.appState.characterSet[:3]}.." if self.appState.showCharSetButton: self.statusBar.charSetButton.label = miniSetName # [Name..] def setUnicodeBlock(self, block="Symbols for Legacy Computing"): self.fullCharMap = durchar.load_unicode_block(block) self.chMap = self.fullCharMap[self.charMapNumber] if self.statusBar: if self.appState.characterSet == "Unicode Block": miniSetName = f"{self.appState.unicodeBlock[:3]}.." else: miniSetName = f"{self.appState.characterSet[:3]}.." if self.appState.showCharSetButton: self.statusBar.charSetButton.label = miniSetName # [Name..] self.refreshCharMap() #self.chMapString = "F1%cF2%cF3%cF4%cF5%cF6%cF7%cF8%cF9%cF10%c" % \ #self.chMapString = "F1%c F2%c F3%c F4%c F5%c F6%c F7%c F8%c F9%c F10%c " % \ # (self.chMap['f1'], self.chMap['f2'], self.chMap['f3'], self.chMap['f4'], self.chMap['f5'], \ # self.chMap['f6'], self.chMap['f7'], self.chMap['f8'], self.chMap['f9'], self.chMap['f10'] ) def nextCharSet(self): if self.charMapNumber == len(self.fullCharMap) - 1: self.charMapNumber = 0 else: self.charMapNumber += 1 self.refreshCharMap() def prevCharSet(self): if self.charMapNumber == 0: self.charMapNumber = len(self.fullCharMap) - 1 else: self.charMapNumber -= 1 self.refreshCharMap() def refreshCharMap(self): # checks self.charMapNumber and does the rest self.chMap = self.fullCharMap[self.charMapNumber] #self.chMapString = "F1%c F2%c F3%c F4%c F5%c F6%c F7%c F8%c F9%c F10%c " % \ # Build a string, one character a time self.chMapString = "" for fkey in range(1,11): keyIndex = f"f{fkey}" keyString = f"F{fkey}" try: # See if we can encode character in the current mode. #keyChar = chr(self.chMap[keyIndex]).encode(self.appState.charEncoding) keyChar = chr(self.chMap[keyIndex]).encode(self.appState.charEncoding) #keyChar = keyChar.encode(self.appState.charEncoding) except: # If character can't encode, eg: in cp437 mode, make it blank keyChar = ' ' self.chMapString += f"{keyString}{keyChar}" try: self.chMapString = "F1%cF2%cF3%cF4%cF5%cF6%cF7%cF8%cF9%cF10%c" % \ (self.chMap['f1'], self.chMap['f2'], self.chMap['f3'], self.chMap['f4'], self.chMap['f5'], \ self.chMap['f6'], self.chMap['f7'], self.chMap['f8'], self.chMap['f9'], self.chMap['f10'] ) except Exception as E: print(f"Exception: {E}") pdb.set_trace() #self.chMapString = self.chMapStringncoding=self.appState.charEncoding) def clearStatusLine(self): # fyi .. width and height in this context should go into # appState, not in Movie(). In other words, this == not # the width and height of the movie, but of the editor screen. realmaxY,realmaxX = self.realstdscr.getmaxyx() self.addstr(self.statusBarLineNum, 0, " " * realmaxX) self.addstr(self.statusBarLineNum+1, 0, " " * realmaxX) def resizeHandler(self): """ Called when the window is resized """ # stick bottom of drawing to the bottom of the window # when resizing topLine = self.mov.sizeY - self.realmaxY - 2 if topLine < 0: topLine = 0 #self.appState.topLine = topLine if self.xy[0] < self.appState.topLine: # if cursor is off screen self.xy[0] = self.appState.topLine # put it back on def drawStatusBar(self): if self.statusBar.hidden: return False if self.realmaxX >= self.appState.full_ui_width: # Wide big window self.appState.narrowWindow = False # show menu buttons self.statusBar.menuButton.show() self.statusBar.toolButton.show() self.statusBar.animButton.show() self.statusBar.drawCharPickerButton.show() else: # narrow small window self.appState.narrowWindow = True # hide menu buttons self.statusBar.menuButton.hide() self.statusBar.toolButton.hide() self.statusBar.animButton.hide() self.statusBar.drawCharPickerButton.hide() self.setWindowTitle(self.appState.curOpenFileName) mainColor = self.appState.theme['mainColor'] clickColor = self.appState.theme['clickColor'] # This has also become a bit of a window resize handler if not self.playing: self.clearStatusLine() else: #self.clearStatusBarNoRefresh() pass realmaxY,realmaxX = self.realstdscr.getmaxyx() resized = False if self.appState.realmaxY != realmaxY: resized = True if self.appState.realmaxX != realmaxX: #self.notify("resized") resized = True if resized: self.stdscr.clear() self.appState.realmaxY = realmaxY self.appState.realmaxX = realmaxX # How far right to put the toolbar's little animation # stuff. Frame, FPS, Delay and Range. # Move it far right enough for the menus. # self.line_1_offset = 15 if self.appState.narrowWindow: self.line_1_offset = -2 # anchor left else: self.line_1_offset = realmaxX - 63 # anchor to the right by transport line_1_offset = self.line_1_offset statusBarLineNum = realmaxY - 2 self.appState.sideBarShowing = False # If the window is wide enough for the "side bar" (where sticky color goes) if self.playing and self.appState.sideBarShowing: self.appState.sideBarShowing = False self.statusBar.colorPicker.hide() #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.hide() if not self.appState.sideBarShowing and self.appState.sideBarEnabled and not self.playing: # Sidebar not showing, but enabled. Check and see if the window is wide enough if realmaxX > self.mov.sizeX + self.appState.sideBar_minimum_width: self.appState.sideBarShowing = True #self.notify("Wide. Showing color picker.") #if self.appState.colorMode == "256": # self.statusBar.colorPicker.show() self.statusBar.colorPicker.show() #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.show() #if not self.playing and self.appState.colorMode == "256": if not self.playing: if self.window_big_enough_for_colors(): self.appState.sideBarShowing = True self.statusBar.colorPicker.show() #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.show() else: self.statusBar.colorPicker.hide() #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.hide() if self.playing: self.statusBar.colorPicker.hide() #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.hide() self.appState.sideBarColumn = realmaxX - self.appState.sideBar_minimum_width - 1 #if self.appState.sideBarShowing: # We are clear to draw the Sidebar # Anchor the color picker to the bottom right #new_colorPicker_y = realmaxY - self.appState.colorBar_height - 2 #if self.appState.colorMode == "16": # # Make some space for colorPicker to be above the BG picker. # bg_height = self.statusBar.colorPicker_bg_16.handler.height + 1 #else: # bg_height = 0 #new_colorPicker_y = realmaxY - self.statusBar.colorPicker.handler.height - bg_height - 2 new_colorPicker_y = realmaxY - self.statusBar.colorPicker.handler.height - 2 new_colorPicker_x = realmaxX - self.statusBar.colorPicker.handler.width self.statusBar.colorPicker.handler.move(new_colorPicker_x, new_colorPicker_y) #if self.appState.colorMode == "16": # new_colorPicker_y = realmaxY - self.statusBar.colorPicker_bg_16.handler.height - 2 # new_colorPicker_x = realmaxX - self.statusBar.colorPicker_bg_16.handler.width # self.statusBar.colorPicker_bg_16.handler.move(new_colorPicker_x, new_colorPicker_y) #else: # # Move the color picker to just above the status bar # self.statusBar.colorPicker.handler.move(0, realmaxY - 10) self.statusBarLineNum = statusBarLineNum # resize window, tell the statusbar buttons self.statusBar.menuButton.update_real_xy(x = statusBarLineNum) self.statusBar.toolButton.update_real_xy(x = statusBarLineNum) self.statusBar.animButton.update_real_xy(x = statusBarLineNum) self.statusBar.colorPickerButton.update_real_xy(x = statusBarLineNum + 1) if self.appState.showCharSetButton: self.statusBar.charSetButton.update_real_xy(x = statusBarLineNum + 1) if not self.appState.narrowWindow: # Put character picker button left of character picker self.statusBar.drawCharPickerButton.update_real_xy(x = statusBarLineNum + 1) self.statusBar.drawCharPickerButton.update_real_xy(y = self.chMap_offset-11) #if self.appState.colorMode == "256": # self.statusBar.colorPickerButton.update_real_xy(x = statusBarLineNum + 1) canvasSizeBar = f"[{self.mov.sizeX}x{self.mov.sizeY}]" canvasSizeOffset = realmaxX - len(canvasSizeBar) - 1 # right of transport self.addstr(statusBarLineNum, canvasSizeOffset, canvasSizeBar, curses.color_pair(mainColor)) frameBar = "F:%i/%i " % (self.mov.currentFrameNumber, self.mov.frameCount) rangeBar = "R:%i-%i " % (self.appState.playbackRange[0], self.appState.playbackRange[1]) fpsBar = ":%i " % (self.opts.framerate) delayBar = "D:%.2f " % (self.mov.currentFrame.delay) # Ugly hardcoded locations. These should be handled in the GUI # framework instead. frameBar_offset = 2 + line_1_offset fpsBar_offset = 13 + line_1_offset fpsBar_minus_offset = fpsBar_offset + 4 delayBar_offset = 23 + line_1_offset rangeBar_offset = 31 + line_1_offset #chMap_offset = 35 # how far in to show the character map if self.appState.narrowWindow: if self.appState.colorMode == "16": self.chMap_offset = 1 # how far in to show the character map else: self.chMap_offset = 7 # how far in to show the character map else: self.chMap_offset = realmaxX - 50 # how far in to show the character map # > is hardcoded at 66. yeesh. chMap_next_offset = self.chMap_offset + 31 # Move the draw button to under the frameBar for 16 color mode #if self.appState.colorMode == "16": # self.statusBar.drawCharPickerButton.update_real_xy(x = statusBarLineNum) # self.statusBar.drawCharPickerButton.update_real_xy(y = frameBar_offset-5) # if realmaxX < 87: # too small to show the draw character. # self.statusBar.drawCharPickerButton.hide() # else: # self.statusBar.drawCharPickerButton.show() #if self.appState.colorMode == "256": # self.statusBar.drawCharPickerButton.show() # Draw elements that aren't in the GUI framework self.addstr(statusBarLineNum, frameBar_offset, frameBar, curses.color_pair(mainColor)) self.addstr(statusBarLineNum, fpsBar_offset, fpsBar, curses.color_pair(mainColor)) self.addstr(statusBarLineNum, delayBar_offset, delayBar, curses.color_pair(mainColor)) self.addstr(statusBarLineNum, rangeBar_offset, rangeBar, curses.color_pair(mainColor)) #if not self.appState.narrowWindow: # Wide big window # self.addstr(statusBarLineNum, frameBar_offset, frameBar, curses.color_pair(mainColor)) # self.addstr(statusBarLineNum, fpsBar_offset, fpsBar, curses.color_pair(mainColor)) # self.addstr(statusBarLineNum, delayBar_offset, delayBar, curses.color_pair(mainColor)) # self.addstr(statusBarLineNum, rangeBar_offset, rangeBar, curses.color_pair(mainColor)) #else: # Narrow small window # # Draw frame #. that's important. # frameBar_offset = 0 # fpsBar_offset = frameBar_offset + len(frameBar) + 1 # self.addstr(statusBarLineNum, frameBar_offset, frameBar, curses.color_pair(mainColor)) # self.addstr(statusBarLineNum, fpsBar_offset, fpsBar, curses.color_pair(mainColor)) # Move char button to the left of frameBar_offset if self.appState.debug: cp = self.ansi.colorPairMap[(self.colorfg, self.colorbg)] cp2 = self.colorpair pairs = len(self.ansi.colorPairMap) try: extColors = curses.has_extended_color_support() except: extColors = False colorValue = curses.color_content(self.colorfg) debugstring = f"Fg: {self.colorfg}, bg: {self.colorbg}, cpairs: {cp}, {cp2}, pairs: {pairs}, ext: {extColors}, {colorValue}" self.addstr(statusBarLineNum-1, 0, debugstring, curses.color_pair(mainColor)) debugstring2 = f"ButtonPress: {self.pressingButton}, topLine: {self.appState.topLine}, firstCol: {self.appState.firstCol}, resized: {resized}" self.addstr(statusBarLineNum-2, 0, debugstring2, curses.color_pair(mainColor)) colorValue = curses.color_content(self.colorbg) debugstring3= f"bg: {colorValue}" self.addstr(statusBarLineNum-3, 0, debugstring3, curses.color_pair(mainColor)) # Draw FG and BG colors if self.appState.colorMode == "256": self.addstr(statusBarLineNum+1, 0, "FG:", curses.color_pair(clickColor) | curses.A_BOLD) cp = self.ansi.colorPairMap[(self.colorfg, 0)] self.addstr(statusBarLineNum+1, 3, self.appState.colorPickChar * 2, curses.color_pair(cp)) if self.appState.showBgColorPicker: self.addstr(statusBarLineNum+1, 6, "BG:", curses.color_pair(clickColor) | curses.A_BOLD) cp = self.ansi.colorPairMap[(1, self.colorbg)] fillChar = ' ' self.addstr(statusBarLineNum+1, 9, fillChar * 2, curses.color_pair(cp)) # Draw character map for f1-f10 (block characters) self.addstr(statusBarLineNum+1, self.chMap_offset-1, "<", curses.color_pair(clickColor) | curses.A_BOLD) self.addstr(statusBarLineNum+1, self.chMap_offset+31, ">", curses.color_pair(clickColor) | curses.A_BOLD) #if self.appState.charEncoding == "cp437": # try: # chMapString = self.chMapString.encode('cp437') # except: # can't encode as cp437, so just render as blank # chMapString = ' ' if self.colorfg > 8 and self.appState.colorMode == "16": # bright color self.addstr(statusBarLineNum+1, self.chMap_offset, self.chMapString, curses.color_pair(self.colorpair) | curses.A_BOLD) else: # normal color self.addstr(statusBarLineNum+1, self.chMap_offset, self.chMapString, curses.color_pair(self.colorpair)) # draw current character set # charSetNumberString = f"({self.charMapNumber+1}/{len(self.fullCharMap)})" if self.appState.colorMode == "16": # put it to the right instead of the left, to make room for BG colors self.addstr(statusBarLineNum+1, self.chMap_offset+len(self.chMapString)+2, charSetNumberString, curses.color_pair(mainColor)) #if self.appState.colorMode == 256: else: #self.addstr(statusBarLineNum+1, chMap_offset-16, charSetNumberString, curses.color_pair(mainColor)) self.addstr(statusBarLineNum+1, self.chMap_offset-8, charSetNumberString, curses.color_pair(mainColor)) #self.addstr(statusBarLineNum+1, chMap_offset+len(self.chMapString)+2, str(self.charMapNumber+1), curses.color_pair(mainColor)) # overlay draw function key names in normal color y = 0 #for x in range(1,11): # self.addstr(statusBarLineNum+1, chMap_offset+y, "F%i" % x, curses.color_pair(mainColor)) # y = y + 3 # draw 16-color picker if self.appState.colorMode == "16" and self.realmaxX > self.appState.full_ui_width: colorPickerFGOffset = 0 self.addstr(statusBarLineNum+1, colorPickerFGOffset, "FG:", curses.color_pair(mainColor)) #hiColor = 7 # for old way, using bold to get high colors hiColor = 17 # for old way, using bold to get high colors if self.appState.iceColors: hiColor = 16 for c in range(1,hiColor): cp = self.ansi.colorPairMap[(c, 0)] if c > 8: if c == self.colorfg: self.addstr(statusBarLineNum+1, colorPickerFGOffset+2+c,'X', curses.color_pair(cp) | curses.A_BOLD) # block character else: self.addstr(statusBarLineNum+1, colorPickerFGOffset+2+c, self.appState.colorPickChar, curses.color_pair(cp) | curses.A_BOLD) # block character else: if c == self.colorfg: if c == 1: # black fg self.addstr(statusBarLineNum+1, colorPickerFGOffset+2+c,'X', curses.color_pair(mainColor)) else: self.addstr(statusBarLineNum+1, colorPickerFGOffset+2+c,'X', curses.color_pair(cp)) # block character else: self.addstr(statusBarLineNum+1, colorPickerFGOffset+2+c, self.appState.colorPickChar, curses.color_pair(cp)) # bg color colorPickerBGOffset = 21 self.addstr(statusBarLineNum+1, colorPickerBGOffset, "BG:", curses.color_pair(mainColor)) hiColor = 9 # for old way, using bold to get high colors if self.appState.iceColors: hiColor = 16 for c in range(1,hiColor): cp = self.ansi.colorPairMap[(c, 0)] if c == self.colorbg: #or (c == 8 and self.colorbg == 0): #self.addstr(statusBarLineNum+1, colorPickerBGOffset+3+c, 'X', curses.color_pair(self.ansi.colorPairMap[(16, c)])) if not self.appState.iceColors: if c == 9: # black bg self.addstr(statusBarLineNum+1, colorPickerBGOffset+3+c, 'X', curses.color_pair(mainColor)) else: self.addstr(statusBarLineNum+1, colorPickerBGOffset+3+c, 'X', curses.color_pair(self.ansi.colorPairMap[(16, c)])) else: self.addstr(statusBarLineNum+1, colorPickerBGOffset+3+c, 'X', curses.color_pair(self.ansi.colorPairMap[(16, c)])) else: self.addstr(statusBarLineNum+1, colorPickerBGOffset+3+c, self.appState.colorPickChar, curses.color_pair(cp + 1)) # Draw x/y location/position locationString = "(%i,%i)" % (self.xy[1]-1, self.xy[0]) locationStringOffset = realmaxX - len(locationString) - 1 self.addstr(statusBarLineNum+1, locationStringOffset, locationString, curses.color_pair(mainColor)) # Draw button indicator if self.pressingButton: self.addstr(statusBarLineNum + 1, locationStringOffset - 1, "*", curses.color_pair(3) | curses.A_BOLD) else: self.addstr(statusBarLineNum + 1, locationStringOffset - 1, " ", curses.color_pair(3) | curses.A_BOLD) if self.realmaxX >= self.appState.full_ui_width: # Draw Range, FPS and Delay buttons self.addstr(statusBarLineNum, 13 + line_1_offset, "<", curses.color_pair(clickColor) | curses.A_BOLD) # FPS buttons self.addstr(statusBarLineNum, 17 + line_1_offset, ">", curses.color_pair(clickColor) | curses.A_BOLD) if self.appState.modified: self.addstr(statusBarLineNum + 1, realmaxX - 1, "*", curses.color_pair(4) | curses.A_BOLD) else: self.addstr(statusBarLineNum + 1, realmaxX - 1, " ", curses.color_pair(4) | curses.A_BOLD) if not self.playing: self.addstr(statusBarLineNum, 2 + line_1_offset, "F", curses.color_pair(clickColor) | curses.A_BOLD) # Frame button self.addstr(statusBarLineNum, 31 + line_1_offset, "R", curses.color_pair(clickColor) | curses.A_BOLD) # Range button self.addstr(statusBarLineNum, 23 + line_1_offset, "D", curses.color_pair(clickColor) | curses.A_BOLD) # Delay button # draw transport transportString = "|< << |> >> >|" transportOffset = realmaxX - len(transportString) - 9 self.transportOffset = transportOffset if self.appState.narrowWindow: # small window, only show |> transportOffset = realmaxX - 12 if self.playing: playButtonString = "||" else: playButtonString = "|>" self.addstr(statusBarLineNum, transportOffset, playButtonString, curses.color_pair(clickColor) | curses.A_BOLD) else: # wide window, show full transport if self.playing: transportString = "|< << || >> >|" self.addstr(statusBarLineNum, transportOffset, transportString, curses.color_pair(mainColor)) self.addstr(statusBarLineNum, transportOffset+6, "||", curses.color_pair(clickColor) | curses.A_BOLD) else: transportString = "|< << |> >> >|" self.addstr(statusBarLineNum, transportOffset, transportString, curses.color_pair(clickColor) | curses.A_BOLD) # Draw the new status bar if self.commandMode: # When we hit esc in the canvas, show tooltips etc. self.addstr(statusBarLineNum, realmaxX - 1, "*", curses.color_pair(2) | curses.A_BOLD) self.statusBar.showToolTips() else: self.statusBar.hideToolTips() self.addstr(statusBarLineNum, realmaxX - 1, " ", curses.color_pair(2) | curses.A_BOLD) # More offsets for the tooltips - for transport buttons trans_play_offset = transportOffset + 6 trans_prev_offset = transportOffset + 3 trans_next_offset = transportOffset + 10 # Update tooltip locations for free floating tooltips frameBar_tip = self.statusBar.other_tooltips.get_tip("g") frameBar_tip.set_location(row = statusBarLineNum, column = frameBar_offset) fpsBar_plus_tip = self.statusBar.other_tooltips.get_tip("-") fpsBar_plus_tip.set_location(row = statusBarLineNum, column = fpsBar_offset) fpsBar_minus_tip = self.statusBar.other_tooltips.get_tip("+") fpsBar_minus_tip.set_location(row = statusBarLineNum, column = fpsBar_minus_offset) delayBar_tip = self.statusBar.other_tooltips.get_tip("D") delayBar_tip.set_location(row = statusBarLineNum, column = delayBar_offset) rangeBar_tip = self.statusBar.other_tooltips.get_tip("R") rangeBar_tip.set_location(row = statusBarLineNum, column = rangeBar_offset) colorPicker_tip = self.statusBar.other_tooltips.get_tip("c") colorPicker_tip.set_location(row = statusBarLineNum + 1, column = 2) prevChMap_tip = self.statusBar.other_tooltips.get_tip("[") prevChMap_tip.set_location(row = statusBarLineNum + 1, column = self.chMap_offset - 1) nextChMap_tip = self.statusBar.other_tooltips.get_tip("]") nextChMap_tip.set_location(row = statusBarLineNum + 1, column = chMap_next_offset) play_tip = self.statusBar.other_tooltips.get_tip("p") play_tip.set_location(row = statusBarLineNum, column = trans_play_offset) prev_tip = self.statusBar.other_tooltips.get_tip("j") prev_tip.set_location(row = statusBarLineNum, column = trans_prev_offset) next_tip = self.statusBar.other_tooltips.get_tip("k") next_tip.set_location(row = statusBarLineNum, column = trans_next_offset) # hide un-used tool tips in narrow view if self.appState.narrowWindow: play_tip.alwaysHidden = True prev_tip.alwaysHidden = True next_tip.alwaysHidden = True else: play_tip.alwaysHidden = False prev_tip.alwaysHidden = False next_tip.alwaysHidden = False #if self.appState.colorMode == "16": # colorPicker_tip.hide() self.statusBar.draw() # if cursor is outside of canvas, fix it bottomLine = self.realmaxY - 3 + self.appState.topLine if self.xy[0] > self.mov.sizeY - 1: # cursor is past bottom of the canvas self.xy[0] = self.mov.sizeY - 1 if self.xy[1] > self.mov.sizeX: # cursor is past right edge of the canvas self.xy[1] = self.mov.sizeX # if it's off screen.. fix that, too if self.xy[0] - self.appState.topLine > realmaxY - 3: self.xy[0] = realmaxY - 3 + self.appState.topLine if self.xy[1] < 0: self.xy[1] = 1 self.move(self.xy[0], self.xy[1] - 1) # Draw fill character button with proper (preview) colors if not self.statusBar.drawCharPickerButton.hidden: drawChar_line = self.statusBar.drawCharPickerButton.realX drawChar_col = self.statusBar.drawCharPickerButton.realY + 1 self.addstr(drawChar_line, drawChar_col, self.appState.drawChar, curses.color_pair(self.colorpair)) if resized: self.refresh() self.showFileInformation() self.hardRefresh() def window_big_enough_for_colors(self): # Returns true if window is either tall enough or wide enough # to fit a palette to the right of or below the canvas returnValue = True realmaxY,realmaxX = self.realstdscr.getmaxyx() if realmaxX < self.mov.sizeX + self.appState.sideBar_minimum_width: # I'm not wide enough if realmaxY - self.appState.bottomBar_minimum_height < self.mov.sizeY: # I'm not tall enough returnValue = False # and gosh darnit, pepple like me. if realmaxY - self.appState.bottomBar_minimum_height < self.mov.sizeY: if realmaxX < self.mov.sizeX + self.appState.sideBar_minimum_width: returnValue = False #debugString = f"big enough: {returnValue}" #self.addstr(realmaxY - 3, 0, debugString, curses.color_pair(self.appState.theme['clickHighlightColor']) | curses.A_BOLD) return returnValue def clickedUndo(self): self.undo.undo() if self.appState.playbackRange[1] > self.mov.frameCount: #self.appState.playbackRange = (start, stop) self.setPlaybackRange(1, self.mov.frameCount) self.hardRefresh() def clickedRedo(self): self.undo.redo() if self.appState.playbackRange[1] > self.mov.frameCount: self.setPlaybackRange(1, self.mov.frameCount) self.hardRefresh() def clickHighlight(self, pos, buttonString, bar='top'): # Visual feedback # example: self.clickHighlight(52, "|>") # Highlight clicked item at "pos" by drawing it as "str" in bright white or yellow, sleep a moment, # then back to green if bar == 'top': y = self.statusBarLineNum if bar == 'bottom': y = self.statusBarLineNum + 1 self.addstr(y, pos, buttonString, curses.color_pair(self.appState.theme['clickHighlightColor']) | curses.A_BOLD) curses.curs_set(0) # turn off cursor self.stdscr.refresh() time.sleep(0.2) self.addstr(y, pos, buttonString, curses.color_pair(self.appState.theme['clickColor'])) curses.curs_set(1) # turn on cursor def mainLoop(self): self.metaKey = 0 self.commandMode = False cursorMode = self.appState.cursorMode mouseX, mouseY = 0, 0 self.pressingButton = False self.drawStatusBar() # to make sure the inital state looks correct mouseState = None intense_burning = ['idkfa'] sumbitch = [] sumbitch_len = 25 curses.noecho() while self.appState.editorRunning: # Real "main loop" - get user input, aka "edit mode" self.testWindowSize() # print statusbar stuff self.drawStatusBar() self.move(self.xy[0], self.xy[1] - 1) # move cursor to the right # spot for refresh curses.panel.update_panels() self.stdscr.refresh() c = self.stdscr.getch() # non-wide characters, for old ncurses #c = ord(self.stdscr.get_wch()) # wide characters, for ncursesw self.testWindowSize() #if c in ["\x1b\x1b\x5b\x42"]: self.notify("alt-down") if self.metaKey == 1: self.pressingButton = False if self.pushingToClip: self.pushingToClip = False if cursorMode != "Draw" and cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if c == 111: # alt-o - open self.openFromMenu() # as if we clicked menu->open elif c == 115: # alt-s - save self.save() c = None elif c == 113: # alt-q - quit if self.appState.durview_running: self.appState.editorRunning = False c = None #return else: self.safeQuit() elif c in [104, 63]: # alt-h - help self.showHelp() c = None #elif c in [98, curses.KEY_LEFT]: # alt-left - prev bg color elif c in [curses.KEY_LEFT]: # alt-left - prev bg color (for 16) if self.appState.colorMode == "16": self.prevBgColor() elif self.appState.colorMode == "256": self.prevFgColor() c = None #elif c in [102, curses.KEY_RIGHT]: # alt-right - next bg color (for 16) elif c in [curses.KEY_RIGHT]: # alt-right - next fg color if self.appState.colorMode == "16": self.nextBgColor() elif self.appState.colorMode == "256": self.nextFgColor() c = None elif c in [curses.KEY_DOWN, "\x1b\x1b\x5b\x42"]: # alt-down - prev bg color if self.appState.colorMode == "16": self.prevFgColor() elif self.appState.colorMode == "256": self.statusBar.colorPicker.handler.move_down_256() c = None elif c == curses.KEY_UP: # alt-up - next bg color if self.appState.colorMode == "16": self.nextFgColor() elif self.appState.colorMode == "256": self.statusBar.colorPicker.handler.move_up_256() c = None elif c == 91 or c == 339: # alt-[ (91) or alt-pgup (339). previous character set self.prevCharSet() elif c == 93 or c == 338: # alt-] or alt-pgdown, next character set self.nextCharSet() elif c == 83: # alt-S - pick a character set or unicode block self.showCharSetPicker() elif c == 44: # alt-, - erase/pop current column in frame self.delCol() elif c == 46: # alt-. - insert column in frame self.addCol() elif c == 62: # alt-> - insert column in canvas self.addColToCanvas() elif c == 60: # alt-< - delete column from canvas self.delColFromCanvas() elif c == 34: # alt-" - insert line in canvas self.addLineToCanvas() elif c == 58: # alt-: - erase line from canvas self.delLineFromCanvas() elif c == 39: # alt-' - insert line self.addLine() elif c == 59: # alt-; - erase line self.delLine() elif c == 121 or c == ord('u'): # alt-y - Eyedrop, alt-u is what Aciddraw used self.eyeDrop(self.xy[1] - 1, self.xy[0]) # cursor position elif c == ord('P'): # alt-P - pick up charcter self.pickUpDrawingChar(self.xy[1] - 1, self.xy[0]) #self.notify(f"Picked up character: {self.appState.drawChar}") elif c == ord(' '): # alt-space - insert drawing character drawChar = self.appState.drawChar x_param = self.xy[1] y_param = self.xy[0] self.insertChar(ord(drawChar), fg=self.colorfg, bg=self.colorbg, x=x_param, y=y_param, moveCursor=True, pushUndo=True) elif c == ord('l'): # alt-l - color under cursor self.insertColor(fg=self.colorfg, bg=self.colorbg, pushUndo=True) elif c == ord('L'): # alt-L - search and replace color self.replaceColorUnderCursor() elif c == 73: # alt-I - Character Inspector self.showCharInspector() elif c == 105: # alt-i - File/Canvas Information self.clickedInfoButton() #if self.appState.sideBarShowing: # self.toggleShowFileInformation() #else: # self.showFileInformation(notify=True) elif c == 109 or c == 102: # alt-m or alt-f - load menu self.commandMode = False self.openMenu("File") elif c == ord('a'): # alt-a - Animation menu self.commandMode = False self.openMenu("Anim") elif c == 116: # or c =- 84: # alt-t or alt-T - mouse tools menu self.commandMode = False self.openMenu("Mouse Tools") #self.statusBar.toolButton.on_click() elif c == 99: # alt-c - color picker self.commandMode = False #if self.appState.colorMode == "256": #if self.appState.sideBarShowing: # self.statusBar.colorPicker.switchTo() #else: # self.statusBar.colorPickerButton.on_click() #self.selectColorPicker() self.statusBar.colorPickerButton.on_click() # Animation Keystrokes elif c == 68: #alt-D - set delay for current frame self.getDelayValue() elif c == 107: # alt-k - next frame self.mov.nextFrame() self.refresh() elif c == 106: # alt-j or alt-j - previous frame self.mov.prevFrame() self.refresh() elif c == 103: # alt-g - go to frame self.gotoFrameGetInput() elif c == 67: # alt-C - clear canvas/new self.clearCanvas(prompting=True) elif c == 110 or c == 14: # alt-n - clone to new frame self.cloneToNewFrame() elif c == 78: # alt-N (shift-alt-n) - new empty frame self.appendEmptyFrame() elif c == 77: # alt-M - move current frame self.moveCurrentFrame() elif c == 100: # alt-d - delete current frame self.deleteCurrentFrame() elif c == 125: # alt-} - shift frames right self.shiftMovieRight() elif c == 123: # alt-{ - shift frames left self.shiftMovieLeft() elif c == 122: # alt-z = undo self.clickedUndo() elif c == 114: # alt-r = redo self.clickedRedo() elif c == ord('F'): # alt-F, find/search self.searchForStringPrompt() elif c == 118: # alt-v - paste # Paste from the clipboard if self.clipBoard: # If there is something in the clipboard self.askHowToPaste() #self.pasteFromClipboard() elif c == ord('V'): # alt-V, View mode self.enterViewMode() elif c == 82: # alt-R = set playback range self.getPlaybackRange() elif c == 112: # esc-p - start playing, any key exits self.commandMode = False self.metaKey = 0 self.startPlaying() elif c in [61, 43]: # esc-= and esc-+ - fps up self.increaseFPS() elif c in [45]: # esc-- (alt minus) - fps down self.decreaseFPS() elif c in [75]: # alt-K = start marking selection startPoint=(self.xy[0] + self.appState.topLine, self.xy[1] + self.appState.firstCol) self.startSelecting(firstkey=c) # start selecting text elif c in [ord('1')]: # esc-1 copy of F1 - insert extended character self.insertChar(self.chMap['f1'], fg=self.colorfg, bg=self.colorbg) #self.hardRefresh() self.stdscr.refresh() c = None elif c in [ord('2')]: # esc-2 copy of F2 - insert extended character self.insertChar(self.chMap['f2'], fg=self.colorfg, bg=self.colorbg) #self.hardRefresh() #self.refresh() #self.refresh(refreshScreen=False) self.stdscr.refresh() c = None elif c in [ord('3')]: # F3 - insert extended character self.insertChar(self.chMap['f3'], fg=self.colorfg, bg=self.colorbg) #self.hardRefresh() self.stdscr.refresh() c = None elif c in [ord('4')]: # F4 - insert extended character self.insertChar(self.chMap['f4'], fg=self.colorfg, bg=self.colorbg) #self.refresh() self.stdscr.refresh() c = None elif c in [ord('5')]: # F5 - insert extended character self.insertChar(self.chMap['f5'], fg=self.colorfg, bg=self.colorbg) #self.hardRefresh() self.stdscr.refresh() c = None elif c in [ord('6')]: # F6 - insert extended character self.insertChar(self.chMap['f6'], fg=self.colorfg, bg=self.colorbg) #self.hardRefresh() self.stdscr.refresh() c = None elif c in [ord('7')]: # F7 - insert extended character self.insertChar(self.chMap['f7'], fg=self.colorfg, bg=self.colorbg) #self.hardRefresh() self.stdscr.refresh() c = None elif c in [ord('8')]: # F8 - insert extended character self.insertChar(self.chMap['f8'], fg=self.colorfg, bg=self.colorbg) #self.hardRefresh() self.stdscr.refresh() c = None elif c in [ord('9')]: # F9 - insert extended character self.insertChar(self.chMap['f9'], fg=self.colorfg, bg=self.colorbg) #self.hardRefresh() self.stdscr.refresh() c = None elif c in [ord('0')]: # F10 - insert extended character self.insertChar(self.chMap['f10'], fg=self.colorfg, bg=self.colorbg) #self.hardRefresh() self.stdscr.refresh() c = None elif c == 27: # 2nd esc byte - possibly alt-arrow. # eg: alt-down: 27 27 91 66 or \x1b\x1b\x5b\x42 c = self.stdscr.getch() if c == 91: # 3rd byte (\x5b) in arrow key sequence c = self.stdscr.getch() if c == 65: # real alt-up, not esc-up if self.appState.colorMode == "16": self.nextFgColor() elif self.appState.colorMode == "256": self.statusBar.colorPicker.handler.move_up_256() c = None elif c == 66: # real alt-down, not esc-down if self.appState.colorMode == "16": self.prevFgColor() elif self.appState.colorMode == "256": self.statusBar.colorPicker.handler.move_down_256() c = None elif c == 67: # real alt-right, not esc-right if self.appState.colorMode == "16": self.nextBgColor() elif self.appState.colorMode == "256": self.nextFgColor() c = None elif c == 68: # real alt-left, not esc-left if self.appState.colorMode == "16": self.prevBgColor() elif self.appState.colorMode == "256": self.prevFgColor() c = None self.pressingButton = False if cursorMode != "Draw" and cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if self.pushingToClip: self.pushingToClip = False else: self.pressingButton = False if cursorMode != "Draw" and cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if self.pushingToClip: self.pushingToClip = False if self.appState.debug: if c == ord('X'): # esc-X - drop into pdb debugger self.jumpToPythonConsole() else: self.notify("keystroke: %d" % c) # alt-unknown self.commandMode = False self.metaKey = 0 c = None # Meta key (alt) was not pressed, so look for non-meta chars if c == 27: self.metaKey = 1 self.commandMode = True self.pressingButton = False if self.pushingToClip: self.pushingToClip = False if cursorMode != "Draw" and cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) self.appState.renderMouseCursor = False c = None if c == 24: self.safeQuit() # ctrl-x elif c == 15: # ctrl-o - open # holy crap, this is still in here? lol self.openFromMenu() # as if we clicked menu->open c = None elif c == 23: # ctrl-w - save self.save() c = None elif c == 12: # ctrl-l - harder refresh self.stdscr.clear() self.hardRefresh() c = None elif c in [10, 13, curses.KEY_ENTER]: # enter (10 if we self.move_cursor_enter() elif c in [263, 127]: # backspace self.backspace() elif c in [330]: # delete self.deleteKeyPop() elif c in [383]: # shift-delete - delete from opposite direction self.reverseDelete() elif c in [9, 353]: # 9 = tab, 353 = shift-tab #if self.appState.colorMode == "256": #self.statusBar.colorPickerButton.on_click() #self.selectColorPicker() self.selectColorPicker() elif c in [339, curses.KEY_PPAGE]: # page up self.move_cursor_pgup() elif c in [338, curses.KEY_NPAGE]: # page down self.move_cursor_pgdown() elif c in [1, curses.KEY_HOME]: # ctrl-a or home self.move_cursor_home() elif c in [5, curses.KEY_END]: # ctrl-e or end self.move_cursor_end() elif c in [curses.KEY_F1]: # F1 - insert extended character self.insertChar(self.chMap['f1'], fg=self.colorfg, bg=self.colorbg) c = None elif c in [curses.KEY_F2]: # F2 - insert extended character self.insertChar(self.chMap['f2'], fg=self.colorfg, bg=self.colorbg) c = None elif c in [curses.KEY_F3]: # F3 - insert extended character self.insertChar(self.chMap['f3'], fg=self.colorfg, bg=self.colorbg) c = None elif c in [curses.KEY_F4]: # F4 - insert extended character self.insertChar(self.chMap['f4'], fg=self.colorfg, bg=self.colorbg) c = None elif c in [curses.KEY_F5]: # F5 - insert extended character self.insertChar(self.chMap['f5'], fg=self.colorfg, bg=self.colorbg) c = None elif c in [curses.KEY_F6]: # F6 - insert extended character self.insertChar(self.chMap['f6'], fg=self.colorfg, bg=self.colorbg) c = None elif c in [curses.KEY_F7]: # F7 - insert extended character self.insertChar(self.chMap['f7'], fg=self.colorfg, bg=self.colorbg) c = None elif c in [curses.KEY_F8]: # F8 - insert extended character self.insertChar(self.chMap['f8'], fg=self.colorfg, bg=self.colorbg) c = None elif c in [curses.KEY_F9]: # F9 - insert extended character self.insertChar(self.chMap['f9'], fg=self.colorfg, bg=self.colorbg) c = None elif c in [curses.KEY_F10]: # F10 - insert extended character self.insertChar(self.chMap['f10'], fg=self.colorfg, bg=self.colorbg) c = None elif c == curses.KEY_LEFT: # left - move cursor right a character self.move_cursor_left() elif c == curses.KEY_RIGHT: # right - move cursor right self.move_cursor_right() elif c == curses.KEY_UP: # up - move cursor up self.move_cursor_up() elif c == curses.KEY_DOWN: # down - move curosr down self.move_cursor_down() elif c in [339, curses.KEY_HOME]: # 339 = home. not in xterm :'( self.move_cursor_home() elif c in [338, curses.KEY_END]: # 338 = end self.move_cursor_end() elif c != curses.KEY_MOUSE and self.pressingButton: self.pressingButton = False if self.pushingToClip: self.pushingToClip = False cursorMode = self.appState.cursorMode if cursorMode != "Draw" and cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) elif c == curses.KEY_MOUSE: # We are not playing try: _, mouseX, mouseY, _, mouseState = curses.getmouse() except: mouseState = 0 b1_press = mouseState & curses.BUTTON1_PRESSED b1_release = mouseState & curses.BUTTON1_RELEASED b1_click = mouseState & curses.BUTTON1_CLICKED b1_dclick = mouseState & curses.BUTTON1_DOUBLE_CLICKED self.appState.mouse_col = mouseX self.appState.mouse_line = mouseY self.appState.renderMouseCursor = True if mouseState > 2: # probably click-dragging. We want an instant response, so... pass #self.pressingButton = True #print('\033[?1003h') # enable mouse tracking with the XTERM API #print('\033[?1003l') # disable mouse reporting #curses.mousemask(1) #curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if mouseState == 1: self.pressingButton = False if self.pushingToClip: self.pushingToClip = False cursorMode = self.appState.cursorMode if cursorMode != "Draw" and cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if self.pushingToClip: self.pushingToClip = False #self.notify("Released from drag, hopefully.") if self.appState.debug: # clear mouse state lines winHeight,winWidth = self.realstdscr.getmaxyx() blank_line = " " * winWidth self.addstr(self.statusBarLineNum-3, 0, blank_line, curses.color_pair(3) | curses.A_BOLD) self.addstr(self.statusBarLineNum-4, 0, blank_line, curses.color_pair(3) | curses.A_BOLD) # print mouse state mouseDebugString = f"mX: {mouseX}, mY: {mouseY}, mState: {mouseState}, press:{b1_press} rel:{b1_release} clk:{b1_click} dclk: {b1_dclick}" self.addstr(self.statusBarLineNum-4, 0, mouseDebugString, curses.color_pair(3) | curses.A_BOLD) #self.addstr(self.statusBarLineNum-5, 0, mouseDebugStates, curses.color_pair(2) | curses.A_BOLD #except: # pass #if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX \ # and mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum: if mouseY + self.appState.topLine < self.mov.sizeY and mouseX + self.appState.firstCol < self.mov.sizeX: # we're in the canvas, not playing if mouseState & curses.BUTTON1_PRESSED: if not self.pressingButton: self.pressingButton = True print('\033[?1003h') # enable mouse tracking with the XTERM API self.hardRefresh() if not self.pushingToClip: cmode = self.appState.cursorMode if cmode == "Draw" or cmode == "Paint" or cmode == "Color" or cmode == "Erase": self.undo.push() self.pushingToClip = True elif mouseState & curses.BUTTON1_RELEASED: if self.pressingButton: self.pressingButton = False if self.pushingToClip: self.pushingToClip = False cursorMode = self.appState.cursorMode if cursorMode != "Draw" and cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if cursorMode == "Draw" and cursorMode == "Paint": self.enableMouseReporting() if self.pushingToClip: self.pushingToClip = False self.refresh() #self.stdscr.redrawwin() if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 try: curses.BUTTON5_PRESSED except: curses.BUTTON5_PRESSED = 0 try: curses.BUTTON4_PRESSED except: curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up if self.appState.scrollColors: self.nextFgColor() else: self.move_cursor_up() elif mouseState & curses.BUTTON5_PRESSED: # wheel down if self.appState.scrollColors: self.prevFgColor() else: self.move_cursor_down() elif self.appState.cursorMode == "Move": # select mode/move the cursor if self.pressingButton: self.xy[1] = mouseX + 1 + self.appState.firstCol # set cursor position self.xy[0] = mouseY + self.appState.topLine elif self.appState.cursorMode == "Draw": if self.pressingButton: if self.appState.debug: self.addstr(self.statusBarLineNum-2, 20, "Draw triggered.", curses.color_pair(6) | curses.A_BOLD) #self.notify("Draw triggered.") # also set cursor position - not anymore. #self.xy[1] = mouseX + 1 # set cursor position #self.xy[0] = mouseY + self.appState.topLine # Insert the selected character. drawChar = self.appState.drawChar try: x_param = mouseX + 1 + self.appState.firstCol y_param = mouseY + self.appState.topLine self.insertChar(ord(drawChar), fg=self.colorfg, bg=self.colorbg, x=x_param, y=y_param, moveCursor=False, pushUndo=False) except IndexError: self.notify(f"Error, debug info: x={x_param}, y={y_param}, topLine={self.appState.topLine}, mouseX={mouseX}, mouseY={mouseY}", pause=True) #self.refresh() else: if self.appState.debug: self.addstr(self.statusBarLineNum-2, 20, "Draw untriggered.", curses.color_pair(5) | curses.A_BOLD) elif self.appState.cursorMode == "Paint": # Draw Brush under cursor if self.pressingButton: if self.appState.debug: self.addstr(self.statusBarLineNum-2, 20, "Paint triggered.", curses.color_pair(6) | curses.A_BOLD) # Paint brush onto canvas #drawChar = self.appState.drawChar painting = True if not self.appState.brush: # if no brush is set... painting = False # don't paint. if painting: if self.appState.debug: self.addstr(self.statusBarLineNum-2, 20, "Paint painting.", curses.color_pair(6) | curses.A_BOLD) try: x_param = mouseX + 1 + self.appState.firstCol y_param = mouseY + self.appState.topLine #self.insertChar(ord(drawChar), fg=self.colorfg, bg=self.colorbg, x=x_param, y=y_param, moveCursor=False, pushUndo=False) self.pasteFromClipboard(startPoint = [y_param, x_param], clipBuffer=self.appState.brush, transparent=True, pushUndo=False) except IndexError: self.notify(f"Error, debug info: x={x_param}, y={y_param}, topLine={self.appState.topLine}, mouseX={mouseX}, mouseY={mouseY}", pause=True) self.refresh() else: if self.appState.debug: self.addstr(self.statusBarLineNum-2, 20, "Paint untriggered.", curses.color_pair(5) | curses.A_BOLD) elif self.appState.cursorMode == "Color": # Change the color under the cursor if self.pressingButton: # also set cursor position self.xy[1] = mouseX + 1 + self.appState.firstCol # set cursor position self.xy[0] = mouseY + self.appState.topLine self.insertColor(fg=self.colorfg, bg=self.colorbg, x=mouseX+1 + self.appState.firstCol, y=mouseY + self.appState.topLine, pushUndo=False) self.refresh() elif self.appState.cursorMode == "Erase": # Erase character under the cursor # also set cursor position if self.pressingButton: self.xy[1] = mouseX + 1 + self.appState.firstCol# set cursor position self.xy[0] = mouseY + self.appState.topLine color_fg = self.appState.defaultFgColor color_bg = self.appState.defaultBgColor self.insertChar(ord(' '), fg=color_fg, bg=color_bg, x=mouseX + self.appState.firstCol, y=mouseY + self.appState.topLine, pushUndo=False) elif self.appState.cursorMode == "Eyedrop": # Change the color under the cursor self.eyeDrop(mouseX + self.appState.firstCol, mouseY + self.appState.topLine) self.statusBar.setCursorModeMove() self.drawStatusBar() elif self.appState.cursorMode == "Select": # this does not exist lol self.xy[1] = mouseX + 1 + self.appState.firstCol # set cursor position self.xy[0] = mouseY + self.appState.topLine self.startSelecting(mouse=True) # else, not in the canvas elif self.pressingButton: self.pressingButton = False if self.pushingToClip: self.pushingToClip = False if self.appState.cursorMode != "Draw" and self.appState.cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) #self.mov.currentFrame.newColorMap[self.xy[0]][self.xy[1] - 1] = [self.colorfg, self.colorbg] if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 try: curses.BUTTON5_PRESSED except: curses.BUTTON5_PRESSED = 0 try: curses.BUTTON4_PRESSED except: curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON1_PRESSED or mouseState & curses.BUTTON4_PRESSED or mouseState & curses.BUTTON5_PRESSED or b1_press > 0 or mouseState == curses.BUTTON1_DOUBLE_CLICKED: #print('\033[?1003h') #self.notify("Farfenugen") realmaxY,realmaxX = self.realstdscr.getmaxyx() cmode = self.appState.cursorMode #if mouseState & curses.BUTTON1_PRESSED: # self.gui.got_click("Click", mouseX, mouseY) # If we clicked in the status bar: if mouseX < realmaxX and mouseY in [self.statusBarLineNum, self.statusBarLineNum+1]: # we clicked on the status bar somewhere.. if mouseState & curses.BUTTON1_PRESSED: #if cmode == "Draw" or cmode == "Paint" or cmode == "Color": self.disableMouseReporting() self.gui.got_click("Click", mouseX, mouseY) if cmode == "Draw" or cmode == "Paint": self.enableMouseReporting() # Add stuff here to take mouse 'commands' like clicking # play/next/etc on transport, or clicking "start button" if mouseY == self.statusBarLineNum: # clicked upper bar offset = self.line_1_offset # line 1 of the status bar tOffset = self.transportOffset if not self.appState.narrowWindow: if mouseX in [tOffset + 6, tOffset + 7]: # clicked play button self.clickHighlight(tOffset + 6, "|>") self.startPlaying() self.metaKey = 0 elif mouseX in [tOffset + 3, tOffset + 4]: # goto prev frame self.clickHighlight(tOffset + 3, "<<") self.mov.prevFrame() elif mouseX in [tOffset + 9, tOffset + 10]: # goto next frame self.clickHighlight(tOffset + 9, ">>") self.mov.nextFrame() elif mouseX in [tOffset, tOffset + 1]: # goto first frame self.clickHighlight(tOffset, "|<") self.mov.gotoFrame(1) elif mouseX in [tOffset + 12, tOffset + 13]: # goto last frame self.clickHighlight(tOffset + 12, ">|") self.mov.nextFrame() self.mov.gotoFrame(self.mov.frameCount) if mouseX == 13 + offset: # clicked FPS down self.clickHighlight(13 + offset, "<") self.decreaseFPS() elif mouseX == 17 + offset: # clicked FPS up self.clickHighlight(17 + offset, ">") self.increaseFPS() elif mouseX == 23 + offset: # clicked Delay button self.clickHighlight(23 + offset, "D") self.getDelayValue() elif mouseX == 31 + offset: # clicked Range button self.clickHighlight(31 + offset, "R") self.getPlaybackRange() elif mouseX == 2 + offset: # clicked Frame button self.clickHighlight(2 + offset, "F") self.gotoFrameGetInput() elif mouseY == self.statusBarLineNum+1: # clicked bottom bar if self.appState.colorMode == "16" and self.realmaxX >= self.appState.full_ui_width: if mouseX in range(3,19): # clicked a fg color fg = mouseX - 2 self.setFgColor(fg) elif mouseX in range(25,33): # clicked a bg color bg = mouseX - 24 self.setBgColor(bg) char_area_start = self.chMap_offset char_area_end = self.chMap_offset+len(self.chMapString) if mouseX == self.chMap_offset + len(self.chMapString): # clicked next character set self.clickHighlight(self.chMap_offset + len(self.chMapString), ">", bar='bottom') self.nextCharSet() elif mouseX == self.chMap_offset - 1: # clicked previous character set self.clickHighlight(self.chMap_offset - 1, "<", bar='bottom') self.prevCharSet() elif mouseX in range(char_area_start, char_area_end): self.clickedChMap(mouseX, mouseY) elif self.appState.debug: self.notify("bottom bar. " + str([mouseX, mouseY])) else: if self.appState.debug: self.notify(str([mouseX, mouseY])) # If we clicked in the sidebar area, aka to the right of the canvas # and above the status bar: if self.appState.sideBarEnabled: # If we're in the right toolbar sort of area if mouseX >= self.appState.sideBarColumn and mouseY < self.statusBarLineNum: #if self.appState.colorMode == "256": # Tell the color picker to respond if the click is in its area: # self.statusBar.colorPicker.handler.gotClick(mouseX, mouseY) if mouseState == curses.BUTTON1_DOUBLE_CLICKED: if self.appState.colorMode == "16": # set BG color self.statusBar.colorPicker.handler.gotDoubleClick(mouseX, mouseY) else: self.statusBar.colorPicker.handler.gotClick(mouseX, mouseY) #elif mouseState & curses.BUTTON1_RELEASED: # pass #print('\033[?1003l') #curses.mousemask(1) #curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if mouseState == curses.BUTTON1_CLICKED: self.pressingButton = False if cursorMode != "Draw" and cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if self.pushingToClip: self.pushingToClip = False realmaxY,realmaxX = self.realstdscr.getmaxyx() cmode = self.appState.cursorMode #if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX \ if mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum and mouseX + self.appState.firstCol < self.mov.sizeX: # we're in the canvas if cmode == "Draw" or cmode == "Color" or cmode == "Erase" or cmode == "Paint": self.undo.push() else: # Not in the canvas, so give the GUI a click #pass if cmode == "Draw" or cmode == "Paint": self.disableMouseReporting() self.gui.got_click("Click", mouseX, mouseY) if cmode == "Draw" or cmode == "Paint": self.enableMouseReporting() # If we clicked in the sidebar area, aka to the right of the canvas # and above the status bar: if self.appState.sideBarEnabled: if mouseX >= self.appState.sideBarColumn and mouseY < self.statusBarLineNum: #if self.appState.colorMode == "256": # Tell the color picker to respond if the click is in its area: self.statusBar.colorPicker.handler.gotClick(mouseX, mouseY) elif c in [curses.KEY_SLEFT, curses.KEY_SRIGHT, 337, 336, 520, 513]: # 337 and 520 - shift-up, 336 and 513 = shift-down # shift-up, shift-down, shift-left and shift-right = start selecting text block # shift-up and shift-down not defined in ncurses :( # doesn't seem to work in screen? startPoint=(self.xy[0] + self.appState.topLine, self.xy[1] + self.appState.firstCol) self.startSelecting(firstkey=c) # start selecting text # pass c to something here that starts selecting and moves # the cursor based on c, and knows whether it's selecting # via shift-arrow or mouse. elif c == None: pass elif c <= 128 and c >= 32: # printable ASCII character self.insertChar(c, fg=self.colorfg, bg=self.colorbg) sumbitch.append(chr(c)) # cheat codes if len(sumbitch) >= sumbitch_len: sumbitch.pop(0) for heat in intense_burning: if ''.join(sumbitch).endswith(heat): self.activate_heat_code() self.appState.renderMouseCursor = False #else: # self.notify(f"Weird character: {chr(c)} or {c}") #self.drawStatusBar() if self.appState.viewModeShowInfo: self.showFileInformation() self.refresh(refreshScreen=False) #self.hardRefresh() def selectColorPicker(self, message=None): #if self.appState.colorMode == "256": self.appState.colorPickerSelected = True #self.statusBar.colorPicker.handler.showColorPicker() color = self.statusBar.colorPicker.showFgPicker(message=message) #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.showFgPicker() self.appState.colorPickerSelected = False return color def replaceColorUnderCursor(self): self.commandMode = False #self.notify("Pick a new color") # Search and replace color under cursor # Save old (UI) color setting, so we can use the color picker and then set the color back when done ui_fg = self.colorfg ui_bg = self.colorbg #charColor = [self.colorfg, self.colorbg] #old_color_pair = # Get color pair under cursor #old_fg, new_bg = self.mov.currentFrame.newColorMap[line][col] old_fg, old_bg = self.mov.currentFrame.newColorMap[self.xy[0]][self.xy[1]-1] oldCharColor = [old_fg, old_bg] # Print a message for the user to set the New color self.clearStatusLine() printMessage = "Please pick the New color." self.addstr(self.statusBarLineNum, 0, printMessage, curses.color_pair(self.appState.theme['notificationColor'])) self.stdscr.refresh() # Use color picker to pick new destination color (pair) #self.selectColorPicker(message=printMessage) picker_color = self.selectColorPicker() # picker_color is False if user hits Esc in color picker. self.clearStatusLine() if picker_color == False: self.notify("Replace color canceled.") return False new_fg = self.colorfg new_bg = self.colorbg newCharColor = [new_fg, new_bg] # Use movie search and replace color function askingAboutRange = False if self.mov.hasMultipleFrames(): self.promptPrint("Apply to all frames in playback range (Y/N)? ") askingAboutRange = True else: # only 1 frame in movie, so just apply to akk without asking self.undo.push() self.mov.search_and_replace_color_pair(oldCharColor, newCharColor) askingAboutRange = False while askingAboutRange: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['y', 'Y']: # yes, all in range self.undo.push() self.mov.search_and_replace_color_pair(oldCharColor, newCharColor, frange=self.appState.playbackRange) askingAboutRange = False if chr(prompt_ch) in ['n', 'N']: # No, only current frame self.undo.push() self.mov.search_and_replace_color_pair(oldCharColor, newCharColor) askingAboutRange = False elif prompt_ch == 27: # esc, cancel askingAboutRange = False #self.notify(f"Replaced {oldCharColor} with {newCharColor}") # Set UI color back to what it should be self.setFgColor(ui_fg) self.setBgColor(ui_bg) self.stdscr.refresh() def cloneToNewFrame(self): """ Take current frame, clone it to a new one, insert it immediately after current frame """ self.undo.push() if self.mov.insertCloneFrame(): if self.appState.playbackRange[0] == 1 and \ self.appState.playbackRange[1] == self.mov.frameCount - 1: self.appState.playbackRange = (self.appState.playbackRange[0], \ self.appState.playbackRange[1] + 1) self.refresh() def appendEmptyFrame(self): self.undo.push() if self.mov.addEmptyFrame(): if self.appState.playbackRange[0] == 1 and \ self.appState.playbackRange[1] == self.mov.frameCount - 1: self.appState.playbackRange = (self.appState.playbackRange[0], \ self.appState.playbackRange[1] + 1) self.refresh() def deleteCurrentFrame(self): if self.deleteCurrentFramePrompt(): if self.appState.playbackRange[0] == 1 and \ self.appState.playbackRange[1] == self.mov.frameCount + 1: self.appState.playbackRange = (self.appState.playbackRange[0], \ self.appState.playbackRange[1] - 1) self.refresh() def shiftMovieRight(self): """ Shift all frames to the right, wrapping back around """ if self.mov.shift_right(): self.mov.nextFrame() self.mov.prevFrame() #self.notify("Shifted frames to the right.") self.refresh() def shiftMovieLeft(self): """ Shift all frames to the left, wrapping back around """ if self.mov.shift_left(): self.mov.nextFrame() self.mov.prevFrame() #self.notify("Shifted frames to the right.") self.refresh() def enterViewMode(self, mov=None, opts=None): self.statusBar.hide() self.stdscr.clear() old_xy = self.xy old_top_line = self.appState.topLine old_first_col = self.appState.firstCol self.appState.topLine = 0 oldDrawBorders = self.appState.drawBorders # to turn back on when done self.appState.playOnlyMode = True self.startPlaying(mov=mov, opts=opts) # Return to normal when done self.appState.playOnlyMode = False self.appState.topLine = old_top_line self.appState.firstCol = old_first_col self.xy = old_xy self.statusBar.show() self.appState.drawBorders = oldDrawBorders self.cursorOn() self.stdscr.clear() def move_cursor_enter(self): # move the cursor down if self.xy[0] == self.mov.sizeY - 1: self.addLineToCanvas() if self.xy[0] < self.mov.sizeY: self.move_cursor_down() self.move_cursor_home() self.appState.renderMouseCursor = False def move_cursor_pgup(self): # if we're at the top of the screen if self.xy[0] == 0: # already on first line if self.xy[1] > 0: # not on first column.. self.xy[1] = 1 # so go there # top of screen, but not top of file, go up a page elif self.xy[0] == self.appState.topLine: pageSize = self.realmaxY - 2 self.xy[0] -= pageSize self.appState.topLine -= pageSize if self.xy[0] < 0: # if we overflowed into negatives... self.xy[0] = 0 self.appState.topLine = 0 # not at top of screen at all, go to top of screen elif self.xy[0] > self.appState.topLine: self.xy[0] = self.appState.topLine self.appState.renderMouseCursor = False def move_cursor_pgdown(self): bottomLine = self.realmaxY - 3 + self.appState.topLine if bottomLine > self.mov.sizeY: bottomLine = self.mov.sizeY - 1 # We're already on the last line if self.xy[0] == self.mov.sizeY - 1: if self.xy[1] > 0: # not on first column.. self.xy[1] = 1 # so go there # bottom of screen, not bottom of file, go down a page elif self.xy[0] == bottomLine: pageSize = self.realmaxY - 2 self.xy[0] += pageSize self.appState.topLine += pageSize if self.xy[0] > self.mov.sizeY - 1: # if we overshot the end of the file.. self.xy[0] = self.mov.sizeY - 1 self.appState.topLine = self.mov.sizeY - pageSize # middle of screen, go to bottom elif self.xy[0] - self.appState.topLine < bottomLine: self.xy[0] = bottomLine if self.xy[1] < self.appState.firstCol + 1:# Scrolled off screen ot the left, Need to scroll left self.appState.firstCol = self.xy[1] - 1 self.appState.renderMouseCursor = False def move_cursor_topleft(self): self.appState.topLine = 0 self.appState.firstCol = 0 self.xy[0] = 0 self.xy[1] = 1 self.refresh() self.appState.renderMouseCursor = False def move_cursor_left(self): # pressed LEFT key if self.xy[1] > 1: self.xy[1] = self.xy[1] - 1 if self.xy[1] < self.appState.firstCol + 1:# Scrolled off screen ot the left, Need to scroll left self.appState.firstCol = self.xy[1] - 1 self.appState.renderMouseCursor = False def move_cursor_right(self): # pressed RIGHT key if self.xy[1] < self.mov.sizeX: self.xy[1] = self.xy[1] + 1 #lastCol = min(mov.sizeX, self.appState.realmaxX) if self.xy[1] - self.appState.firstCol > self.appState.realmaxX: # scrolled off screen to the right, need to scroll right self.appState.firstCol = self.xy[1] - self.appState.realmaxX self.appState.renderMouseCursor = False def scroll_viewer_left(self): # pressed LEFT key in viewer mode if self.appState.firstCol > 0: self.appState.firstCol = self.appState.firstCol - 1 def scroll_viewer_right(self): # if we are already scrolled right last_column_shown = self.appState.firstCol + self.appState.realmaxX if last_column_shown > self.mov.sizeX: pass # do nothing else: # otherwise, scroll right one self.appState.firstCol += 1 def move_cursor_up(self): # pressed UP key if self.xy[0] > 0: self.xy[0] = self.xy[0] - 1 if self.xy[0] - self.appState.topLine + 1 == 0 and self.appState.topLine > 0: # if we're at the top of the screen self.appState.topLine -= 1 # scrolll up a line self.appState.renderMouseCursor = False def move_cursor_down(self): # pressed DOWN key if self.xy[0] < self.mov.sizeY - 1: self.xy[0] = self.xy[0] + 1 # down a line if self.xy[0] - self.appState.topLine > self.realmaxY - 3: # if we're at the bottom of the screen self.appState.topLine += 1 # scroll down a line self.appState.renderMouseCursor = False def move_cursor_home(self): self.xy[1] = 1 self.appState.firstCol = 0 if self.xy[1] < self.appState.firstCol + 1: # Scrolled off screen ot the left, Need to scroll left self.appState.firstCol = self.xy[1] - 1 self.appState.renderMouseCursor = False def move_cursor_end(self): self.xy[1] = self.mov.sizeX if self.xy[1] > self.appState.realmaxX: # scrolled off screen to the right, need to scroll right self.appState.firstCol = self.xy[1] - self.appState.realmaxX self.appState.renderMouseCursor = False def move_cursor_to_line_and_column(self, line, col): self.appState.topLine = line self.xy[0] = line self.xy[1] = col self.appState.renderMouseCursor = False def getDelayValue(self): """ Ask the user for the delay value to set for current frame, then set it """ self.clearStatusLine() self.stdscr.nodelay(0) # wait for input when calling getch self.promptPrint(f"Current frame delay == {self.mov.currentFrame.delay}, new value in seconds: ") curses.echo() try: delayValue = float(self.stdscr.getstr()) except ValueError: delayValue = -1 curses.noecho() self.clearStatusLine() if delayValue >= 0.0 and delayValue <= 120.0: # hard limit of 0 to 120 seconds self.undo.push() self.mov.currentFrame.setDelayValue(delayValue) else: self.notify("Delay must be between 0-120 seconds.") def deleteCurrentFramePrompt(self): self.clearStatusLine() self.promptPrint("Are you sure you want to delete the current frame? (Y/N) ") prompting = True while prompting: time.sleep(0.01) c = self.stdscr.getch() if c == 121: # 'y' prompting = False self.undo.push() self.mov.deleteCurrentFrame() self.clearStatusLine() return True elif c == 110: # 'n' prompting = False self.clearStatusLine() return False time.sleep(0.01) def killAllHumans(self): # actually, kill all threads. #for thread in threading.enumerate(): # if thread.is_alive(): # thread.set() if self.appState.pool_executor != None: # Preview Exeutor thread dispatch running. Kill em self.appState.pool_executor.shutdown(wait=True, cancel_futures=True) def safeQuit(self): self.stdscr.nodelay(0) # wait for input when calling getch self.clearStatusLine() if self.appState.modified: self.promptPrint("Changes have not been saved! Are you sure you want to Quit? (Y/N) " ) else: self.promptPrint("Are you sure you want to Quit? (Y/N) " ) prompting = True while prompting: time.sleep(0.01) c = self.stdscr.getch() if c == 121: # 121 = y exiting = True prompting = False elif c == 110: # 110 = n exiting = False prompting = False time.sleep(0.01) self.clearStatusLine() if exiting: self.verySafeQuit() def jumpToPythonConsole(self): self.getReadyToSuspend() pdb.set_trace() self.resumeFromSuspend() def getReadyToSuspend(self): # Get the terminal ready for fun times curses.nocbreak() self.stdscr.keypad(0) curses.echo() def resumeFromSuspend(self): # Get the terminal ready for fun times curses.cbreak() self.stdscr.keypad(1) curses.noecho() def verySafeQuit(self): # non-interactive part.. close out curses screen and exit. curses.nocbreak() self.stdscr.keypad(0) curses.echo() curses.endwin() #print("Waiting for threads to die...") self.killAllHumans() #print("Done.") exit(0) def promptPrint(self, promptText): """ Prints prompting text in a consistent manner """ self.addstr(self.statusBarLineNum, 0, promptText, curses.color_pair(self.appState.theme['promptColor'])) def openTransformMenu(self): """ Show the status bar's menu for settings """ #self.statusBar.mainMenu.handler.panel.show() self.statusBar.animMenu.handler.panel.show() #response = self.statusBar.transformMenu.showHide() response = self.statusBar.transformMenu.show() if response == "Pop": pass else: self.statusBar.animMenu.handler.panel.hide() def openSettingsMenu(self): """ Show the status bar's menu for settings """ self.statusBar.mainMenu.handler.panel.show() response = self.statusBar.settingsMenu.showHide() self.statusBar.mainMenu.handler.panel.hide() def openEditMenu(self): """ Show the Edit menu """ self.statusBar.mainMenu.handler.panel.show() response = self.statusBar.editMenu.showHide() self.statusBar.mainMenu.handler.panel.hide() def openDrawCharPicker(self): self.stdscr.nodelay(0) #if self.appState.debug: self.notify(f"Loading character picker.") self.statusBar.drawCharPicker.pickChar() #if self.appState.debug: self.notify(f"Closing character picker.") #self.statusBar.toolMenu.handler.panel.hide() #self.statusBar.mainMenu.showHide() if self.playing: self.stdscr.nodelay(1) def openMainMenu(self): self.openMenu("File") def openAnimMenu(self): self.openMenu("Anim") def openMouseToolsMenu(self): self.openMenu("Mouse Tools") def setCursorModePaint(self): if self.appState.brush == None: self.notify("No brush set. Please use select tool to make one before using Paint.", pause=True) else: self.statusBar.setCursorModePaint() def openMenu(self, current_menu: str): menu_open = True self.stdscr.nodelay(0) # wait for input when calling getch cmode = self.appState.cursorMode if cmode == "Draw" or cmode == "Paint": self.disableMouseReporting() if not self.statusBar.toolButton.hidden: self.drawStatusBar() response = "Right" if self.playing: menus = ["File"] else: menus = ["File", "Anim", "Mouse Tools"] #fail_count = 0 # debug while menu_open: if current_menu == "File": #response = self.statusBar.menuButton.on_click() self.statusBar.menuButton.become_selected() response = self.statusBar.mainMenu.showHide() self.statusBar.menuButton.draw() # redraw as unselected/not-inverse elif current_menu == "Mouse Tools": #response = self.statusBar.toolButton.on_click() self.statusBar.toolButton.become_selected() response = self.statusBar.toolMenu.showHide() self.statusBar.toolButton.draw() # redraw as unselected/not-inverse elif current_menu == "Anim": #response = self.statusBar.toolButton.on_click() self.statusBar.animButton.become_selected() response = self.statusBar.animMenu.showHide() self.statusBar.animButton.draw() # redraw as unselected/not-inverse if response == "Close": menu_open = False if cmode == "Draw" or cmode == "Paint": self.enableMouseReporting() self.hardRefresh() elif response == "Right": # if we're at the rightmost menu if menus.index(current_menu) == len(menus) - 1: current_menu = menus[0] # circle back around else: next_menu_index = menus.index(current_menu) + 1 current_menu = menus[next_menu_index] elif response == "Left": # If we're at the leftmose menu if menus.index(current_menu) == 0: next_menu_index = len(menus) - 1 current_menu = menus[next_menu_index] else: next_menu_index = menus.index(current_menu) - 1 current_menu = menus[next_menu_index] #fail_count += 1 # debug #if fail_count > 3: # pdb.set_trace() if cmode == "Draw" or cmode == "Paint": self.enableMouseReporting() #self.hardRefresh() if self.playing: self.stdscr.nodelay(1) def openFromMenu(self): #self.stdscr.nodelay(0) # wait for input when calling getch self.clearStatusLine() if self.appState.modified: self.promptPrint("Changes have not been saved! Are you sure you want to load another file? (Y/N) ") prompting = True while prompting: time.sleep(0.01) c = self.stdscr.getch() if c == 121: # 121 = y prompting = False elif c == 110 or c == 27: # 110 == n, 27 == esc prompting = False return False time.sleep(0.01) self.clearStatusLine() load_filename, uri_type = self.openFilePicker() if uri_type == "remote": # Remote URL, from 16colo.rs url = load_filename #self.notify(f"url: {url}") file_data = b"There was an error downloading the file." # Download remote file try: with urllib.request.urlopen(url) as response: file_data = response.read() #f = open(filename, 'r', encoding='cp437') except urllib.request.HTTPError as E: self.notify(f"There was an error downloading the file: {E.code}") # save it to a temporary file #with tempfile.NamedTemporaryFile() as fp: with tempfile.TemporaryDirectory() as temp_path: # write file to temp directory, but with original name filename = pathlib.Path(url).name load_filename = temp_path + '/' + filename #self.notify(f"Full load path: {load_filename}") with open(load_filename, mode='wb') as fp: fp.write(file_data) #fp.write(file_data) #self.notify(f"Length: {len(file_data)}") # Clear the canvas and Open it load_filename = fp.name self.clearCanvas(prompting=False) #if not self.loadFromFile(load_filename, 'dur'): self.loadFromFile(load_filename, 'dur') self.appState.curOpenFileName = pathlib.Path(url).name self.move_cursor_topleft() self.stdscr.clear() self.hardRefresh() self.appState.fileShortPath = url fp.close() elif uri_type == "local" and load_filename != False: # if not False lol self.clearCanvas(prompting=False) self.loadFromFile(load_filename, 'dur') self.move_cursor_topleft() self.stdscr.clear() self.hardRefresh() elif load_filename == False: # User hit Esc out of the file picker return False def toggleColorScrolling(self): self.appState.scrollColors = not self.appState.scrollColors def toggleSideBar(self): if self.appState.sideBarEnabled: self.appState.sideBarEnabled = False self.statusBar.colorPicker.hide() if self.appState.colorMode == "16": self.statusBar.colorPicker_bg_16.hide() else: self.appState.sideBarEnabled = True #if self.appState.colorMode == "256": self.statusBar.colorPicker.show() if self.appState.colorMode == "16": self.statusBar.colorPicker_bg_16.show() def showCharSetPicker(self): set_list = self.appState.userCharSets + ["Durdraw Default"] block_list = self.appState.unicodeBlockList.copy() for set_name in set_list: block_list.insert(0, set_name) # draw ui selected_item_number = 0 current_line_number = 0 search_string = "" mask_all = False top_line = 0 # topmost viewable line, for scrolling prompting = True # Turn on keyboard buffer waiting here, if necessary.. self.stdscr.nodelay(0) self.stdscr.clear() self.cursorOff() while prompting: # draw list of files from top of the window to bottom realmaxY,realmaxX = self.realstdscr.getmaxyx() page_size = realmaxY - 4 current_line_number = 0 if selected_item_number > top_line + page_size-1 or selected_item_number < top_line: # item is off the screen top_line = selected_item_number - int(page_size-3) # scroll so it's at the bottom for blockname in block_list: if current_line_number >= top_line and current_line_number - top_line < page_size: # If we're within screen size currentActiveSet = False if blockname == self.appState.characterSet: # currently used character set currentActiveSet = True block_label = f"{block_list[current_line_number]} *" elif self.appState.characterSet == "Unicode Block" and blockname == self.appState.unicodeBlock: currentActiveSet = True block_label = f"{block_list[current_line_number]} *" else: block_label = block_list[current_line_number] if selected_item_number == current_line_number: # if block is selected self.addstr(current_line_number - top_line, 0, block_label, curses.A_REVERSE) if block_list[current_line_number] not in set_list: # if it's a unicode block... # Draw inline preview characters for the set #previewCharMap = durchar.load_unicode_block(block_list[selected_item_number]) previewCharMap = durchar.load_unicode_block(block_list[current_line_number]) previewChars = '' maxChars = 60 # number of preview characters to load totalChars = 0 previewOffset = len(block_label) + 2 # column to display preview characters at for miniMap in previewCharMap: # for all characters in this block... for key in miniMap: if totalChars <= maxChars: # If we're within range, previewChars += chr(miniMap[key]) # add to the preview string totalChars += 1 try: pass self.addstr(current_line_number - top_line, previewOffset, previewChars) except Exception as E: pass else: # print a block that isn't currently selected if block_list[current_line_number] in set_list: # Durdraw custom character set (like durdraw default), not a Unicode block if currentActiveSet: # currently used character set self.addstr(current_line_number - top_line, 0, block_label, curses.color_pair(self.appState.theme['menuTitleColor']) | curses.A_BOLD) else: self.addstr(current_line_number - top_line, 0, block_label, curses.color_pair(self.appState.theme['menuTitleColor'])) else: if currentActiveSet: self.addstr(current_line_number - top_line, 0, block_label, curses.color_pair(self.appState.theme['promptColor']) | curses.A_BOLD) else: self.addstr(current_line_number - top_line, 0, block_label, curses.color_pair(self.appState.theme['promptColor'])) # Draw inline preview characters for the set #previewCharMap = durchar.load_unicode_block(block_list[selected_item_number]) previewCharMap = durchar.load_unicode_block(block_list[current_line_number]) previewChars = '' maxChars = 60 # number of preview characters to load totalChars = 0 previewOffset = len(block_label) + 2 # column to display preview characters at for miniMap in previewCharMap: # for all characters in this block... for key in miniMap: if totalChars <= maxChars: # If we're within range, previewChars += chr(miniMap[key]) # add to the preview string totalChars += 1 try: pass self.addstr(current_line_number - top_line, previewOffset, previewChars) except Exception as E: pass current_line_number += 1 #if mask_all: # self.addstr(realmaxY - 4, 0, f"[X]", curses.color_pair(self.appState.theme['clickColor'])) #else: # self.addstr(realmaxY - 4, 0, f"[ ]", curses.color_pair(self.appState.theme['clickColor'])) #self.addstr(realmaxY - 4, 4, f"Show All Files", curses.color_pair(self.appState.theme['menuItemColor'])) #self.addstr(realmaxY - 4, 20, f"[PGUP]", curses.color_pair(self.appState.theme['clickColor'])) #self.addstr(realmaxY - 4, 27, f"[PGDOWN]", curses.color_pair(self.appState.theme['clickColor'])) #self.addstr(realmaxY - 4, 36, f"[OK]", curses.color_pair(self.appState.theme['clickColor'])) #self.addstr(realmaxY - 4, 41, f"[CANCEL]", curses.color_pair(self.appState.theme['clickColor'])) #self.addstr(realmaxY - 3, 0, f"Folder: {current_directory}", curses.color_pair(self.appState.theme['menuTitleColor'])) if search_string != "": self.addstr(realmaxY - 2, 0, f"search: ") self.addstr(realmaxY - 2, 8, f"{search_string}", curses.color_pair(self.appState.theme['menuItemColor'])) # print preview characters errorLoadingBlock = False if block_list[selected_item_number] in set_list: # not a unicode block errorLoadingBlock = False name = block_list[selected_item_number] self.addstr(realmaxY - 1, 0, f"Character set: {name}") if name != "Durdraw Default": # Load charater map and draw preview previewCharMap = durchar.load_charmap_file(self.appState.userCharSetFiles[name], self.appState) previewChars = "Preview: " maxChars = 100 totalChars = 0 for miniMap in previewCharMap: # for all characters in this block... for key in miniMap: if totalChars <= maxChars: # If we're within range, previewChars += chr(miniMap[key]) # add to the preview string totalChars += 1 try: self.addstr(realmaxY - 4, 0, previewChars) errorLoadingBlock = False except Exception as E: errorLoadingBlock = True else: previewCharMap = durchar.load_unicode_block(block_list[selected_item_number]) self.addstr(realmaxY - 1, 0, f"Unicode block: {block_list[selected_item_number]}") previewChars = "Preview: " maxChars = 100 totalChars = 0 for miniMap in previewCharMap: # for all characters in this block... for key in miniMap: if totalChars <= maxChars: # If we're within range, previewChars += chr(miniMap[key]) # add to the preview string totalChars += 1 try: self.addstr(realmaxY - 4, 0, previewChars) errorLoadingBlock = False except Exception as E: errorLoadingBlock = True self.addstr(realmaxY - 4, 0, "Cannot load this block") self.stdscr.refresh() c = self.stdscr.getch() self.stdscr.clear() if c == curses.KEY_LEFT: pass elif c == curses.KEY_RIGHT: pass elif c == curses.KEY_UP: # move cursor up if selected_item_number > 0: if selected_item_number == top_line and top_line != 0: top_line -= 1 selected_item_number -= 1 pass elif c == curses.KEY_DOWN: if selected_item_number < len(block_list) - 1: # move cursor down selected_item_number += 1 # if we're at the bottom of the screen... if selected_item_number - top_line == page_size and top_line < len(block_list) - page_size: top_line += 1 elif c in [339, curses.KEY_PPAGE]: # page up if selected_item_number - top_line > 0: # first go to the top of the page selected_item_number = top_line else: # if already there, go up a full page selected_item_number -= page_size top_line -= page_size # correct any overflow if selected_item_number < 0: selected_item_number = 0 if top_line < 0: top_line = 0 elif c in [338, curses.KEY_NPAGE]: # page down if selected_item_number - top_line < page_size - 1: # first go to bottom of the page selected_item_number = page_size + top_line - 1 else: # if already there, go down afull page selected_item_number += page_size top_line += page_size # correct any overflow if selected_item_number >= len(block_list): selected_item_number = len(block_list) - 1 if top_line >= len(block_list): top_line = len(block_list) - page_size elif c in [339, curses.KEY_HOME]: # 339 = home selected_item_number = 0 top_line = 0 elif c in [338, curses.KEY_END]: # 338 = end selected_item_number = len(block_list) - 1 top_line = selected_item_number - page_size + 1 if top_line < 0: # for small file lists top_line = 0 elif c in [13, curses.KEY_ENTER]: if errorLoadingBlock: pass elif block_list[selected_item_number] in set_list: # Durdraw character set selected. Set it name = block_list[selected_item_number] self.charMapNumber = 0 if name in self.appState.userCharSets: self.loadCharsetFile(self.appState.userCharSetFiles[name]) else: # Not a custom file. Probably DurDraw Default. self.setCharacterSet(block_list[selected_item_number]) #self.appState.characterSet = block_list[selected_item_number] self.initCharSet() self.stdscr.clear() prompting = False else: # Unicode block selected. self.appState.characterSet = "Unicode Block" self.charMapNumber = 0 self.appState.unicodeBlock = block_list[selected_item_number] self.setUnicodeBlock(block=self.appState.unicodeBlock) self.stdscr.clear() prompting = False #pdb.set_trace() #full_path = f"{current_directory}/{block_list[selected_item_number]}" #return full_path elif c == 27: # esc key if search_string != "": search_string = "" else: self.stdscr.clear() prompting = False if self.playing: self.stdscr.nodelay(1) self.cursorOn() return False elif c in [' curses.KEY_BACKSPACE', 263, 127]: # backspace if search_string != "": # if string is not empty, remove last character search_string = search_string[:len(search_string)-1] elif c == curses.KEY_MOUSE: try: _, mouseCol, mouseLine, _, mouseState = curses.getmouse() except: pass if mouseState == curses.BUTTON1_CLICKED or mouseState == curses.BUTTON1_DOUBLE_CLICKED: self.pressingButton = False if self.pushingToClip: self.pushingToClip = False if self.appState.cursorMode != "Draw" and self.appState.cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if mouseLine < realmaxY - 4: # above the 'status bar,' in the file list if mouseLine < len(block_list) - top_line: # clicked item line if mouseCol < len(block_list[top_line+mouseLine]): # clicked within item width selected_item_number = top_line+mouseLine if mouseState == curses.BUTTON1_DOUBLE_CLICKED: if block_list[selected_item_number] in set_list: # Durdraw character set selected. Set it name = block_list[selected_item_number] self.charMapNumber = 0 if name in self.appState.userCharSets: self.loadCharsetFile(self.appState.userCharSetFiles[name]) else: # Not a custom file. Probably DurDraw Default. self.setCharacterSet(block_list[selected_item_number]) #self.appState.characterSet = block_list[selected_item_number] self.initCharSet() self.stdscr.clear() prompting = False else: # clicked a unicode block self.appState.characterSet = "Unicode Block" self.charMapNumber = 0 self.setUnicodeBlock(block=block_list[selected_item_number]) self.stdscr.clear() prompting = False #if mouseLine == realmaxY - 4: # on the button bar # if mouseCol in range(0,3): # clicked [X] All # if mask_all: # mask_all = False # masks = default_masks # else: # mask_all = True # masks = ['*.*'] # update file list if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 try: curses.BUTTON5_PRESSED except: curses.BUTTON5_PRESSED = 0 try: curses.BUTTON4_PRESSED except: curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up # scroll up # if the item isn't at the top of teh screen, move it up if selected_item_number > top_line: selected_item_number -= 1 elif top_line > 0: top_line -= 1 elif mouseState & curses.BUTTON5_PRESSED: # wheel down # scroll down if selected_item_number < len(block_list) - 1: selected_item_number += 1 if selected_item_number == len(block_list) - top_line: top_line += 1 else: # add to search string search_string += chr(c) search_string = search_string.lower() # case insensitive search for blockname in block_list: # search list for search_string # prioritize unicode block names for some reason if blockname not in set_list and blockname.lower().startswith(search_string): selected_item_number = block_list.index(blockname) break # stop at the first match # then search character sets elif blockname in set_list and blockname.lower().startswith(search_string): selected_item_number = block_list.index(blockname) break # stop at the first match # Finally if nothing begins with the search string, see if # any items contain the search string elif search_string in blockname.lower(): selected_item_number = block_list.index(blockname) break # stop at the first match self.cursorOn() def sixteenc_update_diz_cache_start_thread(self, year): #if self.diz_caching_thread == None: # self.diz_caching_thread = threading.Thread(target=self.sixteenc_update_diz_cache, args=(year,)) #if self.diz_caching_thread.is_alive(): if year in self.appState.sixteenc_cached_years: # Thread already ran or running return False new_caching_thread = threading.Thread(target=self.sixteenc_update_diz_cache, args=(year,), daemon = True) new_caching_thread.start() def sixteenc_cache_diz_for_pack(self, pack): self.thread_update_user("Downloading previews... 1") pack_files = self.sixteenc_api.list_files_for_pack(pack) #self.notify(f"pack: {pack}, pack_files: {pack_files}") if pack_files != False: preview_filename=None for name in pack_files: if name.lower() == "file_id.ans": preview_filename = name elif name.lower() == "file_id.diz" and preview_filename == None: preview_filename = name if preview_filename == None: url = None else: url = self.sixteenc_api.get_url_for_file(pack, preview_filename) #pdb.set_trace() if url != None and url != '': # success #self.notify(f"preview URL: {url}") try: with urllib.request.urlopen(url) as response: file_data = response.read() except urllib.request.HTTPError: #self.notify(f"There was an error downloading: {url}")a pass #return False else: pass if file_data != None: decoded_data = file_data.decode('cp437') load_width = 44 # file_id.diz width ## Figure out if width/height is in sauce. If so, use that file_sauce = dursauce.SauceParser() # empty sauce file_sauce.parse_blob(file_data) #self.notify(f"Sauce, pack: {pack}, filename: {preview_filename}, width: {file_sauce.width}", wait_time=100) if file_sauce.width != None: if file_sauce.width > 0 and file_sauce.width < load_width: load_width = file_sauce.width elif file_sauce.width > load_width: load_width = file_sauce.width diz_frame = dur_ansiparse.parse_ansi_escape_codes(file_data, filename = None, appState=self.appState, caller=self, debug=self.appState.debug, maxWidth=load_width) self.appState.sixteenc_dizcache[pack] = diz_frame #time.sleep(0.50) # sleep for 1/2 second betwewen each pack, to prevent throttling def thread_update_user(self, message): # print status update progress_string = message progress_column = self.appState.realmaxX - len(progress_string) # anchor right progress_line = self.appState.realmaxY # bottom #self.addstr(progress_line, progress_column, progress_string, curses.color_pair(self.appState.theme['menuItemColor'])) #self.notify(progress_string, wait_time=0) #self.stdscr.refresh() def sixteenc_update_diz_cache(self, year): """ cache file_id.diz files for the packs in a given year. Populate self.appState.sixteenc_dizcache with {"packname": diz_data_frame} diz_data_frame is type Dur Frame """ # print status update #self.thread_update_user("Downloading previews... 2") url = None file_data = None preview_mov = None packs = [] #packs to cache if year in self.appState.sixteenc_cached_years: return False else: self.appState.sixteenc_cached_years.append(year) # Get list of packs for given year from 16c year_packs = self.sixteenc_api.list_packs_for_year(year) if year_packs == False: # There was a problem getting the packs. HTTP error return False # For each pack in the returned list, see if self.appState.sixteenc_dizcache has # the pack as a key. # If not, download the file_id.diz and populate self.appState.sixteenc_dizcache. for pack in year_packs: if pack not in self.appState.sixteenc_dizcache: packs.append(pack) # Launch concurrent threads for downloading diz files in parallel max_threads = 10 with ThreadPoolExecutor(max_workers=max_threads) as self.appState.pool_executor: res = self.appState.pool_executor.map(self.sixteenc_cache_diz_for_pack, packs, timeout=15) #for r in res: # print(r.status_code) def findLocalFiles(self, current_directory, folders, masks): # update file list self.log.debug('finding local files!!!', {'current_directory': current_directory, 'folders': folders, 'masks': masks}) file_list, matched_files = [], [] if self.appState.sixteenc_browsing: search_files_list = [] else: search_files_list = os.listdir(current_directory) for file in search_files_list: for mask in masks: if fnmatch.fnmatch(file.lower(), mask.lower()): matched_files.append(file) break for dirname in folders: file_list.append(dirname) file_list += sorted(matched_files) self.log.debug('repopulated file list', {'file_list': file_list}) return file_list def openFilePicker(self): """ Draw UI for selecting a file to load, return the filename """ # get file list self.stdscr.nodelay(0) # wait for input when calling getch self.cursorOff() folders = ["../"] default_masks = ['*.dur', '*.durf', '*.asc', '*.ans', '*.txt', '*.diz', '*.nfo', '*.ice', '*.ansi'] masks = default_masks matched_files = [] file_list = [] preview_mov = None old_color_mode = self.appState.colorMode # in case we switch while file picker is open #self.selected_item_number = 0 self.sixteenc_api = None # set correct color mode for initial picker opening if self.appState.sixteenc_browsing: if self.appState.colorMode != "16": self.switchTo16ColorMode() elif not self.appState.sixteenc_browsing: # IF we aren't in the original mode, switch back if old_color_mode != self.appState.colorMode: if old_color_mode == "256": self.switchTo256ColorMode() # Set the directory listing for local files if self.appState.workingLoadDirectory: if os.path.exists(self.appState.workingLoadDirectory): current_directory = self.appState.workingLoadDirectory else: current_directory = os.getcwd() else: current_directory = os.getcwd() if not self.appState.sixteenc_browsing: file_list = [] #folders += sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, "*/")))) folders = ['../'] + sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, "*/")))) # remove leading paths new_folders = [] for path_string in folders: new_folders.append(os.path.sep.join(path_string.split(os.path.sep)[-2:])) folders = new_folders for file in os.listdir(current_directory): for mask in masks: if fnmatch.fnmatch(file.lower(), mask.lower()): matched_files.append(file) break elif self.appState.sixteenc_browsing: self.sixteenc_api = SixteenColorsAPI() if self.sixteenc_years == None: self.sixteenc_years = self.sixteenc_api.list_years() # Set the initial listing/UI for 16colo.rs browsing if self.sixteenc_levels[self.sixteenc_level] == "root": self.appState.sixteenc_year = None sixteenc_packs = None folders = self.sixteenc_years file_list = [] full_file_list = file_list search_files_list = file_list matched_files = search_files_list elif self.sixteenc_levels[self.sixteenc_level] == "year": sixteenc_packs = self.sixteenc_api.list_packs_for_year(self.appState.sixteenc_year) folders = ['../'] + sixteenc_packs #file_list = folders file_list = [] full_file_list = file_list search_files_list = file_list elif self.sixteenc_levels[self.sixteenc_level] == "pack": sixteenc_files = self.sixteenc_api.list_files_for_pack(self.appState.sixteenc_pack) folders = ['../'] #file_list = folders file_list = folders + sixteenc_files full_file_list = file_list search_files_list = file_list #file_list = sorted(file_list) if not self.appState.sixteenc_browsing: for dirname in folders: file_list.append(dirname) file_list += sorted(matched_files) # stash away file list so we can use it for search, and pop it back # in when user hits esc full_file_list = file_list black_frame = durmovie.Frame(80, 25) # easy way to clear parts of the screen? # draw ui #self.selected_item_number = 0 current_line_number = 0 search_string = '' mask_all = False #tabbed = False #sections = ["main", "showall", "sixteenc"] # Different sections of the UI we can tab through sections = ["main", "showall"] if self.appState.sixteenc_available: sections.append("sixteenc") current_section = 0 show_modtime = True # show a column for file modified times top_line = 0 # topmost viewable line, for scrolling showall_column = 0 # column for [X] Show All Files checkbox sixteen_column = 21 # Column for [X] 16colo.rs Archive prompting = True # Turn on keyboard buffer waiting here, if necessary.. self.stdscr.nodelay(0) self.stdscr.clear() # stuff for 16colo.rs - caches to avoid hitting API repeatedly sixteenc_packs = [] sixteenc_files = [] # cache for current file listing sixteenc_current_file = None while prompting: #file_list = [] #full_file_list = [] # If we need to cache 16c years, make it so. if self.appState.sixteenc_browsing: show_modtime = False # If there's no 16c cache, make a new API object and cache the years if self.sixteenc_api == None: self.sixteenc_api = SixteenColorsAPI() if self.sixteenc_years == None: self.sixteenc_years = self.sixteenc_api.list_years() # If we aren't in a year, then populate the directory list with years if self.appState.sixteenc_year == None: # We are at the top level, so list the years. folders = ["../"] folders += self.sixteenc_years file_list = self.sixteenc_years full_file_list = file_list # Set search matching #filtered_list = [item for item in full_file_list if search_string.lower() in item.lower()] if search_string != '': file_list = [item for item in full_file_list if search_string.lower() in item.lower()] if len(file_list) == 0: file_list = ["../"] else: file_list = full_file_list # draw list of files from top of the window to bottom realmaxY,realmaxX = self.realstdscr.getmaxyx() page_size = realmaxY - 4 current_line_number = 0 file_column_number = 0 if show_modtime: file_column_number = 20 if self.selected_item_number > top_line + page_size-1 or self.selected_item_number < top_line: # item is off the screen top_line = self.selected_item_number - int(page_size-3) # scroll so it's at the bottom # remove any files we can't open if not self.appState.sixteenc_browsing: for filename in file_list: full_path = f"{current_directory}/{filename}" try: file_modtime_string = durfile.get_file_mod_date_time(full_path) except FileNotFoundError: # Probably a dead symlink. # Access problems. Remove from list. file_list = [item for item in file_list if filename not in item] for filename in file_list: if current_line_number >= top_line and current_line_number - top_line < page_size: # If we're within screen size if self.selected_item_number == current_line_number and sections[current_section] == "main": # if file is selected if file_list[current_line_number] in folders: self.addstr(current_line_number - top_line, 0, file_list[current_line_number], curses.A_REVERSE) else: # not a folder, print modified column if show_modtime and not self.appState.sixteenc_browsing: full_path = f"{current_directory}/{file_list[current_line_number]}" file_modtime_string = durfile.get_file_mod_date_time(full_path) self.addstr(current_line_number - top_line, 0, file_modtime_string, curses.color_pair(self.appState.theme['menuItemColor'])) self.addstr(current_line_number - top_line, file_column_number, file_list[current_line_number], curses.A_REVERSE) else: if file_list[current_line_number] in folders: if file_list[current_line_number] in self.appState.sixteenc_dizcache and self.appState.sixteenc_browsing: # file_id has been downloaded, so draw in bold self.addstr(current_line_number - top_line, 0, file_list[current_line_number], curses.color_pair(self.appState.theme['menuTitleColor']) | curses.A_BOLD) else: self.addstr(current_line_number - top_line, 0, file_list[current_line_number], curses.color_pair(self.appState.theme['menuTitleColor'])) else: if show_modtime and self.appState.sixteenc_browsing == False: full_path = f"{current_directory}/{file_list[current_line_number]}" file_modtime_string = durfile.get_file_mod_date_time(full_path) self.addstr(current_line_number - top_line, 0, file_modtime_string, curses.color_pair(self.appState.theme['menuItemColor'])) self.addstr(current_line_number - top_line, file_column_number, file_list[current_line_number], curses.color_pair(self.appState.theme['promptColor'])) current_line_number += 1 if self.appState.sixteenc_browsing == False: if mask_all: if sections[current_section] == "showall": self.addstr(realmaxY - 4, showall_column, f"[X]", curses.A_REVERSE) else: self.addstr(realmaxY - 4, showall_column, f"[X]", curses.color_pair(self.appState.theme['clickColor'])) else: if sections[current_section] == "showall": self.addstr(realmaxY - 4, showall_column, f"[ ]", curses.A_REVERSE) else: self.addstr(realmaxY - 4, showall_column, f"[ ]", curses.color_pair(self.appState.theme['clickColor'])) self.addstr(realmaxY - 4, showall_column + 4, f"Show All Files", curses.color_pair(self.appState.theme['menuItemColor'])) if self.appState.sixteenc_available: if self.appState.sixteenc_browsing: if sections[current_section] == "sixteenc": self.addstr(realmaxY - 4, sixteen_column, f"[X]", curses.A_REVERSE) else: self.addstr(realmaxY - 4, sixteen_column, f"[X]", curses.color_pair(self.appState.theme['clickColor'])) else: if sections[current_section] == "sixteenc": self.addstr(realmaxY - 4, sixteen_column, f"[ ]", curses.A_REVERSE) else: self.addstr(realmaxY - 4, sixteen_column, f"[ ]", curses.color_pair(self.appState.theme['clickColor'])) self.addstr(realmaxY - 4, sixteen_column + 4, f"16colo.rs Archive", curses.color_pair(self.appState.theme['menuItemColor'])) #self.addstr(realmaxY - 4, 20, f"[PGUP]", curses.color_pair(self.appState.theme['clickColor'])) #self.addstr(realmaxY - 4, 27, f"[PGDOWN]", curses.color_pair(self.appState.theme['clickColor'])) #self.addstr(realmaxY - 4, 36, f"[OK]", curses.color_pair(self.appState.theme['clickColor'])) #self.addstr(realmaxY - 4, 41, f"[CANCEL]", curses.color_pair(self.appState.theme['clickColor'])) if self.appState.sixteenc_browsing: self.addstr(realmaxY - 3, 0, f"Year: {self.appState.sixteenc_year}, Pack: {self.appState.sixteenc_pack}, Level: {self.sixteenc_levels[self.sixteenc_level]}", curses.color_pair(self.appState.theme['menuTitleColor'])) else: self.addstr(realmaxY - 3, 0, f"Folder: {current_directory}", curses.color_pair(self.appState.theme['menuTitleColor'])) if search_string != "": self.addstr(realmaxY - 2, 0, f"search: ") self.addstr(realmaxY - 2, 8, f"{search_string}", curses.color_pair(self.appState.theme['menuItemColor'])) if self.selected_item_number > len(file_list) - 1: self.selected_item_number = 0 self.log.debug('selected item number', {'selected_item_number': self.selected_item_number, 'n_files': len(file_list)}) #if not self.appState.sixteenc_browsing: # try: # filename = file_list[self.selected_item_number] # except: # pdb.set_trace() filename = None if len(file_list) > 0: filename = file_list[self.selected_item_number] if self.appState.sixteenc_browsing or filename is None: full_path = '' else: full_path = f"{current_directory}/{file_list[self.selected_item_number]}" if filename not in folders and filename is not None : # read sauce, if available #file_sauce = dursauce.SauceParser(full_path) file_sauce = dursauce.SauceParser() file_sauce.parse_file(full_path) sauce_title = file_sauce.title sauce_author = file_sauce.author sauce_width = file_sauce.width sauce_height = file_sauce.height sauce_date = file_sauce.date sauce_year = file_sauce.year sauce_month = file_sauce.month sauce_day = file_sauce.day sauce_width = file_sauce.width sauce_height = file_sauce.height else: file_sauce = dursauce.SauceParser() # empty placeholder sauce sauce_title = None sauce_author = None #sauce_width = file_sauce.width #sauce_height = file_sauce.height if self.appState.sixteenc_browsing or filename is None: file_size = None file_modtime_string = "" else: file_size = os.path.getsize(full_path) file_modtime_string = durfile.get_file_mod_date_time(full_path) # display file info - format data file_info = f"File: {filename}, Size: {file_size}, Modified: {file_modtime_string}" if file_sauce.sauce_found: file_info = f"{sauce_title}, Artist: {sauce_author}, Date: {sauce_year}/{sauce_month}/{sauce_day}, Width: {sauce_width}, Height: {sauce_height}, Size: {file_size}" # show it on screen self.addstr(realmaxY - 1, 0, f"{file_info}") if self.appState.thread_update_string != None: self.thread_update_user(thread_update_string) # If there is a preview to load, load it if self.appState.sixteenc_browsing: selected_item = file_list[self.selected_item_number] if self.sixteenc_levels[self.sixteenc_level] == "year": # If a pack is selected, download the file_id.diz and preview it if selected_item != '../': if selected_item not in self.appState.sixteenc_dizcache: self.drawFrame(frame=black_frame, col_offset=40, preview=True) #pass #self.stdscr.clear() #self.stdscr.refresh() #self.sixteenc_update_diz_cache(self.appState.sixteenc_year) if selected_item in self.appState.sixteenc_dizcache: preview_frame = self.appState.sixteenc_dizcache[selected_item] left_diz_column = max(40, realmaxX - preview_frame.sizeX - 3) # anchor right self.drawFrame(frame=preview_frame, col_offset=left_diz_column, preview=True) elif self.sixteenc_levels[self.sixteenc_level] == "pack": if self.appState.sixteenc_pack in self.appState.sixteenc_dizcache: preview_frame = self.appState.sixteenc_dizcache[self.appState.sixteenc_pack] left_diz_column = max(40, realmaxX - preview_frame.sizeX - 3) # anchor right self.drawFrame(frame=preview_frame, col_offset=left_diz_column, preview=True) #self.stdscr.refresh() # Read keyboard input c = self.stdscr.getch() if c == curses.KEY_MOUSE: try: _, mouseCol, mouseLine, _, mouseState = curses.getmouse() except: pass if mouseState == curses.BUTTON1_CLICKED or mouseState == curses.BUTTON1_DOUBLE_CLICKED: if mouseLine < realmaxY - 4: # above the 'status bar,' in the file list current_section = 0 # switch to "main" section if mouseLine < len(file_list) - top_line: # clicked item line if mouseCol < len(file_list[top_line+mouseLine] + file_modtime_string): # clicked within item width self.selected_item_number = top_line+mouseLine if mouseState == curses.BUTTON1_DOUBLE_CLICKED: #self.notify("Double Clicked on item") if file_list[self.selected_item_number] in folders: # clicked directory if not self.appState.sixteenc_browsing: # change directories. holy fuck this is deep if file_list[self.selected_item_number] == "../": # "cd .." if self.appState.sixteenc_browsing: if self.sixteenc_level > 0: self.sixteenc_level = self.sixteenc_level - 1 else: current_directory = os.path.split(current_directory)[0] else: current_directory = f"{current_directory}/{file_list[self.selected_item_number]}" if current_directory[-1] == "/": current_directory = current_directory[:-1] # get file list folders = ["../"] #folders += glob.glob("*/", root_dir=current_directory) if not self.appState.sixteenc_browsing: folders += sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, "*/")))) # remove leading paths new_folders = [] for path_string in folders: new_folders.append(os.path.sep.join(path_string.split(os.path.sep)[-2:])) folders = new_folders if mask_all: masks = ['*.*'] else: masks = default_masks matched_files = [] file_list = [] for file in os.listdir(current_directory): for mask in masks: if fnmatch.fnmatch(file.lower(), mask.lower()): matched_files.append(file) break for dirname in folders: file_list.append(dirname) file_list += sorted(matched_files) # reset ui self.selected_item_number = 0 search_string = "" elif self.appState.sixteenc_browsing: #self.notify("Double Clicked on 16c item") search_string = "" # Clicked a year or pack name in 16c, so navigate in. if self.sixteenc_levels[self.sixteenc_level] == "root": # Enter into a year #if file_list[current_line_number] != '../': try: self.appState.sixteenc_year = file_list[self.selected_item_number] except: pdb.set_trace() sixteenc_packs = self.sixteenc_api.list_packs_for_year(self.appState.sixteenc_year) try: folders = ['../'] + sixteenc_packs except: pdb.set_trace() file_list = folders full_file_list = file_list search_files_list = file_list self.sixteenc_level += 1 # from "root" to "year" self.selected_item_number = 0 top_line = 0 c = None elif self.sixteenc_levels[self.sixteenc_level] == "year": if file_list[self.selected_item_number] == '../': # Navigate back down into root self.sixteenc_level = 0 # from "year" to "root" self.selected_item_number = 0 #self.selected_item_number = 0 self.appState.sixteenc_year = None sixteenc_packs = None #folders = ['../'] + self.sixteenc_years folders = self.sixteenc_years file_list = [] full_file_list = file_list search_files_list = file_list top_line = 0 c = None else: # Navigate from year into the pack self.appState.sixteenc_pack = file_list[self.selected_item_number] try: sixteenc_files = self.sixteenc_api.list_files_for_pack(self.appState.sixteenc_pack) except: pdb.set_trace() self.selected_item_number = 0 folders = ['../'] file_list = folders file_list += sixteenc_files full_file_list = file_list search_files_list = file_list #for dirname in folders: # file_list.append(dirname) file_list = sorted(file_list) self.sixteenc_level += 1 # from "year" to "pack" top_line = 0 search_string = "" # Because 16c files are being added to the directory list, for some reason: elif self.sixteenc_levels[self.sixteenc_level] == "pack": # Picked a file, so download and load it filename = file_list[self.selected_item_number] url = self.sixteenc_api.get_url_for_file(self.appState.sixteenc_pack, filename) self.cursorOn() return url, "remote" else: # clicked a file, try to load it if self.appState.sixteenc_browsing: # Picked a file, so download and load it filename = file_list[self.selected_item_number] url = self.sixteenc_api.get_url_for_file(self.appState.sixteenc_pack, filename) self.cursorOn() return url, "remote" else: full_path = f"{current_directory}/{file_list[self.selected_item_number]}" self.cursorOn() return full_path, "local" if mouseLine == realmaxY - 4: # on the button bar if mouseCol in range(showall_column,showall_column+3): # clicked [X] All if self.appState.sixteenc_browsing == False: if mask_all: mask_all = False masks = default_masks else: mask_all = True masks = ['*.*'] elif self.appState.sixteenc_available and mouseCol in range(sixteen_column,sixteen_column+3): # clicked [X] 16c self.appState.sixteenc_browsing = not self.appState.sixteenc_browsing folders = ["../"] #folders += glob.glob("*/", root_dir=current_directory) if not self.appState.sixteenc_browsing: if mask_all: folders = ['../'] + sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, ".*/")))) + \ sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, "*/")))) else: folders = ['../'] + sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, "*/")))) # remove leading paths new_folders = [] for path_string in folders: new_folders.append(os.path.sep.join(path_string.split(os.path.sep)[-2:])) folders = new_folders matched_files = [] file_list = [] file_list = self.findLocalFiles(current_directory, folders, masks) # reset ui self.selected_item_number = 0 search_string = "" full_file_list = file_list self.log.debug('repopulated file list', {'file_list': file_list}) if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 try: curses.BUTTON5_PRESSED except: curses.BUTTON5_PRESSED = 0 try: curses.BUTTON4_PRESSED except: curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up # scroll up # if the item isn't at the top of teh screen, move it up current_section = 0 if self.selected_item_number > top_line: self.selected_item_number -= 1 elif top_line > 0: top_line -= 1 elif mouseState & curses.BUTTON5_PRESSED: # wheel down # scroll down current_section = 0 if self.selected_item_number < len(file_list) - 1: self.selected_item_number += 1 if self.selected_item_number == len(file_list) - top_line: top_line += 1 elif sections[current_section] == "showall": # show all files checkbox # Change focus from files to other elements, ie: Show All Files checkbox #c = self.stdscr.getch() if c in [9]: # 9 = Tab current_section += 1 if current_section > len(sections) - 1: # If we've tabbed through all sections, current_section = 0 # circle back to 0 (main file selector) if c in [curses.KEY_UP, curses.KEY_DOWN]: # 9 = Tab # Change focus away, back to file lister current_section = 0 elif c in [ord(' '), 13, curses.KEY_ENTER]: # 13 = enter # Check or uncheck Show All Files mask_all = not mask_all if mask_all: masks = ['*.*'] else: masks = default_masks # update file list matched_files = [] file_list = [] full_file_list = [] search_files_list = [] if self.appState.sixteenc_browsing: if self.sixteenc_levels[self.sixteenc_level] == "pack": search_files_list = sixteenc_files else: search_files_list = os.listdir(current_directory) for file in search_files_list: for mask in masks: if fnmatch.fnmatch(file.lower(), mask.lower()): matched_files.append(file) break for dirname in folders: file_list.append(dirname) file_list += sorted(matched_files) # reset ui self.selected_item_number = 0 search_string = "" full_file_list = file_list elif c in [ord('|')]: # debug pdb.set_trace() elif c in [27]: # esc prompting = False c = None elif sections[current_section] == "sixteenc": # show all files checkbox if c in [9]: # 9 = Tab current_section += 1 if current_section > len(sections) - 1: # If we've tabbed through all sections, current_section = 0 # circle back to 0 (main file selector) if c in [curses.KEY_UP, curses.KEY_DOWN]: # 9 = Tab current_section = 0 elif c in [ord(' '), 13, curses.KEY_ENTER]: # 13 = enter self.appState.sixteenc_browsing = not self.appState.sixteenc_browsing # set correct color mode if self.appState.sixteenc_browsing: if self.appState.colorMode != "16": self.switchTo16ColorMode() elif not self.appState.sixteenc_browsing: # IF we aren't in the original mode, switch back if old_color_mode != self.appState.colorMode: if old_color_mode == "256": self.switchTo256ColorMode() # update file list matched_files = [] file_list = [] full_file_list = [] if self.appState.sixteenc_browsing: # Just turned on sixteenc browsing self.sixteenc_level = 0 self.selected_item_number = 0 # If there's no 16c cache, make a new API object and cache the years if self.sixteenc_api == None: self.sixteenc_api = SixteenColorsAPI() if self.sixteenc_years == None: self.sixteenc_years = self.sixteenc_api.list_years() # If we're at the root, navigate into the selected year if self.sixteenc_levels[self.sixteenc_level] == "root": #self.notify("Root of all 16colors evil") # Update the file or directory listing #if file_list[current_line_number] != '../': self.appState.sixteenc_year = None sixteenc_packs = None #folders = ['../'] + self.sixteenc_years folders = self.sixteenc_years file_list = [] full_file_list = file_list search_files_list = file_list elif self.sixteenc_levels[self.sixteenc_level] == "year": pass elif self.sixteenc_levels[self.sixteenc_level] == "pack": pass else: # update with files, not 16c # update file list matched_files = [] file_list = [] full_file_list = [] if mask_all: folders = ['../'] + sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, ".*/")))) + \ sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, "*/")))) else: folders = ['../'] + sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, "*/")))) # remove leading paths new_folders = [] for path_string in folders: new_folders.append(os.path.sep.join(path_string.split(os.path.sep)[-2:])) folders = new_folders matched_files = self.findLocalFiles(current_directory, folders, masks) self.log.debug('found', {'matched_files': matched_files, 'folders': folders, 'current_directory': current_directory, 'masks': masks}) file_list += matched_files # reset ui self.selected_item_number = 0 search_string = "" full_file_list = file_list # file_list = folders full_file_list = file_list search_files_list = file_list if c in [27]: # esc prompting = False c = None elif sections[current_section] == "main": # show all files checkbox if c == curses.KEY_LEFT: pass elif c == curses.KEY_RIGHT: pass elif c in [9]: # 9 == tab key current_section += 1 if self.appState.sixteenc_browsing: # skip hidden Show All button in 16c mode if sections[current_section] == "showall": current_section += 1 if current_section > len(sections) - 1: # If we've tabbed through all sections, current_section = 0 # circle back to 0 (main file selector) elif c == curses.KEY_UP: # move cursor up if self.selected_item_number > 0: if self.selected_item_number == top_line and top_line != 0: top_line -= 1 self.selected_item_number -= 1 pass elif c == curses.KEY_DOWN: if self.selected_item_number < len(file_list) - 1: # move cursor down self.selected_item_number += 1 # if we're at the bottom of the screen... if self.selected_item_number - top_line == page_size and top_line < len(file_list) - page_size: top_line += 1 elif c in [339, curses.KEY_PPAGE]: # page up if self.selected_item_number - top_line > 0: # first go to the top of the page self.selected_item_number = top_line else: # if already there, go up a full page self.selected_item_number -= page_size top_line -= page_size # correct any overflow if self.selected_item_number < 0: self.selected_item_number = 0 if top_line < 0: top_line = 0 elif c in [338, curses.KEY_NPAGE]: # page down if self.selected_item_number - top_line < page_size - 1: # first go to bottom of the page self.selected_item_number = page_size + top_line - 1 else: # if already there, go down afull page self.selected_item_number += page_size top_line += page_size # correct any overflow if self.selected_item_number >= len(file_list): self.selected_item_number = len(file_list) - 1 if top_line >= len(file_list): top_line = len(file_list) - page_size elif c in [339, curses.KEY_HOME]: # 339 = home self.selected_item_number = 0 top_line = 0 elif c in [338, curses.KEY_END]: # 338 = end self.selected_item_number = len(file_list) - 1 top_line = self.selected_item_number - page_size + 1 if top_line < 0: # for small file lists top_line = 0 elif c in [13, curses.KEY_ENTER]: # Pressed enter on a file or directory. if self.appState.sixteenc_browsing: # If we're at the root, navigate into the selected year if self.sixteenc_levels[self.sixteenc_level] == "root": # Enter into a year #if file_list[current_line_number] != '../': try: self.appState.sixteenc_year = file_list[self.selected_item_number] except: pdb.set_trace() sixteenc_packs = self.sixteenc_api.list_packs_for_year(self.appState.sixteenc_year) try: folders = ['../'] + sixteenc_packs except: pdb.set_trace() file_list = folders full_file_list = file_list search_files_list = file_list self.sixteenc_level += 1 # from "root" to "year" self.selected_item_number = 0 top_line = 0 search_string = "" # run caching thread to update file_id.diz's for the selected year if self.appState.sixteenc_year not in self.appState.sixteenc_cached_years: self.sixteenc_update_diz_cache_start_thread(self.appState.sixteenc_year) #self.sixteenc_update_diz_cache(self.appState.sixteenc_year) c = None elif self.sixteenc_levels[self.sixteenc_level] == "year": if file_list[self.selected_item_number] == '../': # Navigate back down into root self.sixteenc_level = 0 # from "year" to "root" self.selected_item_number = 0 #self.selected_item_number = 0 self.appState.sixteenc_year = None sixteenc_packs = None #folders = ['../'] + self.sixteenc_years folders = self.sixteenc_years file_list = [] full_file_list = file_list search_files_list = file_list top_line = 0 search_string = "" c = None else: # Navigate from year into the pack self.appState.sixteenc_pack = file_list[self.selected_item_number] try: sixteenc_files = self.sixteenc_api.list_files_for_pack(self.appState.sixteenc_pack) except: pdb.set_trace() self.selected_item_number = 0 folders = ['../'] file_list = folders file_list += sixteenc_files full_file_list = file_list search_files_list = file_list #for dirname in folders: # file_list.append(dirname) file_list = sorted(file_list) self.sixteenc_level += 1 # from "year" to "pack" top_line = 0 search_string = "" c = None elif self.sixteenc_levels[self.sixteenc_level] == "pack": if file_list[self.selected_item_number] == '../': # Navigating back down from pack into year self.selected_item_number = 0 sixteenc_packs = self.sixteenc_api.list_packs_for_year(self.appState.sixteenc_year) try: folders = ['../'] + sixteenc_packs except: pdb.set_trace() file_list = folders full_file_list = file_list search_files_list = file_list self.sixteenc_level = 1 # from "root" to "year" top_line = 0 search_string = "" else: # Picked a file, so download and load it :) filename = file_list[self.selected_item_number] #pdb.set_trace() url = self.sixteenc_api.get_url_for_file(self.appState.sixteenc_pack, filename) self.cursorOn() return url, "remote" c = None elif file_list[self.selected_item_number] in folders: # change directories if file_list[self.selected_item_number] == "../": # "cd .." if self.appState.sixteenc_browsing: # Navigate "up" in 16c land... pass else: current_directory = os.path.split(current_directory)[0] else: if not self.appState.sixteenc_browsing: current_directory = f"{current_directory}/{file_list[self.selected_item_number]}" if current_directory[-1] == "/": current_directory = current_directory[:-1] # get file list folders = ["../"] #folders += sorted(glob.glob("*/", root_dir=current_directory)) if self.appState.sixteenc_browsing: pass else: if mask_all: folders = ['../'] + sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, ".*/")))) + \ sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, "*/")))) else: folders = ['../'] + sorted(filter(os.path.isdir, glob.glob(os.path.join(current_directory, "*/")))) # remove leading paths if not self.appState.sixteenc_browsing: new_folders = [] for path_string in folders: new_folders.append(os.path.sep.join(path_string.split(os.path.sep)[-2:])) folders = new_folders if mask_all: masks = ['*.*'] else: masks = default_masks matched_files = [] file_list = [] if self.appState.sixteenc_browsing: if self.appState.sixteenc_pack: search_files_list = sixteenc_files elif self.appState.sixteenc_year: search_files_list = sixteenc_packs else: search_files_list = self.sixteenc_years else: search_files_list = os.listdir(current_directory) for file in search_files_list: for mask in masks: if fnmatch.fnmatch(file.lower(), mask.lower()): matched_files.append(file) break for dirname in folders: file_list.append(dirname) file_list += sorted(matched_files) # reset ui top_line = 0 self.selected_item_number = 0 search_string = "" full_file_list = file_list else: # return the selected file self.stdscr.clear() prompting = False full_path = f"{current_directory}/{file_list[self.selected_item_number]}" self.appState.workingLoadDirectory = current_directory self.cursorOn() return full_path, "local" #self.filePickerOptionsPicker() elif c == 21: # ^U or ctrl-u # clear the search field if search_string != "": search_string = "" elif c == 27: # esc key if search_string != "": search_string = "" else: # switch back to old color mode if old_color_mode != self.appState.colorMode: if old_color_mode == "256": self.switchTo256ColorMode() self.stdscr.clear() prompting = False if self.playing: self.stdscr.nodelay(1) self.cursorOn() return False, "local" elif c in [' curses.KEY_BACKSPACE', 263, 127]: # backspace if search_string != "": # if string is not empty, remove last character search_string = search_string[:len(search_string)-1] elif c == None: pass elif c == 0x019A: # getch() gets this when resizing window, for some reason. # unicode Latin Small Letter L with Bar. pass else: # add to search string try: search_string += chr(c) except ValueError: # unprintable characters inserted by terminal when resizing, # so don't add to search string. pass self.selected_item_number = 0 current_line_number = 0 top_line = 0 for filename in file_list: # search list for search_string if filename not in folders and filename.startswith(search_string): #self.selected_item_number = file_list.index(filename) break # stop at the first match self.stdscr.clear() self.cursorOn() # switch back to old color mode if old_color_mode != self.appState.colorMode: if old_color_mode == "256": self.switchTo256ColorMode() return False, False def open(self): self.clearStatusLine() if self.appState.modified: self.promptPrint("Changes have not been saved! Are you sure you want to load another file? (Y/N) ") prompting = True while prompting: time.sleep(0.01) c = self.stdscr.getch() if c == 121: # 121 = y prompting = False elif c == 110 or c == 27: # 110 == n, 27 == esc prompting = False return None time.sleep(0.01) self.clearStatusLine() self.move(self.mov.sizeY, 0) self.stdscr.nodelay(0) # wait for input when calling getch self.promptPrint("File format? [I] ASCII, [D] DUR (or JSON), [ESC] Cancel: ") prompting = True while prompting: c = self.stdscr.getch() time.sleep(0.01) if c == 105: # 105 i = ascii loadFormat = 'ascii' prompting = False elif c == 100: # 100 = d = dur loadFormat = 'dur' prompting = False elif c == 27: # 27 = esc = cancel self.clearStatusLine() prompting = False return None self.clearStatusLine() self.promptPrint("Enter file name to open: ") curses.echo() shortfile = self.stdscr.getstr().decode('utf-8') curses.noecho() if shortfile.replace(' ', '') == '': self.notify("File name cannot be empty.") return False self.clearStatusLine() if not self.loadFromFile(shortfile, loadFormat): return False self.undo = UndoManager(self, appState = self.appState) # reset undo system self.stdscr.redrawwin() self.stdscr.clear() #self.refresh() # so we can see the new ascii in memory. self.hardRefresh() # so we can see the new ascii in memory. def convertToCurrentFormat(self, fileColorMode = None): # should this and loadFromFile be in a # separate class for file operations? or into Movie() or Options() ? # then loadFromFile could return a movie object instead. """ If we load old .dur files, convert to the latest format """ # aka "fill in the blanks" #if self.appState.debug: self.notify(f"Converting to new format. Current format: {self.opts.saveFileFormat}") if self.opts.saveFileFormat < 3: # version 4 should rename saveFileFormat if self.appState.debug: self.notify(f"Upgrading to format 3. Making old color map.") # to saveFormatVersion # initialize color map for all frames: for frame in self.mov.frames: frame.initColorMap() self.opts.saveFileFormat = 3 if self.opts.saveFileFormat < 4: if self.appState.debug: self.notify(f"Upgrading to format 4. Adding delays.") # old file, needs delay times populated. for frame in self.mov.frames: frame.setDelayValue(0) self.opts.saveFileFormat = 4 if self.opts.saveFileFormat < 5: if self.appState.debug: self.notify(f"Upgrading to format 5. Making new color map.") for frame in self.mov.frames: frame.newColorMap = durmovie.convert_dict_colorMap(frame.colorMap, frame.sizeX, frame.sizeY) self.opts.saveFileFormat = 5 if self.opts.saveFileFormat < 6: if self.appState.debug: self.notify(f"Upgrading to format 6. Making new color map.") for frame in self.mov.frames: try: frame.height # for really old pickle files except: frame.height = frame.sizeY frame.width = frame.sizeY for line in range(0, frame.height): for col in range(0, frame.width): oldPair = frame.newColorMap[line][col] oldMode = self.appState.colorMode self.opts.saveFileFormat = 6 convertedColorMap = False if self.opts.saveFileFormat < 7: if self.appState.debug: self.notify(f"Upgrading to format 7. Converting color map.") if self.mov.contains_high_colors(): if self.appState.debug: self.notify(f"Old file format was 256 color.") self.ansi.convert_colormap(self.mov, dur_ansilib.legacy_256_to_256) else: if self.appState.colorMode == '16': if self.appState.debug: self.notify(f"Old file format was 16 color. Converting to new 16.") self.ansi.convert_colormap(self.mov, dur_ansilib.legacy_16_to_16) convertedColorMap = True else: if self.appState.debug: self.notify(f"Old file format was 16 color. Converting to new 256.") self.ansi.convert_colormap(self.mov, dur_ansilib.legacy_16_to_256) convertedColorMap = True self.opts.saveFileFormat = 7 if fileColorMode == "16" and self.appState.colorMode == "256" and convertedColorMap == False: for frame in self.mov.frames: # conert from 16 to 256 pallette for line in range(0, frame.height): for col in range(0, frame.width): if frame.newColorMap[line][col][0] == 1: # convert black color frame.newColorMap[line][col][0] = 16 frame.newColorMap[line][col][0] = frame.newColorMap[line][col][0] - 1 # convert rest of colors if frame.newColorMap[line][col][1] == 1: # convert black color frame.newColorMap[line][col][1] = 16 frame.newColorMap[line][col][1] = frame.newColorMap[line][col][1] - 1 # convert rest of colors self.opts.saveFileFormat = self.appState.durFileVer def loadFromFile(self, shortfile, loadFormat): # shortfile = non full path filename filename = os.path.expanduser(shortfile) if loadFormat == 'ascii': # or ANSI... try: if self.appState.debug2: self.notify("Trying to open() file as ascii.") f = open(filename, 'r') self.appState.curOpenFileName = os.path.basename(filename) shortpath = os.path.split(filename)[0] if len(os.path.split(shortpath)) > 1: shortpath = os.path.split(shortpath)[1] self.appState.fileShortPath = shortpath #self.appState.fileLongPath = fullpath except Exception as e: #if self.appState.debug: self.notify(f"self.opts = pickle.load(f)") self.notify(f"Could not open file for reading: {e}") return None # here we add the stuff to load the file into self.mov.currentFrame.content[][] self.undo.push() if self.appState.colorMode == '256': fg, bg = 7, 0 elif self.appState.colorMode == '16': fg, bg = 8, 0 lineNum = 0 colNum = 0 i = 0 try: raw_text = f.read() except UnicodeDecodeError: f.close() if self.appState.charEncoding == 'utf-8': if not self.appState.playOnlyMode and self.appState.debug: self.notify("This appears to be a CP437 ANSI/ASCII - Converting to Unicode.") f = open(filename, 'r', encoding='cp437') raw_text = f.read() # Load file into a new frame, make a new movie, #default_width= 80 # default with for ANSI file if filename[-4].lower() == ".diz" or filename.lower().endswith("file_id.ans"): default_width = 44 # default with for file_id.diz newFrame = dur_ansiparse.parse_ansi_escape_codes(raw_text, filename = filename, appState=self.appState, caller=self, debug=self.appState.debug, maxWidth=self.appState.wrapWidth) self.appState.topLine = 0 self.appState.firstCol = 0 newMovieOpts = Options(width=newFrame.width, height=newFrame.height) newMovie = Movie(newMovieOpts) # add the frame with the loaded ANSI file to the movie newMovie.addFrame(newFrame) newMovie.deleteCurrentFrame() # remove the blank first frame self.mov = newMovie f.close() #self.notify(f"From color map at 1, 1: {self.mov.currentFrame.newColorMap[1][1]}") #for x in range(lineNum, self.mov.sizeY): # clear out rest of contents. # self.mov.currentFrame.content[x] = list(" " * self.mov.sizeX) # If we're in the wrong color mode, switch modes and reload file. if self.appState.colorMode == "256": if not self.mov.contains_high_colors(): # if not using 256 colors #self.notify("Does not contain extended colors.") if self.mov.contains_background_colors(): # but using background colors... #self.notify("Contains background colors.") # Must be a 16 color ANSI. Switch since 256 can't do background colors. if self.appState.debug: self.notify(f"16 color file. Switching to 16 color mode and reloading file.") self.switchTo16ColorMode() self.loadFromFile(shortfile, 'ascii') # If drawing does contain high colors, and backgrounds... remove the backgrounds until 256 bg colors works. if self.mov.contains_high_colors(): if self.mov.contains_background_colors(): self.mov.strip_backgrounds() self.hardRefresh() elif loadFormat == 'dur': try: f = open(filename, 'rb') except Exception as e: self.notify(f"Could not open file for reading: {type(e)}: {e}") return None # check for gzipped file f.seek(0) fileHeader = f.read(2) if self.appState.debug2: self.notify(f"File header: {fileHeader.hex()}") if self.appState.debug2: self.notify(f"Checking for gzip file...") if fileHeader == b'\x1f\x8b': # gzip magic numbers if self.appState.debug2: self.notify(f"gzip found") # file == gzip compressed f.close() try: f = gzip.open(filename, 'rb') f.seek(0) if self.appState.debug2: self.notify(f"Un-gzipped successfully") except Exception as e: self.notify(f"Could not open file for reading as gzip: {type(e)}: {e}", pause=True) else: if self.appState.debug2: self.notify(f"gzip NOT found") #f.seek(0) # check for JSON Durdraw file #if (f.read(16) == b'\x7b\x0a\x20\x20\x22\x44\x75\x72\x64\x72\x61\x77\x20\x4d\x6f\x76'): # {. "Durdraw Mov if self.appState.debug2: self.notify(f"Checking for JSON file.") f.seek(0) if f.read(12) == b'\x7b\x0a\x20\x20\x22\x44\x75\x72\x4d\x6f\x76\x69': # {. "DurMov if self.appState.debug2: self.notify(f"JSON found. Loading JSON dur file.") f.seek(0) fileColorMode, fileCharEncoding = durfile.get_dur_file_colorMode_and_charMode(f) self.appState.fileColorMode = fileColorMode if fileColorMode == "256" and fileColorMode != self.appState.colorMode and self.appState.maxColors == 16: if not self.appState.playOnlyMode: self.notify(f"Loaded 256 color file in 16 color mode.") if fileColorMode == "256" and fileColorMode != self.appState.colorMode and self.appState.maxColors > 255: #self.notify(f"Warning: Loading a 256 color file in 16 color mode. Some colors may not be displayed.") if self.appState.debug: self.notify(f"256 color file. Switching to 256 color mode.") self.switchTo256ColorMode() self.loadFromFile(shortfile, 'dur') #if fileCharEncoding != self.appState.charEncoding: # self.notify(f"Warning: File uses {fileCharEncoding} character encoding, but Durdraw is in {self.appState.charEncoding} mode.") newMovie = durfile.open_json_dur_file(f, self.appState) self.opts = newMovie['opts'] self.mov = newMovie['mov'] self.setPlaybackRange(1, self.mov.frameCount) if self.appState.debug2: self.notify(f"{self.opts}") if self.appState.debug2: self.notify(f"Finished loading JSON dur file") self.appState.curOpenFileName = os.path.basename(filename) self.appState.modified = False #self.setWindowTitle(shortfile) self.convertToCurrentFormat(fileColorMode = fileColorMode) # Convert palettes as necessary if fileColorMode == "xterm-256" and fileColorMode != self.appState.colorMode: # Old file format, does not specify whether it's 16 or 256 colors. fileColorMode = "256" pass if fileColorMode == "16" and fileColorMode != self.appState.colorMode: #self.notify(f"Warning: Loading 16 color ANSI in {self.appState.colorMode} color mode will lose background colors.", pause=True) if self.appState.debug: self.notify(f"16 color file. Switching to 16 color mode.") self.switchTo16ColorMode() self.loadFromFile(shortfile, 'dur') self.hardRefresh() shortpath = os.path.split(filename)[0] if len(os.path.split(shortpath)) > 1: shortpath = os.path.split(shortpath)[1] self.appState.fileShortPath = shortpath #self.appState.fileLongPath = fullpath if self.appState.colorMode == "256": if self.mov.contains_background_colors(): self.mov.strip_backgrounds() # Remove un-printable characters, eg: errant \n from old .dur files, where enter accidentally inserted ^Ms. self.mov.strip_unprintable_characters() return True try: # Maybe it's a really old Pickle file... if self.appState.debug2: self.notify(f"Unpickling..") pickle_fail = False f.seek(0) unpickler = durfile.DurUnpickler(f) if self.appState.debug2: self.notify(f"self.opts = unpickler.load()") self.opts = unpickler.load() if self.appState.debug2: self.notify(f"self.mov = unpickler.load()") self.mov = unpickler.load() if self.appState.debug2: self.notify(f"self.appState.curOpenFileName = os.path.basename(filename)") self.appState.curOpenFileName = os.path.basename(filename) if self.appState.debug2: self.notify(f"self.appState.playbackRange = (1,self.mov.frameCount)") self.appState.playbackRange = (1,self.mov.frameCount) except Exception as e: pickle_fail = True if self.appState.debug2: self.notify(f"Exception in unpickling: {type(e)}: {e}") # If the first unpickling fails, try looking for another pickle format if pickle_fail: try: f.seek(0) if self.appState.debug2: self.notify(f"self.opts = pickle.load(f ") self.opts = pickle.load(f) if self.appState.debug2: self.notify(f"self.mov = pickle.load(f ") self.mov = pickle.load(f) self.appState.playbackRange = (1,self.mov.frameCount) pickle_fail = False except Exception as e: if self.appState.debug2: self.notify(f"Exception in unpickling other format: {type(e)}: {e}") pickle_fail = True if pickle_fail: # pickle is still failing loadFormat = 'ascii' # loading .dur format failed, so assume it's ascii instead. # change this to ANSI once ANSI file loading works, stripping out ^M in newlines # change this whole method to call loadDurFile(), loadAnsiFile(), # etc, checking for faiulre. self.convertToCurrentFormat() f.close() if loadFormat == 'ascii': # loading as dur failed, so load as ascii instead. self.loadFromFile(shortfile, loadFormat) self.appState.modified = False #self.setWindowTitle(shortfile) shortpath = os.path.split(filename)[0] if len(os.path.split(shortpath)) > 1: shortpath = os.path.split(shortpath)[1] self.appState.fileShortPath = shortpath #self.appState.fileLongPath = fullpath self.mov.gotoFrame(1) self.hardRefresh() def save(self): self.clearStatusLine() self.move(self.mov.sizeY, 0) self.promptPrint("File format? [D]UR, [A]NSI, A[N]SIMATION, ASCI[I], [M]IRC, [J]SON, [H]TML, [P]NG, [G]IF: ") self.stdscr.nodelay(0) # do not wait for input when calling getch prompting = True saved = False while prompting: c = self.stdscr.getch() time.sleep(0.01) if c in [105, 73]: # 105 i = ascii saveFormat = 'ascii' prompting = False elif c in [100, 68]: # 100 = d = dur, 68 = D saveFormat = 'dur' prompting = False elif c in [106, 74]: # 106 = j, 74 = J saveFormat = 'json' prompting = False elif c in [104, 63]: # 106 = h, 74 = H saveFormat = 'html' prompting = False elif c in [97, 65]: # a = ansi saveFormat = 'ansi' prompting = False elif c in [110, 78]: # 110 = n, 78 = N, ansimation saveFormat = 'ansimation' prompting = False elif c in [109]: # m = mIRC prompting = False if self.mov.contains_high_colors(): self.notify("Sorry, mIRC export only works for 16 colors, but this art contains extended colors.", pause=True) return None else: saveFormat = 'irc' elif c in [112, 80]: # p = png saveFormat = 'png' prompting = False elif c in [103, 71]: # g = gif saveFormat = 'gif' prompting = False elif c == 27: # 27 = esc = cancel self.clearStatusLine() prompting = False return None self.clearStatusLine() if saveFormat == 'ascii' or saveFormat == 'ansi' or saveFormat == 'ansimation': # ansi = escape codes for colors+ascii prompting = True while prompting: # Ask if they want CP437 or Utf-8 encoding self.clearStatusLine() self.promptPrint(f"Character encoding to save with? [C]P437, [U]tf-8? (default: {self.appState.charEncoding}): ") c = self.stdscr.getch() time.sleep(0.01) if c == ord('c'): encoding = "cp437" prompting = False elif c == ord('u'): encoding = "utf-8" prompting = False elif c in [13, curses.KEY_ENTER]: encoding = self.appState.charEncoding prompting = False elif c == 27: # 27 = esc = cancel self.notify("Canceled. File not saved.") prompting = False return False if saveFormat == 'ansimation': prompting = True repeat = False while prompting: # Ask if they want CP437 or Utf-8 encoding self.clearStatusLine() self.promptPrint("Repeat or loop animation? [no]: ") c = self.stdscr.getch() if c == ord('y'): repeat = True prompting = False elif c == ord('n'): repeat = False prompting = False elif c in [13, curses.KEY_ENTER]: repeat = False prompting = False elif c == 27: # 27 = esc = cancel self.notify("Canceled. File not saved.") prompting = False return False if repeat: self.clearStatusLine() self.promptPrint("Repeat how many times? ") curses.echo() repeat_string = str(self.stdscr.getstr().decode('utf-8')) curses.noecho() try: repeat = int(repeat_string) except ValueError: self.notify("Error: Repeat value must be an integer. File not saved.") return False if saveFormat in ['png', 'gif']: PIL_found = False try: import PIL PIL_found = True except ImportError: PIL_found = False if not PIL_found: self.notify("Error: PNG and GIF export requires the Pillow module for Python.") return False self.promptPrint("Which font? IBM PC [A]NSI, AM[I]GA: ") prompting = True while prompting: time.sleep(0.01) c = self.stdscr.getch() time.sleep(0.01) if c in [97, 65]: # a/A = ansi saveFont = 'ansi' prompting = False elif c in [105, 68]: # 105 i/I = amiga saveFont = 'amiga' prompting = False elif c == 27: # 27 = esc = cancel self.clearStatusLine() prompting = False return None if saveFont == 'amiga': self.clearStatusLine() self.promptPrint("Which amiga font? 1=topaz 2=b-strict, 3=microknight, 4=mosoul, 5=pot-noodle: ") prompting = True while prompting: c = self.stdscr.getch() time.sleep(0.01) if c == 49: # 1 = topaz saveFont = 'topaz' prompting = False if c == 50: # 2 = b-strict saveFont = 'b-strict' prompting = False if c == 51: # 3 = microknight saveFont = 'microknight' prompting = False if c == 52: # 4 = mosoul saveFont = 'mosoul' prompting = False if c == 53: # 5 = pot-noodle saveFont = 'pot-noodle' prompting = False elif c == 27: # 27 = esc = cancel self.clearStatusLine() prompting = False return None self.clearStatusLine() if saveFormat == "ansi" or saveFormat == "ansimation": self.promptPrint(f"Enter file name to save as ({saveFormat}/{encoding}) [{self.appState.curOpenFileName}]: ") else: self.promptPrint(f"Enter file name to save as ({saveFormat}) [{self.appState.curOpenFileName}]: ") curses.echo() filename = str(self.stdscr.getstr().decode('utf-8')) curses.noecho() if filename.replace(' ', '') == '': if saveFormat == "dur": filename = self.appState.curOpenFileName else: self.notify("File name cannot be empty.") return False try: filename = os.path.expanduser(filename) except: # possibly due to funk in filename, prevent possible crash self.promptPrint("There was an error saving to that file.") return False # If file exists.. ask if it should overwrite. if os.path.exists(filename): self.clearStatusLine() self.promptPrint(f"This file already exists. Overwrite? ") prompting = True while prompting: c = self.stdscr.getch() time.sleep(0.01) if c in [121, 89]: # y or Y prompting = False if c in [110, 78]: # n or N prompting = False self.notify("Canceled. File not saved.") return False if saveFormat == 'ascii': saved = self.saveAsciiFile(filename, encoding=encoding) if saveFormat == 'durOld': # dur Old = pickled python objects (ew) saved = self.saveDurFile(filename) if saveFormat == 'dur': # dur2 = serialized json gzipped saved = self.saveDur2File(filename) if saveFormat == 'json': # dur2 = serialized json, plaintext saved = self.saveDur2File(filename, gzipped=False) if saveFormat == 'html': # dur2 = serialized json, plaintext saved = self.saveHtmlFile(filename, gzipped=False) if saveFormat == 'ansi': # ansi = escape codes for colors+ascii saved = self.saveAnsiFile(filename, encoding=encoding) if saveFormat == 'ansimation': # ansi = escape codes for colors+ascii saved = self.saveAnsimationFile(filename, encoding=encoding, play_times=repeat) if saveFormat == 'irc': # ansi = escape codes for colors+ascii saved = self.saveAnsiFile(filename, ircColors = True) if saveFormat == 'png': # png = requires ansi love saved = self.savePngFile(filename, font=saveFont) if saveFormat == 'gif': # gif = requires PIL saved = self.saveGifFile(filename, font=saveFont) if saved: self.notify("*Saved* (Press any key to continue)", pause=True) self.appState.curOpenFileName = os.path.basename(filename) self.appState.modified = False self.undo.modifications = 0 #self.setWindowTitle(self.appState.curOpenFileName) elif not saved: self.notify("Save failed.") def saveDurFile(self, filename): # open and write file try: f = gzip.open(filename, 'wb') except: self.notify("Could not open file for writing. (Press any key to continue)", pause=True) return False pickle.dump(self.opts, f) pickle.dump(self.mov, f) f.close() return True def saveHtmlFile(self, filename, gzipped=False): # open and write file try: f = gzip.open(filename, 'w') except: self.notify("Could not open file for writing. (Press any key to continue)", pause=True) return False f.close() durfile.write_frame_to_html_file(self.mov, self.appState, self.mov.currentFrame, filename, gzipped=gzipped) return True def saveDur2File(self, filename, gzipped=True): # open and write file try: f = gzip.open(filename, 'w') except: self.notify("Could not open file for writing. (Press any key to continue)", pause=True) return False f.close() if gzipped: durfile.serialize_to_json_file(self.opts, self.appState, self.mov, filename) else: durfile.serialize_to_json_file(self.opts, self.appState, self.mov, filename, gzipped=False) return True def saveAsciiFile(self, filename, encoding = "default"): """ Saves to ascii file, strips trailing blank lines """ try: if encoding == "default": f = open(filename, 'w') elif encoding == "cp437": f = open(filename, 'w', encoding="cp437") elif encoding == "utf-8": f = open(filename, 'w', encoding="utf-8") except: self.notify("Could not open file for writing. (Press any key to continue)", pause=True) return False # rewrite this. rstrip(' ') looks cool, though. outBuffer = '\n'.join(''.join(line).rstrip(' ') for line in self.mov.currentFrame.content).rstrip('\n') try: f.write(outBuffer + '\n\n') except UnicodeEncodeError: self.notify(f"Error - some characters cannot be saved using encoding {encoding}. (Press any key to continue)", pause=True) f.close() return False f.close() return True def frameToAnsiCodes(self, frame): """ Takes a Durdraw frame, returns a string of escape codes which can be written to a file """ pass def saveAnsimationFile(self, filename, lastLineNum=False, lastColNum=False, firstColNum=False, firstLineNum=None, ircColors=False, encoding="default", play_times=False): """ Saves current frame of current movie to ansi file """ # some escape codes from https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences #print("\\033[;H or \\033[;f - Put the cursor at line L and column C.") #print("\\033[A - Move the cursor up N lines") #print("\\033[B - Move the cursor down N lines") #print("\\033[C - Move the cursor forward N columns") #print("\\033[D - Move the cursor backward N columns\n") #print("\\033[2J - Clear the screen, move to (0,0)") ESC_CLS = "\033[2J" ESC_TOPLEFT = "\033[0;0H" if play_times == False: play_times = 1 try: if encoding == "default": f = open(filename, 'w') elif encoding == "cp437": f = open(filename, 'w', encoding="cp437") elif encoding == "utf-8": f = open(filename, 'w', encoding="utf-8") except: self.notify("Could not open file for writing. (Press any key to continue)", pause=True) return False string = '' string = string + ESC_CLS string = string + ESC_TOPLEFT if not lastLineNum: # if we weren't told what lastLineNum is... # find it (last line we should save) lastLineNum = self.findFrameLastLine(self.mov.currentFrame) for frame in self.mov.frames: newLastLineNum = self.findFrameLastLine(frame) lastLineNum = max(newLastLineNum, lastLineNum) #if newLastLineNum > lastLineNum: # lastLineNum = newLastLineNum if not lastColNum: lastColNum = self.findFrameLastCol(self.mov.currentFrame) for frame in self.mov.frames: newLastColNum = self.findFrameLastCol(frame) lastColNum = max(newLastColNum, lastColNum) if not firstColNum: firstColNum = 0 # Don't crop leftmost blank columns if not firstLineNum: #firstLineNum = self.findFrameFirstLine(self.mov.currentFrame) firstLineNum = 0 for repeat_count in range(0, play_times): frameNum = 0 for frame in self.mov.frames: string = string + ESC_TOPLEFT for lineNum in range(firstLineNum, lastLineNum): # y == lines for colNum in range(firstColNum, lastColNum): char = self.mov.frames[frameNum].content[lineNum][colNum] color = self.mov.frames[frameNum].newColorMap[lineNum][colNum] colorFg = color[0] colorBg = color[1] if self.appState.colorMode == "256": if self.appState.showBgColorPicker == False: colorBg = 0 # black, I hope try: if ircColors: colorCode = self.ansi.getColorCodeIrc(colorFg,colorBg) else: if self.appState.colorMode == "256": colorCode = self.ansi.getColorCode256(colorFg,colorBg) else: colorCode = self.ansi.getColorCode(colorFg,colorBg) except KeyError: if ircColors: # problem? set a default color colorCode = self.ansi.getColorCodeIrc(1,0) else: colorCode = self.ansi.getColorCode(1,0) # If we don't have extended ncurses 6 color pairs, # we don't have background colors.. so write the background as black/0 string = string + colorCode + char if ircColors: string = string + '\n' else: string = string + '\r\n' frameNum += 1 try: f.write(string) saved = True except UnicodeEncodeError as encodeError: self.notify("Error: Some characters were not compatible with this encoding. File not saved.", pause=True) self.notify(f"{encodeError}", pause=True) saved = False #f.close() #return False if ircColors: string2 = string + '\n' else: f.write('\033[0m') # color attributes off string2 = string + '\r\n' # final CR+LF (DOS style newlines) f.close() #return True return saved def saveAnsiFile(self, filename, lastLineNum=False, lastColNum=False, firstColNum=False, firstLineNum=None, ircColors=False, encoding="default"): """ Saves current frame of current movie to ansi file """ try: if encoding == "default": f = open(filename, 'w') elif encoding == "cp437": f = open(filename, 'w', encoding="cp437") elif encoding == "utf-8": f = open(filename, 'w', encoding="utf-8") except: self.notify("Could not open file for writing. (Press any key to continue)", pause=True) return False string = [] if not lastLineNum: # if we weren't told what lastLineNum is... # find it (last line we should save) lastLineNum = self.findFrameLastLine(self.mov.currentFrame) if not lastColNum: lastColNum = self.findFrameLastCol(self.mov.currentFrame) if not firstColNum: firstColNum = 0 # Don't crop leftmost blank columns if not firstLineNum: #firstLineNum = self.findFrameFirstLine(self.mov.currentFrame) firstLineNum = 0 # also don't crop leading blank lines. rude error_encoding = False for lineNum in range(firstLineNum, lastLineNum): # y == lines if lineNum % 1000 == 0 or lineNum == lastLineNum-1: self.log.debug( 'writing ansi', {'lineNum': lineNum+1, 'total': lastLineNum, 'pct': round(((lineNum+1)/lastLineNum)*100, 2), 'colorMode': self.appState.colorMode} ) for colNum in range(firstColNum, lastColNum): char = self.mov.currentFrame.content[lineNum][colNum] color = self.mov.currentFrame.newColorMap[lineNum][colNum] colorFg = color[0] colorBg = color[1] if self.appState.colorMode == "256": if self.appState.showBgColorPicker == False: colorBg = 0 # black, I hope try: if ircColors: colorCode = self.ansi.getColorCodeIrc(colorFg,colorBg) else: if self.appState.colorMode == "256": colorCode = self.ansi.getColorCode256(colorFg,colorBg) else: colorCode = self.ansi.getColorCode(colorFg,colorBg) except KeyError: if ircColors: # problem? set a default color colorCode = self.ansi.getColorCodeIrc(1,0) else: colorCode = self.ansi.getColorCode(1,0) # Make sure encoding this character encodes correctly try: #char.encode('cp437') char.encode(encoding) except UnicodeEncodeError as encodeError: self.notify("Error: Some characters were not compatible with this encoding. File not saved.", pause=True) self.notify(f"line: {lineNum}, col: {colNum} ", pause=True) saved = False error_encoding = True break # If we don't have extended ncurses 6 color pairs, # we don't have background colors.. so write the background as black/0 string.append(colorCode + char) if ircColors: string.append('\n') else: string.append('\n') if not error_encoding: try: f.write(''.join(string)) saved = True except UnicodeEncodeError as encodeError: self.notify("Error: Some characters were not compatible with this encoding. File not saved.", pause=True) self.notify(f"{encodeError}", pause=True) saved = False #f.close() #return False f.write('\033[0m') # color attributes off f.close() #return True return saved def findLastMovieLine(self, movie): """ Cycle through the whole movie, figure out the lowest numbered line that == blank on all frames, return that #. Used to trim blank lines when saving. """ movieLastLine = 1 for frame in movie.frames: frameLastLine = self.findFrameLastLine(frame) if frameLastLine > movieLastLine: movieLastLine = frameLastLine return movieLastLine def findFirstMovieLine(self, movie): """ Cycle through the whole movie, figure out the highest numbered line that == blank on all frames, return that #. Used to trim blank lines when saving. """ movieFirstLine = movie.sizeY for frame in movie.frames: frameFirstLine = self.findFrameFirstLine(frame) if frameFirstLine < movieFirstLine: movieFirstLine = frameFirstLine return movieFirstLine def findFirstMovieCol(self, movie): """ Cycle through the whole movie, figure out the leftmost column that == blank on all frames, return that #. Used to trim blank lines when saving. """ movieFirstCol = movie.sizeX for frame in movie.frames: frameFirstCol = self.findFrameFirstCol(frame) if frameFirstCol < movieFirstCol: movieFirstCol = frameFirstCol return movieFirstCol def findLastMovieCol(self, movie): """ Cycle through the whole movie, figure out the rightmost column that == blank on all frames, return that #. Used to trim blank lines when saving. """ movieLastCol = 1 for frame in movie.frames: frameLastCol = self.findFrameLastCol(frame) if frameLastCol > movieLastCol: movieLastCol = frameLastCol return movieLastCol def findFrameFirstLine(self, frame): """ For the given frame, figure out the first non-blank line, return that # (aka, first used line of the frame) """ # start at the first line, work up until we find a character. for lineNum in range(0, frame.sizeY): for colNum in range(0, frame.sizeX): try: if not frame.content[lineNum][colNum] in [' ', '']: return lineNum # we found a non-empty character except Exception as E: pdb.set_trace() return 1 # blank frame, only save the first line. def findFrameLastLine(self, frame): """ For the given frame, figure out the last non-blank line, return that # + 1 (aka, last used line of the frame) """ # start at the last line, work up until we find a character. for lineNum in reversed(list(range(0, self.mov.sizeY))): #for colNum in range(0, frame.sizeX): for colNum in range(0, len(frame.content[lineNum])): if not frame.content[lineNum][colNum] in [' ', '']: return lineNum + 1 # we found a non-empty character return 1 # blank frame, only save the first line. def findFrameLastCol(self, frame): """ For the given frame, figure out the last non-blank column, return that # + 1 (aka, last used column of the frame) """ # start at the last column, work back until we find a character. #for colNum in reversed(list(range(0, frame.sizeX))): # for lineNum in reversed(list(range(0, frame.sizeY))): for colNum in reversed(range(0, self.mov.sizeX)): for lineNum in reversed(range(0, self.mov.sizeY)): try: if not frame.content[lineNum][colNum] in [' ', '']: return colNum + 1 # we found a non-empty character except Exception as E: return colNum - 1 #pdb.set_trace() return 1 # blank frame, only save the first line. def findFrameFirstCol(self, frame): """ For the given frame, figure out the first non-blank column, return that # (aka, first used column of the frame) """ # start at the first column, work forward until we find a character. for colNum in range(0, self.mov.sizeX): for lineNum in range(0, self.mov.sizeY): if not frame.content[lineNum][colNum] in [' ', '']: return colNum # we found a non-empty character return 1 # blank frame, only save the first line. def savePngFile(self, filename, lastLineNum=None, firstLineNum=None, firstColNum=None, lastColNum=None, font='ansi'): """ Save to ANSI then convert to PNG """ if not self.appState.isAppAvail("ansilove"): # ansilove not found self.notify("Ansilove not found in path. Please find it at https://www.ansilove.org/", pause=True) return False PIL_found = False try: import PIL PIL_found = True except ImportError: PIL_found = False if not PIL_found: self.notify("Error: PNG and GIF export requires the Pillow module for Python.") return False tmpAnsiFileName = filename + '.tmp.ans' # remove this file when done tmpPngFileName = filename + '.tmp.ans.png' # remove this file when done if not self.saveAnsiFile(tmpAnsiFileName, lastLineNum=lastLineNum, firstLineNum=firstLineNum, firstColNum=firstColNum, lastColNum=lastColNum, encoding="cp437"): self.notify("Saving ansi failed, make sure you can write to current directory.") return False devnull = open('/dev/null', 'w') if font == 'ansi': ansiLoveReturn = subprocess.call(['ansilove', '-c', str(self.mov.sizeX + 1), tmpAnsiFileName, tmpPngFileName], stdout=devnull) else: # amiga font ansiLoveReturn = subprocess.call(['ansilove', '-c', str(self.mov.sizeX + 1), tmpAnsiFileName, "-f", font], stdout=devnull) devnull.close() os.remove(tmpAnsiFileName) if ansiLoveReturn == 0: # this doesnt seem right either, as ansilove always pass # returns True. else: self.notify("Ansilove didn't return success.") return False # stop trying to save png # crop out rightmost blank space if not lastColNum: lastColNum = self.findFrameLastCol(self.mov.currentFrame) if not firstColNum: #firstColNum = self.findFrameFirstCol(self.mov.currentFrame) # ^ don't truncate the first columns. That's rude. firstColNum = 0 characterWidth = 8 # 9 pixels wide from PIL import Image cropImage = Image.open(tmpPngFileName) cropWidthPixels = (lastColNum - firstColNum) * characterWidth # right w,h = cropImage.size cropBox = (0, 0, cropWidthPixels, h) # should be right cropImage = cropImage.crop(cropBox) finalImage = open(filename, 'wb') try: cropImage.save(finalImage, "png") #self.notify("Saved successfully!") finalImage.close() except: self.notify("Error: Could not crop png.") os.remove(tmpPngFileName) return False os.remove(tmpPngFileName) return True def saveGifFile(self, filename, font="ansi"): try: # check for PIL/pillow import PIL except ImportError: self.notify("Error: Please install the PIL python module.") return False tmpPngNames = [] # switch to first frame self.mov.currentFrameNumber = 1 self.mov.currentFrame = self.mov.frames[self.mov.currentFrameNumber - 1] self.refresh() firstMovieLineNum = self.findFirstMovieLine(self.mov) # so we can trim lastMovieLineNum = self.findLastMovieLine(self.mov) firstMovieColNum = self.findFirstMovieCol(self.mov) lastMovieColNum = self.findLastMovieCol(self.mov) for num in range(1, self.mov.frameCount + 1): # then for each frame # make a temp png filename and add it to the list tmpPngName = filename + "." + str(num) + ".png" tmpPngNames.append(tmpPngName) # save the png if not self.savePngFile(tmpPngName, lastLineNum=lastMovieLineNum, firstLineNum=firstMovieLineNum, firstColNum=firstMovieColNum, lastColNum=lastMovieColNum, font=font): return False # If frame has a delay, copy saved file FPS times and add new # file names to tmpPngNames. if self.mov.currentFrame.delay > 0: numOfDelayFramesToAdd = int(self.mov.currentFrame.delay) * int(self.opts.framerate) for delayFrameNum in range(1,int(self.mov.currentFrame.delay) * \ int(self.opts.framerate)): # eg: 2 second delay, 8fps, means add 16 frames. delayFramePngName = tmpPngName + str(delayFrameNum) + ".png" shutil.copy(tmpPngName, delayFramePngName) tmpPngNames.append(delayFramePngName) # go to next frame self.mov.nextFrame() self.refresh() # open all the pngs so we can save them to gif from PIL import Image pngImages = [Image.open(fn) for fn in tmpPngNames] self.appState.sleep_time = (1000.0 / self.opts.framerate) / 1000.0 # or 0.1 == good, too self.pngsToGif(filename, pngImages, self.appState.sleep_time) for rmFile in tmpPngNames: os.remove(rmFile) return True def pngsToGif(self, outfile, pngImages, sleeptime): # create frames frames = [] for frame in pngImages: frames.append(frame) # Save into a GIF file that loops forever frames[0].save(outfile, format='GIF', append_images=frames[1:], save_all=True, duration=sleeptime * 1000, loop=0) def showHelp(self): self.stdscr.clear() if self.appState.hasHelpFile: #self.showAnimatedHelpScreen() #self.showAnimatedHelpScreen(page=2) self.showScrollingHelpScreen() else: self.showStaticHelp() def showStaticHelp(self): self.cursorOff() self.stdscr.nodelay(0) helpScreenText = ''' __ __ _| |__ __ _____ __| |_____ _____ __ __ __ / _ | | | __| _ | __| _ | | | |\\ /_____|_____|__|__|_____|__|___\\____|________| | Durr.... \\_____________________________________________\\| v %s alt-k - next frame alt-' - delete current line alt-j - prev frame alt-/ - insert line alt-n - iNsert current frame clone alt-, - delete current column. alt-N - appeNd empty frame alt-. - insert new column alt-p - start/stop Playback alt-c - Color picker (256 only) alt-d - Delete current frame alt-m - Menu alt-D - set current frame Delay F1-F10 - insert character alt-+/alt-- increase/decrease FPS alt-z - undo alt-M - Move current frame alt-r - Redo alt-up - next fg color alt-s - Save alt-down - prev fg color alt-o - Open alt-right - next bg color alt-q - Quit alt-left - prev bg color alt-h - Help alt-R - set playback/edit Range alt-pgdn - next character set alt-g - Go to frame # alt-pgup - prev character set Help file could not be found. You might want to reinstsall Durdraw... Can use ESC or META instead of ALT ''' % self.appState.durVer # remove the blank first line from helpScreenText.. # it's easier to edit here with the blank first line. helpScreenText = '\n'.join(helpScreenText.split('\n')[1:]) self.clearStatusLine() self.addstr(0, 0, helpScreenText) self.promptPrint("* Press the ANY key (or click) to continue *") self.stdscr.getch() self.stdscr.clear() self.cursorOn() if self.playing == True: self.stdscr.nodelay(1) def activate_heat_code(self): # hell's inferno if self.appState.inferno == None: inferno_fullpath = pathlib.Path(__file__).parent.joinpath("help/inferno.dur") self.appState.inferno, self.appState.inferno_opts = self.appState.loadDurFileToMov(inferno_fullpath) self.enterViewMode(mov=self.appState.inferno, opts=self.appState.inferno_opts) #self.notify("heat code activated") def hardRefresh(self): #self.stdscr.clear() self.stdscr.redrawwin() self.refresh() def drawFrame(self, refreshScreen=True, frame=None, topLine = 0, col_offset = 0, line_offset = 0, preview=False): # rename to redraw()? """ Like refresh() below, but only draws the frame passed in, not a whole movie. """ #topLine = self.appState.topLine topLine = 0 # Figure out the last line to draw lastLineToDraw = topLine + self.appState.realmaxY - 3 # right above the status line if lastLineToDraw > frame.sizeY: lastLineToDraw = frame.sizeY screenLineNum = 0 firstCol = self.appState.firstCol lastCol = min(frame.sizeX, self.appState.realmaxX + firstCol) # Draw each character for linenum in range(topLine, lastLineToDraw): line = frame.content[linenum] for colnum in range(firstCol, lastCol): charColor = frame.newColorMap[linenum][colnum] charContent = str(line[colnum]) try: # set ncurss color pair cursesColorPair = self.ansi.colorPairMap[tuple(charColor)] except: # Or if we can't, fail to the terminal's default color cursesColorPair = 0 if charColor[0] > 8 and charColor[0] <= 16 and self.appState.colorMode == "16": # bright color self.addstr(screenLineNum + line_offset, colnum - self.appState.firstCol + col_offset, charContent, curses.color_pair(cursesColorPair) | curses.A_BOLD) elif charColor[0] > 7 and charColor[0] <= 15 and self.appState.colorMode == "256": # bright color self.addstr(screenLineNum + line_offset, colnum - self.appState.firstCol + col_offset, charContent, curses.color_pair(cursesColorPair) | curses.A_BOLD) # If the mouse cursor is over Fg: 1 Bg:1 in 16 color mode, aka Black on Black # then print with defualt charaacters instead. This should prevent the cursor from # disappearing, as well as let you preview "invisible" text under the cursor. else: self.addstr(screenLineNum + line_offset, colnum - self.appState.firstCol + col_offset, charContent, curses.color_pair(cursesColorPair)) # draw border on right edge of line if not preview and self.appState.drawBorders and screenLineNum + topLine < self.frame.sizeY: self.addstr(screenLineNum, frame.sizeX, ": ", curses.color_pair(self.appState.theme['borderColor'])) screenLineNum += 1 # draw bottom border #if self.appState.drawBorders and screenLineNum < self.realmaxY - 3 : if not preview and self.appState.drawBorders and screenLineNum + topLine == frame.sizeY: if screenLineNum < self.statusBarLineNum: borderWidth = min(frame.sizeX, self.realmaxX) self.addstr(screenLineNum, 0, "." * borderWidth, curses.color_pair(self.appState.theme['borderColor'])) self.addstr(screenLineNum, frame.sizeX, ": ", curses.color_pair(self.appState.theme['borderColor'])) screenLineNum += 1 #spaceMultiplier = mov.sizeX + 1 spaceMultiplier = self.realmaxY #for x in range(screenLineNum, self.realmaxY - 2): # self.addstr(x, 0, " " * spaceMultiplier) #curses.panel.update_panels() if refreshScreen: self.stdscr.refresh() def refresh(self, refreshScreen=True, mov=None, col_offset = 0, line_offset = 0, preview=False, topLine=None): # rename to redraw()? """Refresh the screen""" if topLine == None: topLine = self.appState.topLine if self.appState.playingHelpScreen_2: mov = self.appState.helpMov_2 elif self.appState.playingHelpScreen: mov = self.appState.helpMov else: if mov == None: mov = self.mov # Figure out the last line to draw lastLineToDraw = topLine + self.appState.realmaxY - 2 # right above the status line if self.appState.playOnlyMode: lastLineToDraw += 2 if lastLineToDraw > mov.sizeY: lastLineToDraw = mov.sizeY screenLineNum = 0 firstCol = self.appState.firstCol lastCol = min(mov.sizeX, self.appState.realmaxX + firstCol) # Draw each character for linenum in range(topLine, lastLineToDraw): line = mov.currentFrame.content[linenum] for colnum in range(firstCol, lastCol): charColor = mov.currentFrame.newColorMap[linenum][colnum] charContent = str(line[colnum]) if self.appState.cursorMode == "Paint" and not self.playing and not self.appState.playingHelpScreen: if self.appState.brush != None: # draw brush preview # If we're drawing within the brush area: if linenum in range(self.appState.mouse_line + topLine, self.appState.mouse_line + self.appState.brush.sizeX + topLine): if colnum in range(self.appState.mouse_col + self.appState.firstCol, self.appState.mouse_col + self.appState.brush.sizeY + self.appState.firstCol): #brush_line = linenum - self.appState.mouse_line brush_line = linenum - self.appState.mouse_line - topLine brush_col = colnum - self.appState.mouse_col - self.appState.firstCol try: brushChar = self.appState.brush.content[brush_col][brush_line] except IndexError: # This should really never happen now. self.notify(f"Index error: bcol: {brush_col}, bline: {brush_line}, col: {colnum}, line: {linenum}, mcol: {self.appState.mouse_col}, {self.appState.mouse_line}", pause=False) brushChar = ' ' # invisible background for brushes if brushChar == ' ': pass else: # It's a character that we should draw as a brush preview if self.appState.renderMouseCursor: charContent = brushChar charColor = self.appState.brush.newColorMap[brush_col][brush_line] if linenum == self.appState.mouse_line + topLine and colnum == self.appState.mouse_col + self.appState.firstCol: if self.appState.cursorMode == "Draw" and not self.playing and not self.appState.playingHelpScreen: # Drawing preview instead if self.appState.renderMouseCursor: charContent = self.appState.drawChar charColor = [self.colorfg, self.colorbg] try: # set ncurss color pair cursesColorPair = self.ansi.colorPairMap[tuple(charColor)] except: # Or if we can't, fail to the terminal's default color cursesColorPair = 0 injecting = False if self.appState.can_inject and self.appState.colorMode == "256": injecting = True if charColor[0] > 8 and charColor[0] <= 16 and self.appState.colorMode == "16": # bright color self.addstr(screenLineNum + line_offset, colnum - self.appState.firstCol + col_offset, charContent, curses.color_pair(cursesColorPair) | curses.A_BOLD) elif charColor[0] > 7 and charColor[0] <= 15 and self.appState.colorMode == "256": # bright color # Let's try injecting! if injecting: moveCode = f"\x1b[{screenLineNum + 1};{colnum - self.appState.firstCol + 1}H" colorCode = self.ansi.getColorCode256(charColor[0],charColor[1]) sys.stdout.write(moveCode) sys.stdout.write(colorCode) sys.stdout.write(charContent) pass #self.addstr(screenLineNum, colnum - self.appState.firstCol, charContent) else: self.addstr(screenLineNum + line_offset, colnum - self.appState.firstCol + col_offset, charContent, curses.color_pair(cursesColorPair) | curses.A_BOLD) # If the mouse cursor is over Fg: 1 Bg:1 in 16 color mode, aka Black on Black # then print with defualt charaacters instead. This should prevent the cursor from # disappearing, as well as let you preview "invisible" text under the cursor. elif not self.appState.playOnlyMode and colnum + 1 == self.xy[1] and linenum == self.xy[0]: # under the cursor if self.appState.colorMode == "16": visible_color_pair = self.ansi.colorPairMap[(self.appState.defaultFgColor, self.appState.defaultBgColor)] #self.addstr(screenLineNum, colnum, "X", visible_color_pair) # black on black if charColor[0] == 1 and charColor[1] == 0 or \ charColor[0] == 1 and charColor[1] == 8: # make it show self.addstr(screenLineNum, colnum, charContent, visible_color_pair) # not black on black else: # 16 color Normal character, under the cursor. No funny business. Print to the screen self.addstr(screenLineNum, colnum - self.appState.firstCol, charContent, curses.color_pair(cursesColorPair)) else: # 256 color Normal character, under the cursor. No funny business. Print to the screen self.addstr(screenLineNum, colnum - self.appState.firstCol, charContent, curses.color_pair(cursesColorPair)) else: # Normal character. No funny business. Print to the screen #injecting = True if injecting and charColor[1] != 0: #self.stdscr.move(screenLineNum, colnum - self.appState.firstCol) #self.stdscr.refresh() moveCode = f"\x1b[{screenLineNum + 1};{colnum - self.appState.firstCol + 1}H" colorCode = self.ansi.getColorCode256(charColor[0],charColor[1]) #sys.stdout.write(f"\x1b[38:5:{charColor[0]}m") # FG sys.stdout.write(moveCode) sys.stdout.write(colorCode) #sys.stdout.write(f"\x1b[48:5:{charColor[1]}m") # BG sys.stdout.write(charContent) #time.sleep(0.001) #self.stdscr.refresh() #self.notify("Injected", wait_time=40) #sys.stdout.write("\x1b[48:5:46m") #sys.stdout.write("\x1b[38:5:19m") #self.addstr(screenLineNum, colnum - self.appState.firstCol, f"{charContent}") pass #self.addstr(screenLineNum, colnum - self.appState.firstCol, charContent) else: self.addstr(screenLineNum + line_offset, colnum - self.appState.firstCol + col_offset, charContent, curses.color_pair(cursesColorPair)) # draw border on right edge of line if not preview and self.appState.drawBorders and screenLineNum + topLine < self.mov.sizeY: self.addstr(screenLineNum, mov.sizeX, ": ", curses.color_pair(self.appState.theme['borderColor'])) screenLineNum += 1 # draw bottom border #if self.appState.drawBorders and screenLineNum < self.realmaxY - 3 : if not preview and self.appState.drawBorders and screenLineNum + topLine == self.mov.sizeY: if screenLineNum < self.statusBarLineNum: borderWidth = min(mov.sizeX, self.realmaxX) self.addstr(screenLineNum, 0, "." * borderWidth, curses.color_pair(self.appState.theme['borderColor'])) self.addstr(screenLineNum, mov.sizeX, ": ", curses.color_pair(self.appState.theme['borderColor'])) self.addstr(screenLineNum + 1, 0, " " * borderWidth, curses.color_pair(self.appState.theme['borderColor'])) self.addstr(screenLineNum + 1, mov.sizeX, " ", curses.color_pair(self.appState.theme['borderColor'])) screenLineNum += 1 #spaceMultiplier = mov.sizeX + 1 spaceMultiplier = self.realmaxY for x in range(screenLineNum, self.realmaxY - 2): self.addstr(x, 0, " " * spaceMultiplier) if not preview: curses.panel.update_panels() if self.appState.playingHelpScreen: self.addstr(self.statusBarLineNum + 1, 0, "Up/Down, Pgup/Pgdown, Home/End or Mouse Wheel to scroll. Enter or Esc to exit.", curses.color_pair(self.appState.theme['promptColor'])) if refreshScreen: self.stdscr.refresh() def addColToCanvas(self): self.undo.push() # window is big enough fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor for frameNum in range(0, len(self.mov.frames)): for x in range(len(self.mov.frames[frameNum].content)): self.mov.frames[frameNum].content[x].insert(self.xy[1] - 1, ' ') self.mov.frames[frameNum].newColorMap[x].insert(self.xy[1] - 1, [fg,bg]) self.mov.frames[frameNum].sizeX += 1 self.mov.sizeX += 1 #self.mov.currentFrame.sizeX += 1 self.opts.sizeX += 1 self.hardRefresh() def delColFromCanvas(self): if self.mov.sizeX > 1 and self.xy[1] <= self.mov.sizeX: self.undo.push() for frameNum in range(0, len(self.mov.frames)): # Pop current column from every for x in range(len(self.mov.frames[frameNum].content)): # line self.mov.frames[frameNum].content[x].pop(self.xy[1] - 1) self.mov.frames[frameNum].newColorMap[x].pop(self.xy[1] - 1) self.mov.frames[frameNum].sizeX -= 1 self.mov.sizeX -= 1 #self.mov.currentFrame.sizeX -= 1 self.opts.sizeX -= 1 self.hardRefresh() if self.xy[1] == self.mov.sizeX + 1: self.move_cursor_left() self.hardRefresh() def addLineToCanvas(self): self.undo.push() fg = self.appState.defaultFgColor bg = self.appState.defaultBgColor for frameNum in range(0, len(self.mov.frames)): self.mov.frames[frameNum].content.insert(self.xy[0], list(' ' * self.mov.sizeX)) self.mov.frames[frameNum].newColorMap.insert(self.xy[0], [[fg,bg]] * self.mov.sizeX) self.mov.frames[frameNum].sizeY += 1 self.mov.sizeY += 1 #self.mov.currentFrame.sizeY += 1 self.opts.sizeY += 1 def delLineFromCanvas(self): if self.mov.sizeY > 1 and self.xy[0] != self.mov.sizeY: self.undo.push() for frameNum in range(0, len(self.mov.frames)): self.mov.frames[frameNum].content.pop(self.xy[0]) self.mov.frames[frameNum].newColorMap.pop(self.xy[0]) self.mov.frames[frameNum].sizeY -= 1 self.mov.sizeY -= 1 #self.mov.currentFrame.sizeY -= 1 self.opts.sizeY -= 1 self.hardRefresh() if self.xy[0] == self.mov.sizeY: # We're on the last line, and just deleted it. self.move_cursor_up() def addCol(self, frange=None): """Insert column at position of cursor""" fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor self.undo.push() if frange: # framge range for frameNum in range(frange[0] - 1, frange[1]): for x in range(len(self.mov.frames[frameNum].content)): self.mov.frames[frameNum].content[x].insert(self.xy[1] - 1, ' ') self.mov.frames[frameNum].content[x].pop() self.mov.frames[frameNum].newColorMap[x].insert(self.xy[1] - 1, [fg,bg]) self.mov.frames[frameNum].newColorMap[x].pop() else: for x in range(len(self.mov.currentFrame.content)): self.mov.currentFrame.content[x].insert(self.xy[1] - 1, ' ') self.mov.currentFrame.content[x].pop() self.mov.currentFrame.newColorMap[x].insert(self.xy[1] - 1, [fg,bg]) self.mov.currentFrame.newColorMap[x].pop() # insert bit here to shift color map to the right from the column # onward. how: start at top right character, work down to bottom # copying the color from the character to the left. # Then move left a column and repeat, etc, until you're at self.xy[1] -1 :). self.refresh() def delCol(self, frange=None): """Erase column at position of cursor""" self.undo.push() fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor if frange: # framge range for frameNum in range(frange[0] - 1, frange[1]): for x in range(len(self.mov.frames[frameNum].content)): # Pop current column from every self.mov.frames[frameNum].content[x].pop(self.xy[1] - 1) # line & add a blank self.mov.frames[frameNum].content[x].append(' ') # at the end of each line. self.mov.frames[frameNum].newColorMap[x].pop(self.xy[1] - 1) # line & add a blank self.mov.frames[frameNum].newColorMap[x].append([fg,bg]) # at the end of each line. else: for x in range(len(self.mov.currentFrame.content)): # Pop current column from every self.mov.currentFrame.content[x].pop(self.xy[1] - 1) # line & add a blank self.mov.currentFrame.content[x].append(' ') # at the end of each line. self.mov.currentFrame.newColorMap[x].pop(self.xy[1] - 1) # line & add a blank self.mov.currentFrame.newColorMap[x].append([fg,bg]) # at the end of each line. self.hardRefresh() def delLine(self, frange=None): """delete current line""" self.undo.push() fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor if frange: for frameNum in range(frange[0] - 1, frange[1]): self.mov.frames[frameNum].content.pop(self.xy[0]) self.mov.frames[frameNum].content.append([]) self.mov.frames[frameNum].content[len(self.mov.frames[frameNum].content) - 1] = list(' ' * self.mov.sizeX) self.mov.frames[frameNum].newColorMap.pop(self.xy[0]) self.mov.frames[frameNum].newColorMap.append([]) self.mov.frames[frameNum].newColorMap[len(self.mov.frames[frameNum].newColorMap) - 1] = [[fg,bg]] * self.mov.sizeX else: self.mov.currentFrame.content.pop(self.xy[0]) self.mov.currentFrame.content.append([]) self.mov.currentFrame.content[len(self.mov.currentFrame.content) - 1] = list(' ' * self.mov.sizeX) self.mov.currentFrame.newColorMap.pop(self.xy[0]) self.mov.currentFrame.newColorMap.append([]) self.mov.currentFrame.newColorMap[len(self.mov.currentFrame.newColorMap) - 1] = [[fg,bg]] * self.mov.sizeX self.hardRefresh() def addLine(self, frange=None): """Insert new line""" fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor self.undo.push() if frange: for frameNum in range(frange[0] - 1, frange[1]): self.mov.frames[frameNum].content.insert(self.xy[0], list(' ' * self.mov.sizeX)) self.mov.frames[frameNum].content.pop() self.mov.frames[frameNum].newColorMap.insert(self.xy[0], [[fg,bg]] * self.mov.sizeX) self.mov.frames[frameNum].newColorMap.pop() self.refresh() else: self.mov.currentFrame.content.insert(self.xy[0], list(' ' * self.mov.sizeX)) self.mov.currentFrame.content.pop() self.mov.currentFrame.newColorMap.insert(self.xy[0], [[fg,bg]] * self.mov.sizeX) self.mov.currentFrame.newColorMap.pop() self.refresh() def startSelecting(self, firstkey=None, mouse=False): # firstkey is the key the user was #pressing. left, right, etc """Mark selection for copy/cut/move - trigger with shift-arrow-keys""" # any other key returns (cancels) # print message: "Select mode - Enter to select, Esc to cancel" self.undo.push() startPoint = [self.xy[0], self.xy[1]] # set to wherever the cursor is endPoint = startPoint selecting = True self.stdscr.nodelay(0) # wait for getch input c = firstkey self.clearStatusBar() #self.addstr(self.statusBarLineNum, 0, f"Use arrow keys to make selection, enter when done.") while selecting: endPoint = [self.xy[0], self.xy[1]] # set to wherever the cursor is self.refresh() self.addstr(self.statusBarLineNum + 1, 0, f"Use arrow keys to make selection, enter when done.") # draw block area on top of drawing area mov = self.mov if endPoint[0] >= startPoint[0]: # if we're moving right of start point firstLineNum = startPoint[0] lastLineNum = endPoint[0] else: # otherwise, we're moving left lastLineNum = startPoint[0] firstLineNum = endPoint[0] if endPoint[1] >= startPoint[1]: # we're moving down from start point firstColNum = startPoint[1] lastColNum = endPoint[1] else: # we're moving up from start point lastColNum = startPoint[1] firstColNum = endPoint[1] # draw selected area inverse for linenum in range(firstLineNum, lastLineNum + 1): for colnum in range(firstColNum - 1, lastColNum): #if colnum == self.mov.sizeX - 1: # prevent overflow on last line # colnum -= 1 try: charColor = mov.currentFrame.newColorMap[linenum][colnum] except Exception as E: self.notify(f"Exception E: {E}") linenum = linenum - 1 charColor = mov.currentFrame.newColorMap[linenum][colnum] #pdb.set_trace() try: # set ncurss color pair cursesColorPair = self.ansi.colorPairMap[tuple(charColor)] except: # Or if we can't, fail to the terminal's default color cursesColorPair = 0 self.addstr(linenum - self.appState.topLine, colnum - self.appState.firstCol, mov.currentFrame.content[linenum][colnum], curses.color_pair(cursesColorPair) | curses.A_REVERSE) width = lastColNum - firstColNum + 1 height = lastLineNum - firstLineNum + 1 # end draw block area #self.stdscr.redrawwin() c = self.stdscr.getch() if c in [98, curses.KEY_LEFT, curses.KEY_SLEFT]: self.move_cursor_left() elif c in [98, curses.KEY_RIGHT, curses.KEY_SRIGHT]: #if colnum == self.mov.sizeX - 1: # prevent overflow on last line self.move_cursor_right() # 337 and 520 - shift-up, 336 and 513 = shift-down elif c in [339, curses.KEY_PPAGE]: # page up self.move_cursor_pgup() elif c in [338, curses.KEY_NPAGE]: # page down self.move_cursor_pgdown() elif c in [98, curses.KEY_UP, 337, 520]: self.move_cursor_up() elif c in [98, curses.KEY_DOWN, 336, 513]: self.move_cursor_down() elif c in [339, curses.KEY_HOME]: # 339 = home self.xy[1] = 1 elif c in [338, curses.KEY_END]: # 338 = end self.xy[1] = self.mov.sizeX - 1 elif c in [13, curses.KEY_ENTER]: # Ask user what operation they want, and then do it on the selected area # copy, cut, fill, or copy into all frames :) prompting = True self.clearStatusBar() self.promptPrint("[C]opy, Cu[t], [D]elete, [F]ill, Co[l]or, Flip [X/Y], New [B]rush, copy to [A]ll Frames in range? " ) while prompting: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['c', 'C']: # Copy self.copySegmentToClipboard([firstLineNum, firstColNum], height, width) prompting = False if chr(prompt_ch) in ['b', 'B']: # Make Brush self.copySegmentToBrush([firstLineNum, firstColNum], height, width) prompting = False #if chr(prompt_ch) in ['m', 'M']: # move # prompting = False elif chr(prompt_ch) in ['x', 'X']: # flip horizontally self.flipSegmentHorizontal([firstLineNum, firstColNum], height, width) #prompting = False self.refresh() elif chr(prompt_ch) in ['y', 'Y']: # flip vertically self.flipSegmentVertical([firstLineNum, firstColNum], height, width) #prompting = False self.refresh() # self.undo.push() # self.mov.currentFrame.flip_horizontal() if chr(prompt_ch) in ['t', 'T']: # Cut to clipboard self.clearStatusBar() if self.mov.hasMultipleFrames(): self.promptPrint("Cut across all frames in playback range (Y/N)? ") askingAboutRange = True else: self.copySegmentToClipboard([firstLineNum, firstColNum], height, width) self.undo.push() self.deleteSegment([firstLineNum, firstColNum], height, width) askingAboutRange = False while askingAboutRange: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['y', 'Y']: # yes, all range self.copySegmentToClipboard([firstLineNum, firstColNum], height, width) self.undo.push() self.deleteSegment([firstLineNum, firstColNum], height, width, frange=self.appState.playbackRange) askingAboutRange = False if chr(prompt_ch) in ['n', 'N']: # No, only one frame self.copySegmentToClipboard([firstLineNum, firstColNum], height, width) self.undo.push() self.deleteSegment([firstLineNum, firstColNum], height, width) askingAboutRange = False elif prompt_ch == 27: # esc, cancel askingAboutRange = False prompting = False elif chr(prompt_ch) in ['d', 'D']: # delete/clear self.clearStatusBar() if self.mov.hasMultipleFrames(): self.promptPrint("Delete across all frames in playback range (Y/N)? ") askingAboutRange = True else: self.undo.push() self.deleteSegment([firstLineNum, firstColNum], height, width) askingAboutRange = False while askingAboutRange: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['y', 'Y']: # yes, all range self.undo.push() self.deleteSegment([firstLineNum, firstColNum], height, width, frange=self.appState.playbackRange) askingAboutRange = False if chr(prompt_ch) in ['n', 'N']: # yes, all range self.undo.push() self.deleteSegment([firstLineNum, firstColNum], height, width) askingAboutRange = False elif prompt_ch == 27: # esc, cancel askingAboutRange = False prompting = False elif chr(prompt_ch) in ['l', 'L']: # color self.clearStatusBar() if self.mov.hasMultipleFrames(): self.promptPrint("Color across all frames in playback range (Y/N)? ") askingAboutRange = True else: self.undo.push() self.colorSegment([firstLineNum, firstColNum], height, width) askingAboutRange = False while askingAboutRange: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['y', 'Y']: # yes, all range self.undo.push() self.colorSegment([firstLineNum, firstColNum], height, width, frange=self.appState.playbackRange) askingAboutRange = False if chr(prompt_ch) in ['n', 'N']: # yes, all range self.undo.push() self.colorSegment([firstLineNum, firstColNum], height, width) askingAboutRange = False elif prompt_ch == 27: # esc, cancel askingAboutRange = False prompting = False elif chr(prompt_ch) in ['f', 'F']: # fill self.clearStatusBar() self.promptPrint(f"Enter fill character, or press enter for {self.appState.drawChar}: ") askingAboutChar = True canceled = False drawChar = 'X' prompt_ch = self.stdscr.getch() if prompt_ch == 27: # esc, cancel canceled = True elif prompt_ch in [13, curses.KEY_ENTER]: drawChar = self.appState.drawChar elif prompt_ch in [curses.KEY_F1]: drawChar = chr(self.chMap['f1']) elif prompt_ch in [curses.KEY_F2]: drawChar = chr(self.chMap['f2']) elif prompt_ch in [curses.KEY_F3]: drawChar = chr(self.chMap['f3']) elif prompt_ch in [curses.KEY_F4]: drawChar = chr(self.chMap['f4']) elif prompt_ch in [curses.KEY_F5]: drawChar = chr(self.chMap['f5']) elif prompt_ch in [curses.KEY_F6]: drawChar = chr(self.chMap['f6']) elif prompt_ch in [curses.KEY_F7]: drawChar = chr(self.chMap['f7']) elif prompt_ch in [curses.KEY_F8]: drawChar = chr(self.chMap['f8']) elif prompt_ch in [curses.KEY_F9]: drawChar = chr(self.chMap['f9']) elif prompt_ch in [curses.KEY_F10]: drawChar = chr(self.chMap['f10']) else: drawChar = chr(prompt_ch) if canceled: askingAboutRange = False prompting = False else: self.clearStatusBar() if self.mov.hasMultipleFrames(): self.promptPrint("Fill across all frames in playback range (Y/N)? ") askingAboutRange = True else: # Just one frame, so don't worry about the range. self.undo.push() self.fillSegment([firstLineNum, firstColNum], height, width, fillChar=drawChar) askingAboutRange = False while askingAboutRange: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['y', 'Y']: # yes, all range self.undo.push() self.fillSegment([firstLineNum, firstColNum], height, width, frange=self.appState.playbackRange, fillChar=drawChar) askingAboutRange = False if chr(prompt_ch) in ['n', 'N']: # yes, all range self.undo.push() self.fillSegment([firstLineNum, firstColNum], height, width, fillChar=drawChar) askingAboutRange = False elif prompt_ch == 27: # esc, cancel askingAboutRange = False prompting = False elif chr(prompt_ch) in ['a', 'A']: # copy to all frames self.copySegmentToAllFrames([firstLineNum, firstColNum], height, width, frange=self.appState.playbackRange) prompting = False elif prompt_ch == 27: # esc, cancel self.undo.undo() prompting = False elif prompt_ch in [13, curses.KEY_ENTER]: # enter # Confirm. Don't pop the clipboard like esc does. prompting = False selecting = False elif c == curses.KEY_MOUSE: try: _, mouseX, mouseY, _, mouseState = curses.getmouse() except: pass realmaxY,realmaxX = self.realstdscr.getmaxyx() # enable mouse tracking only when the button is pressed if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 try: curses.BUTTON5_PRESSED except: curses.BUTTON5_PRESSED = 0 try: curses.BUTTON4_PRESSED except: curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX \ and mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum: # We're in in edit/canvas area self.move_cursor_up() elif mouseState & curses.BUTTON5_PRESSED: # wheel down if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX \ and mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum: # We're in in edit/canvas area self.move_cursor_down() if mouseState == curses.BUTTON1_CLICKED or mouseState & curses.BUTTON_SHIFT: if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX \ and mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum: # We're in in edit/canvas area self.xy[1] = mouseX + 1 + self.appState.firstCol # set cursor position self.xy[0] = mouseY + self.appState.topLine elif c == 27: # esc selecting = False if self.playing: self.stdscr.nodelay(1) else: self.stdscr.nodelay(0) def pasteFromMenu(self): if self.clipBoard: # If there is something in the clipboard self.askHowToPaste() def askHowToPaste(self): self.clearStatusBar() if self.mov.hasMultipleFrames(): self.promptPrint("Paste across all frames in playback range (Y/N)? ") askingAboutRange = True else: # only one frame self.undo.push() self.pasteFromClipboard() askingAboutRange = False while askingAboutRange: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['y', 'Y']: # yes, all range self.undo.push() self.pasteFromClipboard(frange=self.appState.playbackRange) askingAboutRange = False if chr(prompt_ch) in ['n', 'N']: # no, single frame only self.undo.push() self.pasteFromClipboard() askingAboutRange = False elif prompt_ch == 27: # esc, cancel askingAboutRange = False prompting = False def pasteFromClipboard(self, startPoint=None, clipBuffer=None, frange=None, transparent=False, pushUndo=True): if not clipBuffer: clipBuffer = self.clipBoard if not clipBuffer: # clipboard is empty, and no buffer provided return False if pushUndo: self.undo.push() if not startPoint: startPoint = self.xy lineNum = 0 colNum = 0 #width = len(clipBuffer.content) - 1 width = len(clipBuffer.content) #height = len(clipBuffer.content[0]) - 1 height = len(clipBuffer.content[0]) for lineNum in range(0, height): for colNum in range(0, width): charColumn = startPoint[1] + colNum charLine = startPoint[0] + lineNum character = ord(clipBuffer.content[colNum][lineNum]) cursesColorPair = clipBuffer.newColorMap[colNum][lineNum] charFg = cursesColorPair[0] charBg = cursesColorPair[1] if charColumn < self.mov.sizeX + 1 and charLine < self.mov.sizeY: if not frange: if transparent: if chr(character) == ' ' and charBg == 0: pass else: self.insertChar(character, fg=charFg, bg=charBg, x=charColumn, y=charLine, pushUndo=False) else: self.insertChar(character, fg=charFg, bg=charBg, x=charColumn, y=charLine, pushUndo=False) else: if transparent: if chr(character) == ' ' and charBg == 0: pass else: self.insertChar(character, fg=charFg, bg=charBg, x=charColumn, y=charLine, pushUndo=False, frange=frange) else: self.insertChar(character, fg=charFg, bg=charBg, x=charColumn, y=charLine, pushUndo=False, frange=frange) def copySegmentToClipboard(self, startPoint, height, width): """ startPoint is [line, column] """ clipBoard = self.copySegmentToBuffer(startPoint, height, width) self.clipBoard = clipBoard def copySegmentToBrush(self, startPoint, height, width): """ startPoint is [line, column] """ newBrush = self.copySegmentToBuffer(startPoint, height, width) self.appState.brush = newBrush def copySegmentToAllFrames(self, startPoint, height, width, frange=None): self.undo.push() tempFrame = self.copySegmentToBuffer(startPoint, height, width) # paste into each frame in range, at startPoint self.pasteFromClipboard(clipBuffer=tempFrame, startPoint=startPoint, frange=frange) def copySegmentToBuffer(self, startPoint, height, width): # Return a buffer, aka a frame or movie object # Buffer can be put into clipboard, or used to move # around screen, e, etc. firstLineNum = startPoint[0] firstColNum = startPoint[1] lastLineNum = firstLineNum + height lastColNum = firstColNum + width # Make a buffer for characters and color pairs big enough to store the # copied image segment. # This can be done by making a frame. bufferFrame = durmovie.Frame(height, width) newLineNum = 0 newColNum = 0 # For each character in the selection... for linenum in range(firstLineNum, lastLineNum): for colnum in range(firstColNum - 1, lastColNum - 1): # copy color from movie into buffer charColor = self.mov.currentFrame.newColorMap[linenum][colnum] try: bufferFrame.newColorMap[newColNum][newLineNum] = charColor except Exception as E: print(f"Exception: {str(E)}") pdb.set_trace() # copy character from movie into buffer output_char = self.mov.currentFrame.content[linenum][colnum] try: bufferFrame.content[newColNum][newLineNum] = output_char except Exception as E: print(f"Exception: {str(E)}") pdb.set_trace() newColNum += 1 newLineNum += 1 newColNum = 0 return bufferFrame def flipSegmentVertical(self, startPoint, height, width, frange=None): """ Flip the contents horizontally in the current frame, or framge range """ #self.undo.push() # make a reverse copy segment = self.copySegmentToBuffer(startPoint, height, width) segment.flip_vertical() # replace with our copy self.pasteFromClipboard(clipBuffer = segment, frange=frange, startPoint=startPoint) #self.pasteFromClipboard(frange=self.appState.playbackRange) def flipSegmentHorizontal(self, startPoint, height, width, frange=None): """ Flip the contents horizontally in the current frame, or framge range """ #self.undo.push() # make a reverse copy segment = self.copySegmentToBuffer(startPoint, height, width) segment.flip_horizontal() # replace with our copy self.pasteFromClipboard(clipBuffer = segment, frange=frange, startPoint=startPoint) #self.pasteFromClipboard(frange=self.appState.playbackRange) def deleteSegment(self, startPoint, height, width, frange=None): """ Delete everyting in the current frame, or framge range """ self.undo.push() for lineNum in range(0, height): for colNum in range(0, width): charColumn = startPoint[1] + colNum charLine = startPoint[0] + lineNum character = ord(" ") charFg = self.appState.defaultFgColor charBg = self.appState.defaultBgColor if charColumn < self.mov.sizeX + 1 and charLine < self.mov.sizeY: if not frange: self.insertChar(character, fg=charFg, bg=charBg, x=charColumn, y=charLine, pushUndo=False) else: self.insertChar(character, fg=charFg, bg=charBg, x=charColumn, y=charLine, pushUndo=False, frange=frange) def fillSegment(self, startPoint, height, width, frange=None, fillChar="X"): """ Fill everyting in the current frame, or framge range, with selected character+color """ self.undo.push() for lineNum in range(0, height): for colNum in range(0, width): charColumn = startPoint[1] + colNum charLine = startPoint[0] + lineNum character = ord(fillChar) charFg = self.colorfg charBg = self.colorbg if charColumn < self.mov.sizeX + 1 and charLine < self.mov.sizeY: if not frange: self.insertChar(character, fg=charFg, bg=charBg, x=charColumn, y=charLine, pushUndo=False) else: self.insertChar(character, fg=charFg, bg=charBg, x=charColumn, y=charLine, pushUndo=False, frange=frange) def colorSegment(self, startPoint, height, width, frange=None): """ Color everyting in the current frame, or framge range, with selected color """ self.undo.push() for lineNum in range(0, height): for colNum in range(0, width): charColumn = startPoint[1] + colNum charLine = startPoint[0] + lineNum if charColumn < self.mov.sizeX + 1 and charLine < self.mov.sizeY: if not frange: #self.insertChar(character, fg=charFg, bg=charBg, x=charColumn, y=charLine, pushUndo=False) self.insertColor(fg=self.colorfg, bg=self.colorbg, x=charColumn, y=charLine, pushUndo=False) else: #self.insertChar(character, fg=charFg, bg=charBg, x=charColumn, y=charLine, pushUndo=False, frange=frange) self.insertColor(fg=self.colorfg, bg=self.colorbg, x=charColumn, y=charLine, pushUndo=False, frange=frange) def clearStatusBarNoRefresh(self): self.addstr(self.statusBarLineNum, 0, " " * self.mov.sizeX) # clear lower status bar self.addstr(self.statusBarLineNum + 1, 0, " " * self.mov.sizeX) # clear upper status bar def clearStatusBar(self): self.addstr(self.statusBarLineNum, 0, " " * self.mov.sizeX) # clear lower status bar self.addstr(self.statusBarLineNum + 1, 0, " " * self.mov.sizeX) # clear upper status bar self.refresh() def parseArgs(self): """ do argparse stuff, get filename from user """ pass def openEditorFromDurview(self): # get ready to open editor self.appState.playOnlyMode = False #self.appState.topLine = 0 self.appState.firstCol = 0 self.xy = [self.appState.topLine, 1] self.statusBar.show() self.appState.drawBorders = True self.cursorOn() #self.stdscr.clear() self.stopPlaying() self.stdscr.nodelay(0) # wait for input when calling getch self.statusBar.setCursorModeMove() self.hardRefresh() # open editor self.mainLoop() # get back to viewer self.statusBar.hide() self.appState.drawBorders = True def runDurView(self): """ Launch the UI for the DurView app """ # While there are files to read from the openFilePicker(), put them into view mode # #if self.appState.curOpenFileName != "": # # We already opened a file from the command-line, so play it. # self.enterViewMode() for movie in self.appState.play_queue: self.loadFromFile(movie, 'dur') # this loads ansi files, too. win self.enterViewMode() while self.appState.durview_running: #file = self.openFilePicker() opened = self.openFromMenu() if opened == False: # User exited file picker with esc, so exit DurView self.appState.durview_running = False else: self.enterViewMode() durdraw-0.29.0/durdraw/durdraw_ui_widgets.py000066400000000000000000000744101476305210600212010ustar00rootroot00000000000000# Stuff to draw widgets, status bars, etc. The logical parts, separate from # the drawing/ncurses part. # For example, in the status bar, we have a color swatches widget. # The color swatch widget should know everything about itself except curses. # NO CURSES CODE IN THIS FILE!!!! # Instead make a durdraw_ui_widgets_curses that handles the curses bits. Then do the same with kivy, etc. # # it should know its location inside of the Status Bar import pdb import time from durdraw.durdraw_ui_widgets_curses import ButtonHandler from durdraw.durdraw_ui_widgets_curses import ColorPickerHandler from durdraw.durdraw_ui_widgets_curses import ColorSwatchHandler from durdraw.durdraw_ui_widgets_curses import DrawCharPickerHandler from durdraw.durdraw_ui_widgets_curses import MenuHandler from durdraw.durdraw_ui_widgets_curses import StatusBarHandler from durdraw.durdraw_ui_widgets_curses import FgBgColorPickerHandler from durdraw.durdraw_ui_widgets_curses import ToolTipHandler from durdraw.durdraw_gui_manager import Gui class Button(): def __init__(self, label, x, y, callback, window, invisible = False, appState=None): #self.location = 0; # 0 = beginning of /tring self.x :int = x self.y :int = y self.appState = appState self.realX :int = x self.realY :int = y self.label :str = label # What you should click self.identity :str = label # Unique from the label self.tooltip_command :str = None self.tooltip_hidden :bool = True self.persistant_tooltip :bool = False self.width :int = len(self.label) self.color :str = "brightGreen" # bright green = clickable by defaulta self.image = None # If we want an icon in a GUI version self.window = window # stdscr in the case of screen self.callback = callback # sub_buttons automatically show and hide with this # button. like satellites. # # {my-label: sub-button-pointer} # The idea is that when the label is "Draw" and I run .show(), # call sub-button.show(). self.sub_buttons = {} self.parameter = None self.hidden :bool = False self.enabled = True # Responds to clicks self.selected :bool = False self.picker :bool = False self.invisible :bool = invisible # If true, responds to clicks but does not show. Useful for "overlays" self.handler = ButtonHandler(self, self.window, callback, appState=self.appState) def add_sub_button(self, label, button): """ Label is what self.label should be when I should calll button.show() """ self.sub_buttons.update({label: button}) def set_tooltip_command(self, command: str): """ Command should be the keyboard command, like the "o" in esc-o """ self.tooltip_command = command def get_tooltip_command(self): return self.tooltip_command def set_label(self, label): self.label = label self.width = len(self.label) def hide(self): self.hidden = True #self.handler.hidden = True for label in self.sub_buttons: self.sub_buttons[label].hide() self.handler.hide() def show(self): self.hidden = False for label in self.sub_buttons: if self.label == label: self.sub_buttons[label].show() self.draw() def update_real_xy(self, x=None, y=None): if x == None: x = self.realX if y == None: y = self.realY self.realX = x self.realY = y def draw(self): should_draw = True if self.invisible: should_draw = False if self.hidden: should_draw = False if self.appState.playOnlyMode: should_draw = False if should_draw: self.handler.draw() def make_invisible(self): self.invisible = True def become_selected(self): self.selected = True self.handler.draw() self.selected = False def on_click(self): #result = self.do_nothing() result = None #if self.hidden == False: if self.enabled and not self.hidden: self.selected = True self.handler.draw() result = self.handler.on_click() self.selected = False return result def do_nothing(self): pass def handle_event(self, event): return self.handler.handle_event(event) class Menu(): def __init__(self, window, x=0, y=0, caller=None, appState=None, statusBar=None): """ init menu items """ self.window = window self.caller = caller self.appState=appState self.items = {} self.buttons = [] self.hidden = True self.title = None self.statusBar = None self.is_submenu = False self.x = x self.y = y self.handler = MenuHandler(self, window, appState=appState) self.gui = Gui(window=self.window) def set_x(self, x): self.x = x self.handler.x = x def set_y(self, y): self.y = y self.handler.y = y def set_title(self, title): self.title = title self.handler.title = self.title def add_item(self, label, on_click, hotkey, shortcut=None, has_submenu=False): props = {"on_click": on_click, "hotkey": hotkey, "shortcut": shortcut, "has_submenu": has_submenu} item = {label: props} self.items.update(item) # add button #itemButton = Button(label, 0, 0, on_click, self.window) if shortcut: long_label = f"{label} {shortcut}" else: long_label = f"{label} poop" itemButton = Button(long_label, 0, self.x, on_click, self.window, appState=self.appState) itemButton.make_invisible() self.buttons.append(itemButton) #self.handler.rebuild() #itemButton.update_real_xy(x=self.caller.x) def show(self): for button in self.buttons: #button.x = self.x #button.update_real_xy(x=self.x) self.gui.add_button(button) button.show() #pdb.set_trace() self.hidden = False response = self.handler.show() return response def tryHotKey(self, key): """ Try a hotkey to see if it works """ pass def hide(self): self.handler.hide() for button in self.buttons: self.gui.del_button(button) self.hidden = True def showHide(self): if self.hidden == True: return self.show() else: self.hide() class FgBgColorPicker: """ Draw the FG and BG color boxes that you can click on to launch a color selector """ def __init__(self, window, x=0, y=0): self.window = window self.x = x self.y = y self.handler = FgBgColorPickerHandler(self, x=self.x, y=self.y) def draw(self): self.handler.draw() class DrawCharPicker: """ Ask the user for the character to draw with """ def __init__(self, window, x=0, y=0, caller=None): self.window = window self.caller = caller self.appState = self.caller.caller.appState self.handler = DrawCharPickerHandler(self, self.window) def pickChar(self): self.handler.pickChar() class ColorPicker: """ Draw a color palette, let the user click a color. Makes the user's selected color available somewhere. """ def __init__(self, window, x=0, y=0, caller=None, colorMode="256", type="fg"): self.hidden = True self.window = window self.colorMap = {} self.colorMode = colorMode self.type = type # "fg" or "bg," or maybe "fgbg" self.x = x self.y = y self.totalColors = 256 self.appState = caller.appState if colorMode == "256": self.height = 8 self.width = 38 self.totalColors = 255 elif colorMode == "16": #self.height = 1 #self.width = 16 # short and wide - good self.height = 2 self.width = 10 # tall and thin - good, but color order # is wrong #self.height = 10 #self.width = 4 self.totalColors = 16 if self.appState.iceColors: self.totalColors = 15 self.caller = caller self.handler = ColorPickerHandler(self, window, width=self.width, height=self.height) def showHide(self): #pdb.set_trace() if self.hidden == True: self.hidden = False self.caller.appState.colorPickerSelected = True self.show() elif self.hidden == False: self.hidden = True self.caller.appState.colorPickerSelected = False self.hide() def switchTo(self): """ Switch user interaction to the color picker, already on the screen """ self.caller.appState.colorPickerSelected = True self.showFgPicker() def show(self): self.hidden = False #self.showFgPicker() self.handler.show() def showFgPicker(self, message=None): """ Returns the color picked by the user """ # Draw foreground colors 1-5 in a panel. # Run an input loop to let user use mouse or keyboard # to select color (arrows and return). Then return the # selected color. self.hidden = False color = self.handler.showFgPicker(message=message) if not self.caller.appState.sideBarShowing: self.hide() return color def hide(self): self.hidden = True self.handler.hide() class MiniSelector(): # for seleting cursor mode """ Line up some characters beside each other, and let the user select which one by clicking on it. The currently selected one is drawn inverse. Example, for picking between Select, Draw and Color: SPC """ def __init__(self): self.items = [] class ColorSwatch(): def __init__(self, caller, x=0, y=0): """ Initialize a swatch of 24 colors """ window = caller.window self.window = window self.handler = ColorSwatchHandler(self, self.window) self.bank = [] self.colorMap = {} self.x = x self.y = y for color in range(0,24): self.bank.append(color) #swatch = [1] * 24 # color 1 def draw(self): self.handler.draw() class ToolTipsGroup(): """ These are tooltips that can show up without being tied to any specific object, other than an x/y location """ def __init__(self, caller): self.tips = [] self.hidden = True self.caller = caller def add_tip(self, hotkey :str, row=0, col=0): newTip = ToolTip(self.caller) newTip.hotkey = hotkey newTip.row = row newTip.column = col self.tips.append(newTip) def get_tip(self, hotkey :str): for tip in self.tips: if tip.hotkey == hotkey: return tip return False def show(self): self.hidden = False for tip in self.tips: tip.show() def hide(self): self.hidden = True for tip in self.tips: tip.hide() class ToolTip(): def __init__(self, context): # context is the caller, basically the statusbar # context provides window, appState, etc self.hotkey = '' # the key the user presses self.column = 0 # on the screen self.row = 0 self.hidden = True self.alwaysHidden = False self.handler = ToolTipHandler(self, context) def set_hotkey(self, hotkey :str): self.hotkey = hotkey def set_location(self, row=None, column=None): if row == None: row = self.row if column == None: column = self.column self.row = row self.column = column def show(self): if not self.alwaysHidden: self.hidden = False self.handler.show() def hide(self): self.hidden = True self.handler.hide() class StatusBar(): def __init__(self, caller, x=0, y=0, appState=None): window = caller.stdscr self.caller=caller self.appState = appState self.window = window self.gui = caller.gui # top level gui handler thing self.handler = StatusBarHandler(self, window) self.items = [] self.buttons = [] self.colorPickerEnabled = False self.hidden = False self.x = x self.y = y # Initialize tooltips that aren't tied to a button object # These show up when the user hits esc, along with the # visible buttons' tooltips. # "Free floating tips" self.other_tooltips = ToolTipsGroup(self) self.other_tooltips.add_tip("g", row=0, col=7) # Frame self.other_tooltips.add_tip("+", row=0, col=7) # FPS+ self.other_tooltips.add_tip("-", row=0, col=7) # FPS- self.other_tooltips.add_tip("D", row=0, col=7) # Frame delay self.other_tooltips.add_tip("R", row=0, col=7) # Frame range self.other_tooltips.add_tip("c", row=0, col=7) # color picker self.other_tooltips.add_tip("[", row=0, col=7) # prev charset self.other_tooltips.add_tip("]", row=0, col=7) # next charset self.other_tooltips.add_tip("p", row=0, col=7) # play/pause self.other_tooltips.add_tip("j", row=0, col=7) # prev frame self.other_tooltips.add_tip("k", row=0, col=7) # next frame # If we're in 16 color mode, always hide the "c" button if self.appState.colorMode == "16": colorPicker_tooltip = self.other_tooltips.get_tip('c') colorPicker_tooltip.alwaysHidden = True # Settings menu #settingsMenuColumn = mainMenu.handler.width # Try to place to the right of the main menu settingsMenuColumn = 22 # Try to place to the right of the main menu settingsMenu = Menu(self.window, x = self.x - 2, y = settingsMenuColumn, caller=self, appState=self.appState, statusBar=self) settingsMenu.add_item("16 Color Mode", caller.switchTo16ColorMode, "1") settingsMenu.add_item("256 Color Mode", caller.switchTo256ColorMode, "2") settingsMenu.add_item("VGA Colors", caller.enableTrueVGAColors, "v") settingsMenu.add_item("ZX Spectrum Colors", caller.enableTrueSpeccyColors, "z") settingsMenu.add_item("C64 Colors", caller.enableTrueC64Colors, "c") #settingsMenu.add_item("Deafult Colors", caller.resetColorsToDefault, "d") settingsMenu.add_item("Toggle Mouse", caller.toggleMouse, "m") settingsMenu.add_item("Toggle Color Scroll", caller.toggleColorScrolling, "s") settingsMenu.add_item("Toggle Wide Wrapping", caller.toggleWideWrapping, "w") if self.appState.mental: # Experimental stuff settingsMenu.add_item("Toggle iCE Colors (MENTAL)", caller.toggleIceColors, "i") settingsMenu.add_item("Toggle Injecting (MENTAL)", caller.toggleInjecting, "j") if self.appState.debug: settingsMenu.add_item("Toggle Debug", caller.toggleDebug, "d") settingsMenu.add_item("Python Console", caller.jumpToPythonConsole, "p") settingsMenu.is_submenu = True #settingsMenu.add_item("Show/Hide Sidebar", caller.toggleSideBar, "s") settingsMenu.set_x(self.x - 1) settingsMenu.set_y(settingsMenuColumn) self.settingsMenu = settingsMenu # Transforms menu #transformMenuColumn = 24 # Try to place to the right of the main menu transformMenuColumn = 35 # Try to place to the right of the Animation menu transformMenu = Menu(self.window, x = self.x - 2, y = transformMenuColumn, caller=self, appState=self.appState, statusBar=self) transformMenu.add_item("Bounce", caller.transform_bounce, "b") transformMenu.add_item("Repeat", caller.transform_repeat, "r") transformMenu.add_item("Reverse", caller.transform_reverse, "v") transformMenu.add_item("Apply NeoFetch Keys", caller.apply_neofetch_keys, "n") #transformMenu.add_item("Show/Hide Sidebar", caller.toggleSideBar, "s") transformMenu.set_x(self.x - 1) transformMenu.set_y(transformMenuColumn) transformMenu.is_submenu = True self.transformMenu = transformMenu # Make the Edit menu editMenuColumn = 22 # Try to place to the right of the main menu editMenu = Menu(self.window, x = self.x - 1, y = self.y, caller=self, appState=self.appState, statusBar=self) editMenu.add_item("Undo", caller.clickedUndo, "u", shortcut="esc-z") editMenu.add_item("Redo", caller.clickedRedo, "r", shortcut="esc-r") editMenu.add_item("Mark/Select", caller.startSelecting, "k", shortcut="esc-K") editMenu.add_item("Paste", caller.pasteFromMenu, "p", shortcut="esc-v") editMenu.add_item("Find /", caller.searchForStringPrompt, "/", shortcut="esc-F") editMenu.add_item("Insert Line", caller.addLine, "i", shortcut="esc-'") editMenu.add_item("Delete Line", caller.delLine, "d", shortcut="esc-;") editMenu.add_item("Character Sets", caller.showCharSetPicker, "c", shortcut="esc-S") editMenu.add_item("Replace Color", caller.replaceColorUnderCursor, "e", shortcut="esc-L") editMenu.is_submenu = True editMenu.set_x(self.x - 1) editMenu.set_y(editMenuColumn) self.editMenu = editMenu # main menu items self.menuButton = None # Create a menu list item, add menu items to it mainMenu = Menu(self.window, x = self.x - 1, y = self.y, caller=self, appState=self.appState, statusBar=self) #mainMenu.gui = self.gui mainMenu.add_item("New/Clear", caller.clearCanvasPrompt, "n", shortcut="esc-C") mainMenu.add_item("Open", caller.openFromMenu, "o", shortcut="esc-o") mainMenu.add_item("Save", caller.save, "s", shortcut="esc-s") #mainMenu.add_item("16 Color Mode", caller.switchTo16ColorMode, "1") #mainMenu.add_item("256 Color Mode", caller.switchTo256ColorMode, "2") #mainMenu.add_item("Settings", settingsMenu.showHide, "t", has_submenu=True) #mainMenu.add_item("Transform", caller.showTransformer, "a", has_submenu=True) mainMenu.add_item("Info/Sauce", caller.clickedInfoButton, "i", shortcut="esc-i") mainMenu.add_item("Color Picker", caller.selectColorPicker, "l", shortcut="tab") mainMenu.add_item("Viewer Mode", caller.enterViewMode, "v", shortcut="esc-V") mainMenu.add_item("Edit", caller.openEditMenu, "e", has_submenu=True) mainMenu.add_item("Settings", caller.openSettingsMenu, "t", has_submenu=True) mainMenu.add_item("Help", caller.showHelp, "h", shortcut="esc-h") mainMenu.add_item("Quit", caller.safeQuit, "q", shortcut="esc-q") #menuButton = Button("?", 0, 0, mainMenu.showHide, self.window) #menuButton = Button("Menu", 0, 0, mainMenu.showHide, self.window, appState=self.appState) menuButton = Button("Menu", 0, 0, caller.openMainMenu, self.window, appState=self.appState) menuButton.set_tooltip_command('m') self.menuButton = menuButton menuButton.realX = self.x + menuButton.x menuButton.realY = self.y + menuButton.y menuButton.show() self.menuButton = menuButton #mainMenu.x = menuButton.realX - 1 #mainMenu.y = menuButton.realY mainMenu.set_x(menuButton.realX - 1) mainMenu.set_y(menuButton.realY) self.mainMenu = mainMenu # Animation menu self.animButton = None #animButton_offset = 18 animButton_offset = 7 # Create a menu list item, add menu items to it animMenu = Menu(self.window, x = animButton_offset, y = self.y, caller=self, appState=self.appState, statusBar=self) animMenu.set_title("Animation:") animMenu.add_item("Clone Frame", caller.cloneToNewFrame, "n", shortcut="esc-n") animMenu.add_item("Append Empty Frame", caller.appendEmptyFrame, "a", shortcut="esc-N") animMenu.add_item("Delete Frame", caller.deleteCurrentFrame, "d", shortcut="esc-d") animMenu.add_item("Set Frame Delay", caller.getDelayValue, "l", shortcut="esc-D") animMenu.add_item("Set Playback Range", caller.getPlaybackRange, "r", shortcut="esc-R") animMenu.add_item("Go to Frame", caller.gotoFrameGetInput, "g", shortcut="esc-g") animMenu.add_item("Move Frame", caller.moveCurrentFrame, "m", shortcut="esc-M") animMenu.add_item("Shift Frames Right", caller.shiftMovieRight, "}", shortcut="esc-}") animMenu.add_item("Shift Frames Left", caller.shiftMovieLeft, "{", shortcut="esc-{") animMenu.add_item("Transform", caller.openTransformMenu, "t", has_submenu=True) animButton = Button("Anim", 0, animButton_offset, caller.openAnimMenu, self.window, appState=self.appState) animButton.set_tooltip_command('a') self.animButton = animButton animButton.realX = self.x + animButton.x animButton.realY = self.y + animButton.y animButton.show() self.animButton = animButton animMenu.set_x(animButton.realX - 1) animMenu.set_y(animButton.realY) self.animMenu = animMenu # Mouse tools menu toolMenu = Menu(self.window, x=45, y=self.y, caller=self, appState=self.appState, statusBar=self) toolMenu.set_title("Mouse Tools:") #toolMenu = Menu(self.window, x=5, y=self.y, caller=self) toolMenu.add_item("Move", self.setCursorModeMove, "m") #toolMenu.add_item("Select", self.setCursorModeSelect, "s") toolMenu.add_item("Draw", self.setCursorModeDraw, "d") toolMenu.add_item("Paint", caller.setCursorModePaint, "p") toolMenu.add_item("Color", self.setCursorModeCol, "c") toolMenu.add_item("Erase", self.setCursorModeErase, "e") toolMenu.add_item("Eyedrop", self.setCursorModeEyedrop, "y") toolMenu.add_item("Draw/Fill Char", caller.openDrawCharPicker, "h") self.toolMenu = toolMenu # Make cursor tool selector button # offset is how far right to put the button in the statusbar: #toolButton_offset = 45 #toolButton_offset = 7 toolButton_offset = 14 #toolButton = Button("Tool", 0, toolButton_offset, toolMenu.showHide, self.window, appState=self.appState) toolButton = Button("Tool", 0, toolButton_offset, caller.openMouseToolsMenu, self.window, appState=self.appState) #toolButton = Button("Tool", 0, 5, toolMenu.showHide, self.window) #toolButton.label = self.caller.appState.cursorMode toolButton.set_label(self.caller.appState.cursorMode) toolButton.set_tooltip_command('t') toolButton.picker = True toolButton.realX = self.x + toolButton.x # toolbar shit toolButton.realY = self.y + toolButton.y toolButton.show() toolMenu.set_x(toolButton.realX - 1) # line up the menu above the button toolMenu.set_y(toolButton.realY) self.toolButton = toolButton # This thing is already in the menu. Maybe we should reclaim # the real estate from the status bar. if self.caller.appState.showCharSetButton: charSetButton = Button("CharSet", 1, 26, caller.showCharSetPicker, self.window, appState=self.appState) # make proper label for character set button charSetLabel = self.caller.appState.characterSet charSetButton.set_tooltip_command('S') if charSetLabel == "Unicode Block": charSetLabel = self.caller.appState.unicodeBlock charSetLabel = f"{charSetLabel[:3]}.." charSetButton.set_label(charSetLabel) if self.caller.appState.colorMode == "16": charSetButton.hide() self.charSetButton = charSetButton charSetButton.hide() # Brush picker - make me a real brush someday. drawCharPicker_offset = toolButton_offset + 6 # to the right of Draw menu drawCharPicker_offset += 4 # accomodate for eyedrop for now. yes, this is dumb drawCharPicker = DrawCharPicker(self.window, caller=self) drawCharPickerButton = Button(self.caller.appState.drawChar, 0, drawCharPicker_offset, drawCharPicker.pickChar, self.window, appState=self.appState) drawCharPickerButton.picker = True drawCharPickerButton.identity = "drawChar" drawCharPickerButton.realX = self.x + drawCharPickerButton.x # toolbar shit drawCharPickerButton.realY = self.y + drawCharPickerButton.y drawCharPickerButton.show() #drawCharPickerButton.hide() self.drawCharPickerButton = drawCharPickerButton self.drawCharPicker = drawCharPicker # This is to make the char picker button hide/show when # toolButton's label is set to "Draw" self.toolButton.add_sub_button("Draw", drawCharPickerButton) #colorPicker = ColorPicker(self.window, x=self.x - 2, y = self.y + 2, caller=caller) colorPicker = ColorPicker(self.window, x=self.x - 7, y = self.y + 2, caller=caller) self.colorPicker_256 = colorPicker self.colorPicker = self.colorPicker_256 #self.colorPickerButton = Button("FG: ", 1, 0, colorPicker.showHide, self.window, appState=self.appState) self.colorPickerButton = Button("FG: ", 1, 0, colorPicker.switchTo, self.window, appState=self.appState) self.colorPickerButton.invisible = True self.colorPickerButton.persistant_tooltip = True self.colorPickerButton.set_tooltip_command('c') self.colorPickerButton.realX = self.x + self.colorPickerButton.x self.colorPickerButton.realY = self.y + self.colorPickerButton.y self.items.append(self.colorPickerButton) self.buttons.append(self.colorPickerButton) self.colorPickerButton.show() #if self.caller.appState.colorMode == "256": # self.colorPickerButton.show() #else: # self.colorPickerButton.hide() colorPicker_16 = ColorPicker(self.window, x=self.x - 7, y = self.y + 2, caller=caller, colorMode="16") self.colorPicker_16 = colorPicker_16 #colorPicker_bg_16 = ColorPicker(self.window, x=self.x - 7, y = self.y + 2, caller=caller, colorMode="16", type="bg") #self.colorPicker_bg_16 = colorPicker_bg_16 #pdb.set_trace() #colorPicker.show() # for testing #self.swatch = ColorSwatch(self, x=3, y=self.y) #self.swatch.colorMap = caller.ansi.colorPairMap # Figure out where in the status bar to put it newX = len(str(self.items)) newY = self.y #fgBgColors = FgBgColorPicker(self.window, x=newX, y=newY) #menuButton.addItem(label="Help!", type="link", callback=None) # Initialize individual buttons and items #startButton = Button(label="!", callback=self.draw_start_menu) self.items.append(menuButton) self.items.append(toolButton) self.items.append(drawCharPickerButton) self.items.append(animButton) if self.appState.colorMode != '16': # in 16 color mode, don't cover up the bg color picker if self.caller.appState.showCharSetButton: self.items.append(charSetButton) #self.items.append(fgBgColors) #self.items.append(self.swatch) self.buttons.append(menuButton) self.buttons.append(toolButton) self.buttons.append(drawCharPickerButton) self.buttons.append(animButton) if self.caller.appState.showCharSetButton: self.buttons.append(charSetButton) # Add them to the items def hide(self): self.hidden = True for item in self.items: item.hide() for item in self.buttons: item.hide() def show(self): self.hidden = False exclude_list = ["drawChar"] # button identities to exclude for item in self.items: if item.identity not in exclude_list: item.show() for item in self.buttons: if item.identity not in exclude_list: item.show() def showToolTips(self): for item in self.buttons: item.handler.showToolTip() self.other_tooltips.show() def hideToolTips(self): for item in self.buttons: item.handler.hideToolTip() def enableColorPicker(self): pass def disableColorPicker(self): pass def setCursorModeMove(self): self.caller.appState.setCursorModeMove() self.caller.disableMouseReporting() self.toolButton.set_label(self.caller.appState.cursorMode) #self.drawCharPickerButton.hide() def setCursorModeSelect(self): self.caller.appState.setCursorModeSelect() self.caller.disableMouseReporting() self.toolButton.set_label(self.caller.appState.cursorMode) #self.drawCharPickerButton.hide() def setCursorModeDraw(self): self.caller.appState.setCursorModeDraw() self.caller.enableMouseReporting() self.toolButton.set_label(self.caller.appState.cursorMode) #self.drawCharPickerButton.show() def setCursorModePaint(self): self.caller.appState.setCursorModePaint() self.caller.enableMouseReporting() self.toolButton.set_label(self.caller.appState.cursorMode) def setCursorModeCol(self): self.caller.appState.setCursorModeCol() self.caller.disableMouseReporting() self.toolButton.set_label(self.caller.appState.cursorMode) #self.drawCharPickerButton.hide() def setCursorModeErase(self): self.caller.appState.setCursorModeErase() self.caller.disableMouseReporting() self.toolButton.set_label(self.caller.appState.cursorMode) #self.drawCharPickerButton.hide() def setCursorModeEyedrop(self): self.caller.appState.setCursorModeEyedrop() self.caller.disableMouseReporting() #self.toolButton.set_label(self.caller.appState.cursorMode) self.toolButton.set_label("Eye") #self.drawCharPickerButton.hide() def updateLocation(self, x, y): self.x = x self.y = y def draw(self): """ Draw the status bar """ if self.hidden: return False self.handler.draw() for item in self.items: if item.hidden is False: item.handler.draw(plusX=self.x, plusY=self.y) durdraw-0.29.0/durdraw/durdraw_ui_widgets_curses.py000066400000000000000000001354261476305210600225720ustar00rootroot00000000000000# Handlers for durdraw_ui_widgets.py classes import curses import pdb import time import curses.panel def curses_cursorOff(): try: curses.curs_set(0) # turn off cursor except curses.error: pass # .. if terminal supports it. def curses_cursorOn(): try: curses.curs_set(1) # turn on cursor except curses.error: pass # .. if terminal supports it. def curses_notify(screen, message, pause=False): #screen.cursorOff() #screen.clearStatusLine() screen.addstr(0, 0, message) screen.refresh() if pause: if screen.playing: screen.nodelay(0) # wait for input when calling getch screen.getch() screen.nodelay(1) # do not wait for input when calling getch else: screen.getch() if not pause: curses.napms(1500) curses.flushinp() #screen.clearStatusLine() #screen.cursorOn() screen.refresh() def curses_addstr(window, y, x, text, attr=None): # addstr(y, x, str[, attr]) and addstr(str[, attr]) """ Wraps curses addstr in a try;except, prevents addstr from crashing cureses if it fails """ if not attr: try: window.addstr(y, x, text) except curses.error as e: curses_notify(window, f"Debug: Curses error in addstr(): {e.args[0]}") #self.testWindowSize() else: try: window.addstr(y, x, text, attr) except curses.error: pass # silent ugly fail #curses_notify(window, f"Debug: Curses error in addstr(): {curses.error}") #self.testWindowSize() class MenuHandler: """ hook into Curses to draw menu """ def __init__(self, menu, window, appState=None, statusBar=None): self.menu = menu self.window = window self.appState=appState self.parentWindow = self.menu.caller.window self.x = menu.x self.y = menu.y self.width = None self.height = None self.title = menu.title # show the title if one is set self.menuOriginLine = 0 #self.rebuild() #self.panel = curses.panel.new_panel(self.curses_win) def rebuild(self): height = len(self.menu.items) + 2 # 2 for top and bottom border lines if self.title: height += 1 # find widest item in list, go a few characters larger width = len(max(self.menu.items, key = len)) + 4 + 7 # 4 for padding and side borders, more for shortcuts if self.menu.title: titleWidth = len(self.menu.title) + 4 if titleWidth> width: width = titleWidth self.width = width self.x = self.menu.x - height self.curses_win = curses.newwin(height, width, self.x, self.y) #self.curses_win.border() line = 1 if self.title: line += 1 textColor = curses.color_pair(self.appState.theme['mainColor']) | curses.A_BOLD buttonColor = curses.color_pair(self.appState.theme['clickColor']) shortcutColor = curses.color_pair(self.appState.theme['menuBorderColor']) borderColor = curses.color_pair(self.appState.theme['menuBorderColor']) menuTitleColor = curses.color_pair(self.appState.theme['menuTitleColor']) | curses.A_BOLD | curses.A_UNDERLINE maxX, maxY = self.parentWindow.getmaxyx() self.menuOriginLine = maxX - 2 - height # Draw a pretty border curses_addstr(self.curses_win, 0, 0, ".", borderColor) curses_addstr(self.curses_win, 0, 1, ("." * (self.width - 2)), borderColor) curses_addstr(self.curses_win, 0, width - 1, ".", borderColor) if self.title: curses_addstr(self.curses_win, 1, 0, ':', borderColor) curses_addstr(self.curses_win, 1, 2, self.menu.title, menuTitleColor) # Menu title curses_addstr(self.curses_win, 1, width - 1, ':', borderColor) for (item, button) in zip(self.menu.items, self.menu.buttons): shortcut = self.menu.items[item]["shortcut"] has_submenu = self.menu.items[item]["has_submenu"] curses_addstr(self.curses_win, line, 0, ':', borderColor) curses_addstr(self.curses_win, line, width - 1, ':', borderColor) curses_addstr(self.curses_win, line, 2, item, textColor) # Menu item if shortcut: curses_addstr(self.curses_win, line, width - 7, shortcut, shortcutColor) if has_submenu: curses_addstr(self.curses_win, line, width - 2, ">", shortcutColor) top_of_menu = self.menu.caller.y - len(self.menu.buttons) button.update_real_xy(x=self.menuOriginLine + line, y=self.menu.y) # working for putting menu on first line button.window = self.window line += 1 curses_addstr(self.curses_win, line, 0, ':', borderColor) curses_addstr(self.curses_win, line, width - 1, ':', borderColor) curses_addstr(self.curses_win, line, 1, ("." * (width - 2)), borderColor) self.panel = curses.panel.new_panel(self.curses_win) try: self.panel.hide() except: pass def show(self): try: self.rebuild() self.panel.top() #self.panel.move(0,0) #self.panel.move(self.menuOriginLine, 0) #self.panel.move(self.menuOriginLine, self.menu.x) self.panel.move(self.menuOriginLine, self.menu.y) self.panel.show() except: # The window was probably too short, so panel.move() returns ERR. curses_cursorOn() self.menu.hide() response = "Close" # default thing to do when done, returned to menu wrapper return response self.curses_win.keypad(True) curses.panel.update_panels() curses.doupdate() self.curses_win.refresh() # Input loop self.window.nodelay(1) #self.curses_win.nodelay(0) prompting = True options = [] current_option = 0 for item in self.menu.items: options.append(item) self.refresh() curses_cursorOff() #pdb.set_trace() #curses.mousemask(1) #print('\033[?1003l') # disable mouse reporting borderColor = curses.color_pair(self.appState.theme['borderColor']) menuItemColor = curses.color_pair(self.appState.theme['menuItemColor']) response = "Close" # default thing to do when done, returned to menu wrapper while(prompting): time.sleep(0.01) line = 1 if self.title: line += 1 c = self.window.getch() for item in self.menu.items: if item == options[current_option]: # selected item textColor = menuItemColor | curses.A_REVERSE else: textColor = menuItemColor curses_addstr(self.curses_win, line, 2, item, textColor) hotkeyIndex = 0 itemString = next(iter(item)) foundHotkey = False for letter in item: if letter.lower() == self.menu.items[item]["hotkey"] and foundHotkey == False: curses_addstr(self.curses_win, line, 2 + hotkeyIndex, letter, textColor | curses.A_UNDERLINE | curses.A_BOLD) foundHotkey = True hotkeyIndex += 1 line += 1 curses.panel.update_panels() self.window.refresh() if c == ord(self.menu.items[item]["hotkey"]): # hotkey pressed if self.menu.items[item]["has_submenu"]: # If it opens a sub-menu.. # Keep it on the screen. # Redraw previously selected as normal: if not self.menu.title: curses_addstr(self.curses_win, current_option + 1, 2, options[current_option], menuItemColor) else: curses_addstr(self.curses_win, current_option + 2, 2, options[current_option], menuItemColor) # Then highlight the new one. current_option = options.index(item) #self.rebuild() textColor = menuItemColor | curses.A_REVERSE curses_addstr(self.curses_win, line - 1, 2, item, textColor) else: # Otherwise, hide it self.hide() if not self.menu.caller.caller.playing: # caller.caller is the main UI thing self.window.nodelay(0) self.menu.items[item]["on_click"]() prompting = False if c == curses.KEY_UP: current_option = max(0, current_option - 1) #pdb.set_trace() elif c == curses.KEY_DOWN: current_option = min(len(options) - 1, current_option + 1) elif c in [13, curses.KEY_ENTER]: # selected an option #pdb.set_trace() self.hide() prompting = False # yikes lol self.menu.items[options[current_option]]["on_click"]() self.appState.colorPickerSelected = False #if not self.menu.caller.caller.playing: # caller.caller is the main UI thing # self.window.nodelay(0) elif c in [98, curses.KEY_LEFT]: self.hide() prompting = False response = "Left" if self.menu.is_submenu: response = "Pop" #self.hide() #prompting = False # Here: Launch a different menu #self.menu.statusBar.menuButton.on_click elif c in [102, curses.KEY_RIGHT]: if self.menu.items[options[current_option]]["has_submenu"]: # If it opens a sub-menu.. #curses_notify(window, f"Debug: Fnord") #self.hide() #prompting = False self.menu.items[options[current_option]]["on_click"]() prompting = False else: # Jump out of the menu, and tell the parent handler to move to the next menu self.hide() prompting = False response = "Right" #self.hide() #prompting = False # Here: Launch a different menu elif c == 27: # normal esc self.hide() prompting = False elif c == curses.KEY_MOUSE: try: _, mouseX, mouseY, _, mouseState = curses.getmouse() except: pass if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up current_option = max(0, current_option - 1) elif mouseState & curses.BUTTON5_PRESSED: # wheel down current_option = min(len(options) - 1, current_option + 1) else: # assume a click # Did the user click in the menu area? if mouseY > self.menuOriginLine and mouseY < self.menuOriginLine + len(self.menu.items): # on a menu line? if mouseX < self.x and mouseX > self.x - self.width: # in a menu column # Un-highlight selected item curses_addstr(self.curses_win, current_option + 1, 2, options[current_option], menuItemColor) # Highlight the one we're clicking on current_option = mouseY - self.menuOriginLine - 1 #item = self.menu.items[options[current_option]] #curses_notify(self.window, f"current_option: {current_option}") #self.rebuild() textColor = menuItemColor | curses.A_REVERSE curses_addstr(self.curses_win, current_option + 1, 2, options[current_option], textColor) has_submenu = self.menu.items[options[current_option]]["has_submenu"] if has_submenu: prompting = False self.menu.items[options[current_option]]["on_click"]() else: self.hide() prompting = False #self.menu.items[options[current_option]]["on_click"]() if not self.menu.caller.caller.playing: # caller.caller is the main UI thing self.window.nodelay(0) self.menu.gui.got_click("Click", mouseX, mouseY) else: #curses_notify(self.window, f"Debug: mouseX: {mouseX}, mouseY: {mouseY}, self.x: {self.x}, self.menuOriginLine: {self.menuOriginLine}") prompting = False self.hide() self.menu.gui.got_click("Click", mouseX, mouseY) #curses_notify(self.window, f"This should never happen. mouseX: {mouseX}, mouseY: {mouseY}, self.x: {self.x}, self.menuOriginLine: {self.menuOriginLine}") else: #curses_notify(self.window, f"Debug: mouseX: {mouseX}, mouseY: {mouseY}, self.x: {self.x}, self.menuOriginLine: {self.menuOriginLine}") prompting = False self.hide() self.menu.gui.got_click("Click", mouseX, mouseY) #jif c in [104, 63]: # h H Help # self.hide() # self.items["Help"]["on_click"]() # prompting = False #pdb.set_trace() curses_cursorOn() self.menu.hide() if not self.menu.caller.caller.playing: # lol .. the caller.caller is the main UI thing self.window.nodelay(0) #pdb.set_trace() return response #curses_addstr(self.window, self.menu.x, self.menu.y, "Show menu") def refresh(self): self.window.refresh() curses.panel.update_panels() #self.window.move(0,0) curses.doupdate() def hide(self): try: self.panel.hide() except: pass self.refresh() #curses_addstr(self.window, self.menu.x, self.menu.y, "Hide menu") class DrawCharPickerHandler: def __init__(self, caller, window): self.caller = caller # drawCharPicker self.window = window def pickChar(self): self.window.nodelay(0) # wait for input when calling getch maxLines, maxCol = self.window.getmaxyx() #pdb.set_trace() self.window.addstr(maxLines - 3, 0, "Enter a character to use for drawing: ") prompting = True curses.flushinp() while prompting: #c = self.window.getch() c = self.window.get_wch() time.sleep(0.01) if c in [curses.KEY_F1]: self.caller.appState.drawChar = chr(self.caller.caller.caller.chMap['f1']) prompting = False elif c in [curses.KEY_F2]: self.caller.appState.drawChar = chr(self.caller.caller.caller.chMap['f2']) PROMPting = False elif c in [curses.KEY_F3]: self.caller.appState.drawChar = chr(self.caller.caller.caller.chMap['f3']) prompting = False elif c in [curses.KEY_F4]: self.caller.appState.drawChar = chr(self.caller.caller.caller.chMap['f4']) prompting = False elif c in [curses.KEY_F5]: self.caller.appState.drawChar = chr(self.caller.caller.caller.chMap['f5']) prompting = False elif c in [curses.KEY_F6]: self.caller.appState.drawChar = chr(self.caller.caller.caller.chMap['f6']) prompting = False elif c in [curses.KEY_F7]: self.caller.appState.drawChar = chr(self.caller.caller.caller.chMap['f7']) prompting = False elif c in [curses.KEY_F8]: self.caller.appState.drawChar = chr(self.caller.caller.caller.chMap['f8']) prompting = False elif c in [curses.KEY_F9]: self.caller.appState.drawChar = chr(self.caller.caller.caller.chMap['f9']) prompting = False elif c in [curses.KEY_F10]: self.caller.appState.drawChar = chr(self.caller.caller.caller.chMap['f10']) prompting = False elif c in [27, 13, curses.KEY_ENTER]: # 27 = esc, 13 = enter, cancel prompting = False elif type(c) == str: # Is a printable/unicode character if c.isprintable(): self.caller.appState.drawChar = c prompting = False else: # is an integer, but probably still a printable character try: if chr(c).isprintable(): newChar = chr(c) self.caller.appState.drawChar = newChar prompting = False except: pass pass #self.caller.caller.drawCharPickerButton.label = self.caller.appState.drawChar self.caller.caller.drawCharPickerButton.set_label(self.caller.appState.drawChar) self.window.addstr(maxLines - 3, 0, " ") if self.caller.caller.caller.playing: self.window.nodelay(1) # don't wait for input when calling getch self.caller.caller.caller.refresh() class ColorPickerHandler: def __init__(self, colorPicker, window, width=38, height=8): self.colorPicker = colorPicker self.colorMode = colorPicker.colorMode # "256" or "16" self.totalColors = colorPicker.totalColors self.parentWindow = window self.x = colorPicker.x self.y = colorPicker.y self.parentWindow = colorPicker.caller.stdscr self.appState = colorPicker.caller.appState self.ansi = colorPicker.caller.ansi # figure out picker size #total = curses.COLORS #total = curses.COLORS realmaxY,realmaxX = self.parentWindow.getmaxyx() self.realmaxY = realmaxY self.realmaxX = realmaxX self.height = height self.width = width self.colorGrid = [[0 for i in range(self.width)] for j in range(self.height)] self.window = curses.newwin(self.height, self.width, self.x, self.y) self.window.keypad(True) self.curses_win = self.window self.panel = curses.panel.new_panel(self.curses_win) self.panel.hide() #self.fillChar = 9608 # unicode block self.fillChar = self.appState.colorPickChar # unicode block self.origin = self.x - 2 #self.move(0,self.x - 2) self.move(0,self.origin) def drawBorder(self): """ Draw a highlighted border around color picker to show it is selected. """ x = self.x - 2 y = self.y - 1 width = self.width + 1 borderColor = curses.color_pair(self.appState.theme['menuBorderColor']) curses_addstr(self.parentWindow, y, x, ("." * (width)), borderColor | curses.A_BOLD) for line in range(1, self.height + 1): curses_addstr(self.parentWindow, y + line, x, (":"), borderColor | curses.A_BOLD) def hideBorder(self): x = self.x - 2 y = self.y - 1 width = self.width + 1 borderColor = curses.color_pair(self.appState.theme['menuBorderColor']) curses_addstr(self.parentWindow, y, x, (" " * (width))) for line in range(1, self.height + 1): curses_addstr(self.parentWindow, y + line, x, (" ")) def show(self): #self.showFgPicker() self.updateFgPicker() #self.updateBgPicker() #prompting = False #print('\033[?1003l') # disable mouse movement tracking (xterm api) #curses.mousemask(1) #curses_cursorOff() # populate window with colors self.panel.top() #self.move(0,self.x - 6) self.panel.show() #oldFgColor = self.colorPicker.caller.colorfg #color = self.colorPicker.caller.colorfg #if self.appState.colorPickerSelected: # prompting = True #else: # prompting = False #if self.appState.colorPickerSelected: # if self.appState.sideBarShowing: # self.drawBorder() def hide(self): self.panel.bottom() try: self.panel.hide() except: pass def move(self, x, y): self.x = x self.y = y try: self.panel.move(y, x) except Exception as E: #self.colorPicker.caller.notify(f"Exception {E}") #pdb.set_trace() pass self.origin = self.y def updateFgPicker(self): line = 0 col = 1 if self.colorMode == "16": col = 0 if self.colorMode == "256": plain_color_pair = curses.color_pair(9) elif self.colorMode == "16": plain_color_pair = curses.color_pair(8) #maxWidth = self.realmaxX #maxHeight = self.realmaxY #for fg in range(0,curses.COLORS): # 0-255 width_counter = 0 # for color block width #for fg in range(1,self.appState.totalFgColors+1): # 0-255 firstColor = 1 if self.colorPicker.appState.iceColors: firstColor = 0 for fg in range(firstColor,self.totalColors+1): # 0-255 #color_pair = curses.color_pair(fg) if self.colorMode == "256": color_pair = curses.color_pair(fg) elif self.colorMode == "16": try: color_pair_number = self.ansi.colorPairMap[(fg, 0)] color_pair = curses.color_pair(color_pair_number) except KeyError: pdb.set_trace() if col >= self.width - 2: col = 0 line += 1 if self.colorMode == "256": if fg == 16: # first color for fancy displayed block color palette thing line += 1 col = 0 if fg > 16: width_counter += 1 if width_counter == 36: width_counter = 0 #line += 1 try: self.colorGrid[line][col] = fg except IndexError: pass #if line > 0: # pdb.set_trace() #curses_addstr(self.window, self.colorPicker.y + line, self.colorPicker.x + col, chr(self.fillChar), color_pair) # if fg == app's current fg, draw it as a * instead of self.fillChar bg = self.colorPicker.caller.colorbg if self.colorMode == "16": bg += 1 if bg == 8: bg = 0 if fg == self.colorPicker.caller.colorfg or fg == 0 and bg == 8: if fg == 1: # black if self.appState.colorPickerSelected: if fg == bg: curses_addstr(self.window, line, col, 'X', plain_color_pair | curses.A_UNDERLINE | curses.A_BOLD) else: curses_addstr(self.window, line, col, 'F', plain_color_pair | curses.A_UNDERLINE | curses.A_BOLD) else: if fg == bg: curses_addstr(self.window, line, col, 'X', plain_color_pair) else: curses_addstr(self.window, line, col, 'F', plain_color_pair) else: if self.appState.colorPickerSelected: if self.colorMode == "256": if fg == bg: curses_addstr(self.window, line, col, 'X', color_pair | curses.A_UNDERLINE | curses.A_BOLD) else: curses_addstr(self.window, line, col, 'F', color_pair | curses.A_UNDERLINE | curses.A_BOLD) if self.colorMode == "16": if fg > 8 and self.appState.iceColors == False: curses_addstr(self.window, line, col, 'F', color_pair | curses.A_UNDERLINE | curses.A_BOLD) else: curses_addstr(self.window, line, col, 'F', color_pair | curses.A_UNDERLINE ) else: if self.colorMode == "256": if fg == bg: curses_addstr(self.window, line, col, 'X', color_pair | curses.A_BOLD) else: curses_addstr(self.window, line, col, 'F', color_pair | curses.A_BOLD) if self.colorMode == "16": if fg == bg: if fg > 8 and self.appState.iceColors == False: curses_addstr(self.window, line, col, 'X', color_pair | curses.A_BOLD) else: curses_addstr(self.window, line, col, 'X', color_pair) else: if fg > 8 and self.appState.iceColors == False: curses_addstr(self.window, line, col, 'F', color_pair | curses.A_BOLD) else: curses_addstr(self.window, line, col, 'F', color_pair) # 16 color, black background (8), showing color 1 (black). . why the fuck is it 9 lol elif self.colorMode == "16" and fg == 1 and bg == 9: if self.appState.colorPickerSelected: curses_addstr(self.window, line, col, 'B', plain_color_pair | curses.A_UNDERLINE) else: curses_addstr(self.window, line, col, 'B', plain_color_pair) # 16 color, black background (8), showing color 8 (grey) elif self.colorMode == "16" and fg == 9 and bg == 9: # draw fill character unmodified if fg > 8 and self.appState.iceColors == False: curses_addstr(self.window, line, col, self.fillChar, color_pair | curses.A_BOLD) else: curses_addstr(self.window, line, col, self.fillChar, color_pair) elif fg == bg: if self.appState.colorPickerSelected: if self.colorMode == "256": curses_addstr(self.window, line, col, 'B', color_pair | curses.A_UNDERLINE) elif self.colorMode == "16" and fg == 9: # black, so show as default color curses_addstr(self.window, line, col, 'B', plain_color_pair | curses.A_UNDERLINE) elif self.colorMode == "16" and fg == 0: # black, so show as default color curses_addstr(self.window, line, col, 'B', plain_color_pair | curses.A_UNDERLINE) elif self.colorMode == "16" and bg != 0: curses_addstr(self.window, line, col, 'B', color_pair | curses.A_UNDERLINE) else: if self.colorMode == "256": curses_addstr(self.window, line, col, 'B', color_pair | curses.A_BOLD) elif self.colorMode == "16" and fg == 1: # black, so show as default color curses_addstr(self.window, line, col, 'B', plain_color_pair) elif self.colorMode == "16" and fg == 9: # black, so show as default color curses_addstr(self.window, line, col, 'B', plain_color_pair) elif self.colorMode == "16" and bg != 0: curses_addstr(self.window, line, col, 'B', color_pair | curses.A_UNDERLINE) #if self.colorMode == "16" and fg == 8: # black, so show as default color # curses_addstr(self.window, line, col, 'B', plain_color_pair) #elif self.colorMode == "16": # curses_addstr(self.window, line, col, 'B', color_pair) else: if self.colorMode == "256": curses_addstr(self.window, line, col, self.fillChar, color_pair) elif self.colorMode == "16": # draw bright colors in 16 color mode if fg > 8 and self.appState.iceColors == False: curses_addstr(self.window, line, col, self.fillChar, color_pair | curses.A_BOLD) #debug_string = str(color_pair) #self.colorPicker.caller.notify(debug_string) else: curses_addstr(self.window, line, col, self.fillChar, color_pair) if self.colorMode == "16" and bg == 0 and fg == 0: # black bg in 16 color mode if self.appState.colorPickerSelected: curses_addstr(self.window, line, col, 'B', plain_color_pair | curses.A_UNDERLINE) else: curses_addstr(self.window, line, col, 'B', plain_color_pair) col += 1 curses_addstr(self.window, line, col + 5, " ", color_pair) curses_addstr(self.window, line, col + 5, str(self.colorPicker.caller.colorfg), color_pair) def showFgPicker(self, message=None): #self.colorPicker.caller.notify(f"showFgPicker") return self.showColorPicker(type="fg", message=message) def move_up_256(self): if self.colorMode == "256": color = self.colorPicker.caller.colorfg if color < 32: color -= 16 elif color > 31 and color < 52: color = 15 else: color -= self.width - 2 if color < 1: color = 1 self.colorPicker.caller.setFgColor(color) def move_down_256(self): if self.colorMode == "256": color = self.colorPicker.caller.colorfg if color < 16: color += 16 else: color += self.width - 2 if color >= self.totalColors: color = self.totalColors self.colorPicker.caller.setFgColor(color) def showColorPicker(self, type="fg", message=None): #self.colorPicker.caller.notify(f"showColorPicker") """ Shows picker, has UI loop for letting user pick color with keyboard or mouse """ if type == "fg": self.updateFgPicker() elif type == "bg": self.updateBgPicker() prompting = False #print('\033[?1003l') # disable mouse movement tracking (xterm api) #curses.mousemask(1) curses_cursorOff() # populate window with colors self.panel.top() #self.move(0,self.x - 6) self.panel.show() oldFgColor = self.colorPicker.caller.colorfg oldBgColor = self.colorPicker.caller.colorbg color = self.colorPicker.caller.colorfg if self.appState.colorPickerSelected: prompting = True #self.window.nodelay(0) # wait for input when calling getch else: prompting = False if self.appState.colorPickerSelected: if self.appState.sideBarShowing: self.drawBorder() #self.window.nodelay(1) #self.colorPicker.caller.notify(f"showColorPicker() hit. {prompting=}") mov = self.colorPicker.caller.mov appState = self.colorPicker.caller.appState # ^ Used to determine if we clicked in the canvas: while(prompting): time.sleep(0.01) #self.colorPicker.caller.drawStatusBar() self.update() c = self.window.getch() if c in [98, curses.KEY_LEFT, ord('h')]: if color == 0: color = self.totalColors else: color -= 1 #self.colorPicker.caller.colorfg = color self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif c in [102, curses.KEY_RIGHT, ord('l')]: color += 1 #if color >= curses.COLORS: if color > self.totalColors: color = 0 #self.colorPicker.caller.colorfg = color self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif c in [curses.KEY_UP, ord('k')]: if self.colorMode == "256": if color < 32: color -= 16 elif color > 31 and color < 52: color = 15 else: color -= self.width - 2 elif self.colorMode == "16": self.colorPicker.caller.nextBgColor() #color -= self.width - 2 if color <= 0: color = 1 self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif c in [curses.KEY_DOWN, ord('j')]: if self.colorMode == "256": if color < 16: color += 16 else: color += self.width - 2 if color >= self.totalColors: color = self.totalColors self.colorPicker.caller.setFgColor(color) elif self.colorMode == "16": self.colorPicker.caller.prevBgColor() #color += self.width - 2 self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif c == curses.KEY_HOME: color = 1 self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif c == curses.KEY_END: #COLOr = 255 color = self.totalColors self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif c in [13, curses.KEY_ENTER, 9, 353, 27]: # Return, Accept color. 9=tab, 353=shift-tab, 27 = esc #if not self.appState.stickyColorPicker: if not self.appState.sideBarShowing: self.hide() prompting = False self.appState.colorPickerSelected = False self.updateFgPicker() self.hideBorder() #self.colorPicker.caller.notify(f"{c=}, {prompting=}") if not self.colorPicker.caller.playing: # caller.caller is the main UI thing self.window.nodelay(0) # wait if c == 27: # esc, cancel self.colorPicker.caller.setFgColor(oldFgColor) self.colorPicker.caller.setBgColor(oldBgColor) return False c = None #return color elif c == curses.KEY_MOUSE: try: _, mouseX, mouseY, _, mouseState = curses.getmouse() except: pass if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel down? color += 1 if color > appState.totalFgColors: if appState.colorMode == "16": color = 1 elif appState.colorMode == "256": color = 0 self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif mouseState & curses.BUTTON5_PRESSED: # wheel up? color -= 1 if appState.colorMode == "16": if color <= 0: color = appState.totalFgColors elif appState.colorMode == "256": if color < 0: color = appState.totalFgColors self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif mouseY >= self.origin and mouseX > self.x and mouseX < self.x + len(self.colorGrid[0])-2: # cpicked in the color picker self.hideBorder() #self.colorPicker.caller.notify(f"DEBUG: self.origin={self.origin}, self.x = {self.x}. mouseX={mouseX}, mouseY={mouseY}", pause=True) self.gotClick(mouseX, mouseY) prompting = False elif mouseY < mov.sizeY and mouseX < mov.sizeX \ and mouseY + appState.topLine < appState.topLine + self.colorPicker.caller.statusBarLineNum: # we did click in the canvas. self.hideBorder() prompting = False if not prompting: if not self.appState.sideBarShowing: self.hide() #self.hide() #prompting = False elif c == 27: # normal esc, Cancel c = self.window.getch() if c == curses.ERR: # Just esc was hit, no other escape sequence self.hideBorder() self.colorPicker.caller.setFgColor(oldFgColor) self.colorPicker.caller.setBgColor(oldBgColor) self.updateFgPicker() if not self.appState.sideBarShowing: self.hide() #self.hide() prompting = False # Show message, eg: "pick new color" #curses_addstr(self.window, self.appState.realmaxX - 2, 0, message, curses.color_pair(self.appState.theme['notificationColor'])) if not self.appState.colorPickerSelected: if self.appState.sideBarShowing: self.hideBorder() self.appState.colorPickerSelected = False # done prompting self.updateFgPicker() if not self.appState.sideBarShowing: self.hide() #self.hide() curses_cursorOn() self.window.nodelay(0) if self.colorPicker.caller.playing: self.window.nodelay(1) #curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) #print('\033[?1003h') # enable mouse tracking return color def gotClick(self, mouseX, mouseY): # If clicked on a color, set an FG color if not self.colorPicker.hidden and mouseY >= self.origin and mouseX < + self.x + len(self.colorGrid[0])-2: # cpicked in the color picker clickedCol = mouseX - self.x clickedLine = mouseY - self.origin if mouseY < self.origin + self.height: if self.appState.colorMode == "16": if self.colorGrid[clickedLine][clickedCol] != 0: color = self.colorGrid[clickedLine][clickedCol] self.colorPicker.caller.setFgColor(color) self.updateFgPicker() elif self.appState.colorMode == "256": if self.colorGrid[clickedLine][clickedCol] != 0: color = self.colorGrid[clickedLine][clickedCol] self.colorPicker.caller.setFgColor(color) self.updateFgPicker() if self.colorGrid[clickedLine][clickedCol] == 0: if clickedLine == 0 and clickedCol == 0: # true black, not a fake black color = self.colorGrid[clickedLine][clickedCol] self.colorPicker.caller.setFgColor(color) self.updateFgPicker() def gotDoubleClick(self, mouseX, mouseY): # set a BG color if not self.colorPicker.hidden and mouseY >= self.origin and mouseX < + self.x + len(self.colorGrid[0])-2: # cpicked in the color picker clickedCol = mouseX - self.x - 1 clickedLine = mouseY - self.origin if mouseY < self.origin + self.height: if self.colorGrid[clickedLine][clickedCol] != 0: color = self.colorGrid[clickedLine][clickedCol] if self.colorPicker.caller.appState.colorMode == "16" and color < 9: # Only set BG color if it's one of the non-bright colors. (Sorry, no ice yet) self.colorPicker.caller.setBgColor(color) self.updateFgPicker() def update(self): curses.panel.update_panels() curses.doupdate() self.window.refresh() class ColorSwatchHandler: def __init__(self, colorSwatch, window): self.colorSwatch = colorSwatch self.window = window self.fillChar = 9608 # unicode block def draw(self, plusX=0, plusY=0): for c in range(0, len(self.colorSwatch.bank)-1): curses_addstr(self.window, self.colorSwatch.y, self.colorSwatch.x, chr(self.fillChar)) class FgBgColorPickerHandler: def __init__(self, fgBgPicker, x=0, y=0): self.fgBgPicker = fgBgPicker self.window = fgBgPicker.window self.x = x self.y = y #self.on_click = on_click def draw(self, plusX=0, plusY=0): curses_addstr(self.window, self.y, self.x, "F: G: ") class ToolTipHandler: """ Draw and hide tooltips """ #def __init__(self, tooltip, window, appState=None): def __init__(self, tooltip, context): self.tooltip = tooltip self.window = context.window self.appState = context.appState def draw(self): tipString = self.tooltip.hotkey tipColor = self.appState.theme['clickHighlightColor'] | curses.A_BOLD | curses.A_UNDERLINE #curses_addstr(self.window, self.tooltip.column, self.tooltip.row, tipString, tipColor) curses_addstr(self.window, self.tooltip.row, self.tooltip.column, tipString, tipColor) def show(self): self.draw() def hide (self): pass class ButtonHandler: """ hook into Curses to draw button """ def __init__(self, button, window, on_click, appState=None): #self.label = button.label self.button = button self.window = window self.x = self.button.x self.y = self.button.y self.on_click = on_click self.appState = appState self.color = curses.color_pair(self.appState.theme['clickColor']) | curses.A_BOLD # bright green, clickable def draw(self, plusX=0, plusY=9): if not self.button.invisible: self.color = curses.color_pair(self.appState.theme['clickColor']) | curses.A_BOLD # reload color from theme textColor = self.color if self.button.selected: #curses_notify(self.window, "Tacos") textColor = textColor | curses.A_REVERSE if self.button: # a selector type button, to be iframed in []s buttonString = f"[{self.button.label}]" else: buttonString = self.button.label #curses_addstr(self.window, plusX + self.button.x, plusY + self.button.y, self.button.label) #curses_addstr(self.window, plusX + self.button.x, plusY + self.button.y, self.button.label, self.color) #curses_addstr(self.window, self.button.realX, self.button.realY, self.button.label, textColor) curses_addstr(self.window, self.button.realX, self.button.realY, buttonString, textColor) # render the button on the window if self.button.persistant_tooltip or not self.button.tooltip_hidden: if self.button.get_tooltip_command(): toolTip :str = self.button.get_tooltip_command() tipColor = self.appState.theme['clickHighlightColor'] | curses.A_BOLD | curses.A_UNDERLINE # Figure out if the hint is in the button label, if so, place it over it with # an index offset tipColOffset = 0 if toolTip.lower() in self.button.label.lower(): tipColOffset = self.button.label.lower().index(toolTip.lower()) + 1 #curses_notify(self.window, "Gorditas") # keep the tip from going off screen for some buttons if self.button.realY == 0 and tipColOffset == 0: tipColOffset += 1 # Print it next to the button for now curses_addstr(self.window, self.button.realX, self.button.realY + tipColOffset, toolTip, tipColor) def showToolTip(self): self.button.tooltip_hidden = False def hideToolTip(self): self.button.tooltip_hidden = True # Cover up with spaces #if not self.button.hidden: # self.button.draw() #if self.button.get_tooltip_command() and not self.button.hidden: # toolTip :str = self.button.get_tooltip_command() # toolTip = " " * len(toolTip) # curses_addstr(self.window, self.button.realX, self.button.realY, toolTip) def hide(self): self.hidden = True def on_click(self): curses_addstr(self.window, 0, 0, f"{self.button.label} clicked") def update_real_xy(x=0, y=0): self.button.realX = x self.button.realY = y def handle_event(self, event): # handle events for the button if event.type == 'click': self.on_click() return True class StatusBarHandler: def __init__(self, statusBar, window): self.statusBar = statusBar self.window = window self.panel = curses.panel.new_panel(self.window) #curses_addstr(window, self.statusBar.x, self.statusBar.y, "Fnord") #self.on_click = on_click def draw(self, plusX=0, plusY=0): pass #for item in self.statusBar.items: # curses_addstr(self.window, self.statusBar.x + item.x, self.statusBar.y + item.y, item.label) def handle_event(self, event): # handle events for the button if event.type == 'click': self.on_click() return True durdraw-0.29.0/durdraw/durdraw_undo.py000066400000000000000000000102041476305210600177720ustar00rootroot00000000000000import os import pickle import tempfile class UndoManager(): # pass it a UserInterface object so Undo can tell UI # when to switch to another saved movie state. """ Manages undo/redo "stack" by storing the last 100 movie states in a list. Takes a UserInterface object for syntax. methods for push, undo and redo """ def __init__(self, ui, appState = None): self.ui = ui self.undoIndex = 0 # will be 0 when populated with 1 state. self.modifications = 0 # self.undoList = [] self.historySize = 100 # default, but really determined by self.appState = appState # AppState values passed to setHistorySize() below. self.push() # push initial state def push(self): # maybe should be called pushState or saveState? """ Take current movie, add to the end of a list of movie objects - ie, push current state onto the undo stack. """ if self.modifications > 0: if self.appState.modified == False: self.appState.modified = True self.modifications += 1 if len(self.undoList) >= self.historySize: # How far back our undo history can # go. Make this configurable. # if undo stack == full, dequeue from the bottom self.undoList.pop(0) # if undoIndex isn't indexing the last item in undoList, # ie we have redo states, remove all items after undoList[undoIndex] self.undoList = self.undoList[0:self.undoIndex] # trim list # then add the new state to the end of the queue. self._append_state(self.ui.mov) # last item added == at the end of the list, so.. self.undoIndex = len(self.undoList) # point index to last item def undo(self): if self.modifications > 1: self.modifications = self.modifications - 1 if self.modifications == 2: self.appState.modified = False if self.undoIndex == 1: # nothing to undo self.ui.notify("Nothing to undo.") return False # if we're at the end of the list, push current state so we can # get back to it. A bit confusing. if self.undoIndex == len(self.undoList): self.push() self.undoIndex -= 1 self.undoIndex -= 1 self.ui.mov = self._read_state(self.undoIndex) # read & set UI movie state return True # succeeded def redo(self): if self.undoIndex < (len(self.undoList) -1): # we can redo self.undoIndex += 1 # go to next redo state self.modifications += 1 self.ui.mov = self._read_state(self.undoIndex) # read & set UI movie state if self.appState.modified == False: self.appState.modified = True else: self.ui.notify("Nothing to redo.") def setHistorySize(self, historySize): """ Defines the max number of undo states we will save """ self.historySize = historySize def _append_state(self, obj): '''Stores undo state by pickling it into a temporary file, which is kept open and appended to the state buffer''' if os.environ.get('ENABLE_UNDO_TEMPFILES', '0') == '1': f = tempfile.TemporaryFile() pickle.dump(obj, f) f.seek(0) self.undoList.append(f) else: self.undoList.append(pickle.dumps(obj)) def _read_state(self, idx): '''Reads a state from the undo buffer by unpickling it from the temporary file at position idx. Rewinds the file to the beginning for future reads before returning the object''' if os.environ.get('ENABLE_UNDO_TEMPFILES', '0') == '1': obj = pickle.load(self.undoList[idx]) self.undoList[idx].seek(0) return obj else: return pickle.loads(self.undoList[idx]) durdraw-0.29.0/durdraw/durdraw_version.py000066400000000000000000000000451476305210600205140ustar00rootroot00000000000000DUR_VER = "0.29.0" version = DUR_VER durdraw-0.29.0/durdraw/durf/000077500000000000000000000000001476305210600156665ustar00rootroot00000000000000durdraw-0.29.0/durdraw/durf/bsd.durf000066400000000000000000000272201476305210600173230ustar00rootroot00000000000000jfbsd.durfoW2I#ڛYwCSi3LRd2w;H;rbYI/c,'ER\>˯'w޽ՇuV/=wo޽գO']Wzoo,?xz'_?ꏼc?]=R~(_U͛χzϷ/>~ǿ^@O~ߪN>{oUub'O37^n1,]寯>}u 7,{4{y=ח+ɋ'W/Ts\\<ŪƫS^-o}uSV\]^VC}WxU}Wjcxq{rC5W9{˫>NI.roW*[m*EWU5FU 8AvY|^zmWW_[e1!;5\_fʪ=QW/~UY}x=/v///뇮>ǯj7/>۳Alu{pT _uy^fol=n;sޯ}\lTO[z}rM7PIqQj}yZzj翼Ư@;8W&_56L=Z5|vYJn│z+{}cŋ/>Zב^^.oEY a=ݫ_{Wbtw_}?] yK.]t5ry̲ÿWy|Oɋ&o8JyS+cSN:u[ܯK.~_޽W?N}ui,ƝGmZۯ恭yÖ[W2diqYܕZ;'y)۴\.ʘZɇPf괚kˆUgݲ3Wv;Ou1s|1~) aj]{f柯߼iL?^WGnjZ*O/iOWM?=_25|z|sq, k~>QI[CNZimHυM6merʍ5Xom)®C`̵{)5G\aRVZN/.Z#? Z̰3Ks,gy OJ*UEe`QJ*XA V+ bXA V+ bXA V+ bXA V+ bX@)28ʇJyEO뷘ZA*}QְUp5| v%_k}Mk#|텲6P ZvCYǷQΓQ;'O37=QցoՌmp[eV(k9?`fm@YCP{u: ʺdem$Y{$+l ev+ˀlXkGryb徏d)gx:9>DK\v-eL]SkN6YeaS**YP1r֑X5jԨQF5jL e"ePϛ[m*ơȟbP 2yr j ZA*ArP9T*ArP9T*ArP9T*JUJU9e,n5ArP)FeHng9jYY6;ӻjO~|u;IAm9i#IA>zAAȬfP8keu7:ڂA堶`P9,Q0hUB-^m8EkQJ z6*58yR*7{e͋KJGR:j` c|vv)Sf 2V_ed+*E)RH"E)ziקFv8~WjCipTT j|h\P%T U*AJP%T U*AJPe%T U*AJP%T Z* ]Z %n>kWAg/6"\k?AZP-˻Rv̴q{u;fIPm;glv̴ڭ{,g+7cM!VY Av36hT|=jAP#T RճA.T]P]m~k9ņe.M´քZG9E,/'qb^Z *ThXGueᚏxzoWnW^R@z#"5jԨQF7_hԨQc .ur/5O6eRQC4 RErTjJEΎ OâRQTT**JERQTT**JECբRQTT**JERQTxM.T*53bJ6@CP 몞eq R!~T'u;a:dݮR?[ Tg>v4ϺcnRRm1e*Umxz'RPw>)z RR(VR**7JETw;mٮe+PWR#VWVή Ej'ȮErjpw5d*w]rWU*w]rWUZ*w]rWU*wթ]-[v5QrWwC[=V5@]}HvW[fiL]]G2w]]2:*PZ_uԩScNRcO݋XJ::|TJ/uԩHL2e<@abfDdJM#K1X ֜Z0Xo&XOObH kū`e2X V+`e2X V+`e2X V+J'U%J'U9]z4uzK7Xw(ԡ ֭6ju+:C mnPwT lgof V+ V+שT)eSNk10cSb.6;VVZjժUVZG\tiRMcq*t 8EΈt%\7 /Ƨ.K+ѕJt%]DW+o*Jt%]DW+ѕJt%]DW֩JʩV֩ 麝tJ@W"]wաH׭j@tݪ&]몁I?=YwoR%]}iA^^]Αt] ]HW#]HWRHW4N:{φ*Zuݷ`WSV^˹UVZjժUVm\lj;_ݟ; B`!I#5xB`!X, B`!X, B`!X, B`*Z*'";֡B޼I>F`X .{+~UoC:vueSS`ح khYݍW}T]Q`)XS`)XbRXN:{+>V 51+%TN&#X^zիW^zb#H%(vPl ' BEjRl\==!ŒbIXR,)K%ŒbIXR,)K%ŒbIXR,)K%ŒbI UT Ub R,)6>)] /0_]]]qb?}tEM^pbϞW6nrb#tb}N]pb9Xsb9XRXަN:9y(σM<2zիW^zoo>CGdG"!.6:؈]Bb';$6ZGbsq!XH,$ Bb!XH,$ Bbj!XH,$ Bb!XHVJHl'$ jk NNUBb!FBb!@QJ!M:uBb&Z33D+xe:Q ,X` ,x"l{ұ;=ljc[ѱc"9:6GǢcgǨcrѱXt,:EǢcѱXt,:EǢcajѱXt,:EǢcѱXtVܨJtl': It,:;y::iб:5::Ō*UEpԩ`Nx\1Uq' %IVXbŊ+VX)->n({K_͉ E,P( me%P( e@Y,P(KW e@Y,P( e@Y,jU%dU+jUAs,P6>P6#e@YAٚ]%.@Yс@Y,|TR,SN@qDQi&^W(ur~dŊ+VXbŊ+bq>PˉrEl '' rEjl(ܓ/'˓dyvQQl)i+YuQl)[)[-TR-TV[l8W[-=KjժUVZjժ)SVlC\ MDl"A6569¶`7lo' [-Öa˰e2l [d-Öa˰e2l [-Öa˰e2l*Z*i [m|m-خ [-vچ*[1l [ [-TR-SV [l0W [-;KjժUVZjժ6lb =H͓Cl"5ŶHWl[JQl)[-ŖbKRl)[-ŖbKRl)[-ŖbKRl)|WdW|WVl -6>vQQl)i+YuQl)[)[-TR-TV[l8W[-=KjժUVZjժ)SVlC\ MDl"A6569¶`7lo' [-Öa˰e2l [d-Öa˰e2l [-Öa˰e2l*Z*i؞2l`Z0l ilŰe2lFg2lSJOZ2le\e3l,ժUVZjժUfNðzR^"l4 yrm a[f! mW)-Öa˰e2l [-Öa˰e2l [-Öa˰e2l [-Ö몕r†ÖaaG [ ۬: [- [-ÖwT)ÖUV-Tll-ÖZjժUVZj Iw UvQQl)i+YuQl)[)[-TR-TV[l8W[-=KjժUVZjժ)SVlC\ MDl"A6569¶`7lo' [-Öa˰e2l [d-Öa˰e2l [-Öa˰e2l*Z*i. Z]- [ Ub2l [3l [ީR [VZTSٲa [vjժUVZjժU3l'm%b{Hs b' "EjmcKضxRl)[-ŖbKRl)[-ŖbKRl)[-ŖbKRl*ɮZ*'ؖ[m|=Rl)Vlj_Z.([)[-Tҁ\TLZjI1Td>e]&#ZjժUVZj]b"4PMDl"A6569¶n̰e2l [-Öa˰e2l [-ÖaKuU˰e2l [-Öa˰e2lrOU2l; [-Öa˰ bV" 6c2lFg2lSJ2l?sg.&VZd؎j*[ e.ˎBRZjժUVZyWŰmʰe2l [-6a{z2{ö\2l [-Öa˰e2l@W [-Öa˰e2l [-Öa˰e]Ur]]Uذ==a2l3le2lS7lxa`2lFg2lSJ2l/ԪU֘ ۱#QMe˞~v)[v28)ժUVZjժUuu>FĶb & "ŖbKmRl[JQl)[-ŖbKRl)[-ŖbKRl)[-ŖbKRl)|WdW|WVls-6BŶgARl'.URl)[Sl)[RK˓˹:uy,vܾc~Kd%;%KHcr~dŊ+VXbŊ+bq>PU';ʓMJWdɁ=d%O'˓dy:EN] EǢcEǢc1Jc:uDLTE *xe21իW^zի7|=OdGUbB'DTy\&#X^zիW^z{(8sP)6/cbH)6)6'_-St\R,)K%ŒbIXR,)K%ŒbIXvZR,)K%ŒbIXR,).I NWmuݡTݎѮY>K;Rϻ;;Nl5W(xu(Iwۺ 8 ҆b괡ح:mx(f7 fվڄbf .@XPAXP,TTRP,pSNg\KիwNhy iVjժUVZjժM6gmti;;6OP-S` "8_3 B`!X, B`!X,U- B`!X, U .!u fB`w!PƳ*φF`ʳlBlu-6j X!X,0TR,LSN؁~sybA;WuرZjժUVZjz 59ѵMiktE"]HW,IӓٓtE"]HW+tJF"]HW+tE"]HW+vsvrΤktEt#$>0v u;麋tJǎE>74Ց Mnwd!]oEUtEtE?*E1uԉt"&鰟RNZ7:GufJ_ԩS:s2eʔcMf;)eR+D$ u ix5>xvpū aBX!V+ aBX!V+ aBX!V+Š'U &Պ'U9aš*=&t'ºx a݅n^C#[ױ֍y"t֚`BX!VCX!V`R]J/<ԩSqNshSxCڕBWSN:uԩSNò|I}uqf>ڐz~ ztxH^- 9xujX^O!W%x ^WU*xJ! ^WU*x ^WU*T%T+Tճ*x5>x]͢W`סխkhxu^ݪl;m MzD~"M⭝ն~Fn[;9JoONjY:XȫUGv3Am!tRI{!>݉z>@U)Gگn?ƴkEk;y~I8YԚPQ=գ.IB :epU5_)d8E*rL)R(#"5jԨQF7vP5jԨq ,u=r8(!3b(@!z) & J*JmR[JRQTT**JERQTT**JERQTT**JERcROϦRDURYQGR?| fvLZ iO+2o6kh6k}i/+a:ZgKw>m[hf3t+J>vyV`3և[MR[\Jm5aԏ>,TKa-Vj+,eJ-ʃ]tl+J&rQkQJ z6*u,UNrsoqqT(JjC¸-{Ya"q)Sf{2V_ed+*E)RH"E)%Ӎx#;_+qBP <B5OP-q X*BPE"TU*BPE"TUZ*BPE"TU*BډP=P7`"niWB9E6 f޼wڌ!T[ՇM/B5ݯAo!'fnw[mj]erjί$T[͸6N Bu}m T?; 6jBPŐ"TN rxjQ_IP=fj1rJ2yO<R%}2.;بf6T2ʤ#V6TL ud>VF5jԨQFSo ] jCsP9[ jjAZPCA;ٯT*ArP9T*ArP9dPT*ArP9T*U+;S%3Wn~u:tdurdraw-0.29.0/durdraw/durf/cm-eye.durf000066400000000000000000000145001476305210600177270ustar00rootroot00000000000000>ifcm-eye.durfioAB"5_Mu4m"L5[r۴w/ErfvsDGRG_~trRoWw>Nooq|ٯ?zTGq6y?&~u{&Vpnz|{V>psaԣWǻG2{|p'?Tkuj5fWn>]_Ǿ[k?O6=tw뫟揝=ürrs@5ofvtw-/߾Vd1M&r:}5_VFIu^8/#MWň3ditJ4ٻ.7}`tN;]mGO׍~׮{_G{=p;hͷ?,j^-ދbmEUsxӏwb}KS\= ƙՏ[g:}WOSt#Z~Hv]aP T̴l2T>k/7i:_Yh\u5֯FUQ֓hiJgW|ݾn)=[ L])f7b8n)ɕf~jmn0Dg5U$UQHfTK!I\I:%վvAugE><'qqL?_[&Y\,b&KͫƓo0E$wE+*́eKqVċp3O%b/:4t(ըmDEQo]$5ëb鷘&}]T9|SD3:ȵ蹨ry׵l 5H CVkBAQEE[xh:T{E[T=#_"-x1#" " " ?A'Br^uzH nxKbJΏsЬ:bW:J _<ʝ>>M5|p6F OQ Whh[7hbe;Ftn/}A3|y[a6;f@ 3<9EC]xRj55555d5AQ:"!"-s#-0쒺>k ,ƴON2܉3> m83`ߤ]]^^g~*Mt>@Y~xJIUQ[[}̴C U&IRε"Eq7ɜkx&hkm;=3?ϵ_@6&qx2ˎвRr:zI'3IP3y{$zωppi1ppQD86EpppppoS 'N6Epp∍DADAD'yda&[W>5qb7_kS4qM\W.,:;ӵm--%>]9%^UđCK=@I-)# ###g&A0EN#$!IBG#ԐN!$)&nAd?;,΍*~!7=8QnAU\ eDAF+83*qU<(Tx}ګ#6XJHD^XzPEΒW7ibhfeZCelWj{W ,.B2 f8`\l+ҋGætHv#kݼ)Bz`6"*U蛪#nҢƟSĺO,>K`)Fy nnn")"qpn)Gt$ " " n܎Zx㍣MyǗC'O ED<㫑[r>mA!w|J4<,<~<<<<<)<cLRҚ8^HkO1$qۉ_xk2/sa;9Aq;q9'ֺP;jk<^lR͇+SxI/E ,y:P0ɤS3?,8{6L7GiJ,)ʤۺ6 6~ &qd(kǚ"qqqqqdnq)G"q" " "k܇5~555bcc/YܖbccKqqqqqqq:`T$ssok׳P3Ƈq/x\g*$4q}Qظ$q˹-4@>P47E`6:mx!,qI n<,7npiB/nnƻxFܸ]$E\wx)77[$fJv[jip|l!{RuJ)G<*p|Ww{Ux@;mwR88޿&qqqqtnǑ)"q4k)q"|n " " " qYq qKq\LWs䍏wqWYhSqm\U(k\P^R`;sWӝpƓu:#qqqqqqq:Pn%?APnPnIE,qkY7jf`A9RƇ9T _,T]$} ~q]j<~b<1YMF(]'LC;..sHz[,Ntyqk(/.sjC W(0F5a\V꟪ OgQI&I? wۿpBGG{"qqLnPQQQQ)e"LnPQƁ " " " eR׎+ 4Gs䌟8MyƗC./eqxxx.mlt֮6xR}xx݀aq77777O29=|b/2'l꧸ܡ$xV;4>t1Oxm;5ޡ.҄/ęR3qq s~588ƸkcO9 cr[Q㝧*Bc<&HnYgDKjOENMvcpqkɥc[<- ,5ut[Ey\/I-#DxA@IZx0y;FN<0& &&LEUS&&&&&&M68EAo5DADADA`vx &&9&1q5&L\Ǐ6L\91ѓ$&>)&K*{Uo0doUc3E[ $-xMSgmۢTUZ)d9:>cTK㳆xdç1888̺"#rT^ZgӜExPj(./B&җ|2tBm Rr!Fv /|WMbm7 >ȇIB0 =س4c]2v]?û[Q+eFڶXԆëm kB::h"p3pAo۳\us~px}0 =1f;[ YP8|[6гP#N*Áӂ'alӄwLEcRSN6E(lSNADADA{q ppntUn7\cjxj:,ѳRF w/5\0;Ù"3ί|c.BFvTg:CC_IFtxbVmELo'xLמ6uBGGmrrn Mr Frx)<}%^V|1\X͝o'P:21v::|+1i޼'IT~,Qᡛi>")n"qm)' " " Ľ/& ĭ-Z<9^8^xǣ s/ӱ>zpa!|"?( Y/;f6Ls4m|cോYZ;$\7n]$9XcxupD7} bA]Np)Z|Պ<R>fQ>xƒFOy-o;~w{7>-}x|ȇ7??^u|xxa?|w?}kx9^~wzyۇo)}~7}rWϟøM.Gcuw7=SZ^EM8"ЭJwOw`2k ^2".i'a+U7zD#C>c\\3'4w.G݇m FV|p} X=hHeE ֙!k s9ꬻPPT׿}\''?>>|<ǿ'ƫÇw+\_w~x|wo||Ӟ |>5zboǧ}x|I?>':#Jzz_6S D)Bں=u^O7߿՛Dcrß^ZV#Tz<60{П k-n\,B]Z*NsGL1ܺ}ljF8U\DTPBe)Hhyv;yBp]#|؟U΅8,L@$ 6TW Q VuU= TS8TSʰHpOXZb–h`R.OGĨZ+aP6Cd/!T-PX0ׇ/Pi9'\%T"0BN r0 %o U{bTqWo; Nb[ ]wA84 L|)G|"ϚVpӰ9P-gЌhsAvf*}d:Xsyu XС%mwkʂacDrßʉas@u~ճ,™O*3L5 Ƥ: \WU+ UX2ʼn Wt\!d_ҧ9T|l4 cl 1q s P "߭(b6%c+z*?\%Lp&!"wTuesճ)z,Ҥ?*ϧŏ=o5_S5_S5_S5_S5_S5/1[o+/]_F~.x>(uPTV?@iwȖJG:2Y jDef+2ZZ?8 ޵~\w+BKRIQU Bfيo^Li}T HZCa|`%+"Gwd_8XWypPL(dZ5Gce# 7d,qUGNT\{R8=@ʌ`5yAu^/ܕoj*PG4__^9?g)ө?*E^ωs!ktHHfyJC[ #4UU6LZa9#,#.HTF%1\3S}{BF*h\ڣB)qg"=֠fRfqQ7teЊT(Yk`$QyXg_/jv![#kw5(I⿲ B>ѫCNaIg^a՗e8jŴP3UZ^<2od_k $~W:孟l;:W P9αAD67Ңgg^Der;fiIWl߭5js5qI= |zJⶓB '5ol k5 &1s+M<lմ * 3;t%v w<۾GcFU_ ]J1q*S{:v6iJ:r9D\0>.ŪE+Kă3 ؍%k1 dk]N#cɢN?1Sauf4kÔ-rwti$d4a\ '>lpْ(2Xpa($z5  YNܵP"ujYt'(Wz}mSŕݪۤw;`Y-6M 9؍L/ Z-}Zߦj+Au_YZN)|e˧w㷓S'Gߦ}޷2w+.}l Ȉ0|\,3J)TFJl#8<`V>[ r| *;o4T UJqAł٨(E!`D9JcT:YP!T\(TX3bUhUmQHEDV7b.>L_~ԕb'^hXxJ=hoKBzpaݕ-2mU7TJ%\-i;8~&H#K E=@u2,$"5ِjXҍȱ58-uPB;LڳYi.<{s ذyzϘnz{f>Z߃B-m@ `lؿX)ps#g  =Wc9 -0>_0vQ6]P8C?c^ЋlsqeɴTH?ImG4ew;/A@Dev"rI5H hCv3 X/ & qQ_P!;2/_aQl]^tz2 4N]QߕutuybFlR?FニdC#xLW3 >Y]t;0cr&#%xX^)xtn c"OEf[ihKTX;TJ*MBExgӫgSXI-z2?U6O{jZkZkZkZkZkZ?_Z?id k׳KAa@^Z6Vg m *ßIC$$04.#LT4~?`348[\6TBR Р((/X@#:S-U )e,K( M;;'pX$LBfYy^Ji7r.wRB =Pod[(ת@ŶO<$4_|۪k=-Q|nK *ۛ"|2"j2fp6^SEŎ`.h%p=`IJl!Frj 71;`kz}qZ5A"4դcq_i&賿xtp}&SEUNjd@l}LW'ٹp ߠ9i]e=#N_bt!$0u~e pGo"pzaD;H)n0 U2-L(I4Nf"Ran>Jsn`3xeBXy)_I[??YKtīrr c՗2mnBϸϼJv~69i-<ؼ[I+]pkj^÷rzPl:=)Ϗmm'2@N(kڹغ.?̈cVox;dUfwSڹUI4rK/[5| }n+6iLV J)ߠs*ǘJ+ýq|5c?X[N49b?* J-w9,Zrpz&U$Z+ U z ?,J%~HcV 2Y*uwcD7f ޙbjwhbܣ6=;aLHv?JŠaT޸,Qa&PO=EkwLg'JV翛5Jx9GqX˽2 ! ϻΏs%e:*_ȇiB_J?yb0 |*\ck}\"ɩs@_ec *F3?Q@~eM,݈P<ԏU01j߱=p|> ~.`P+v1TDreU|p2Z\]1Ʊ_ƹ񿗥PX]4*P1:Ţ^B`Y#A͑SbnRMQ WQ*Ά PXj$ ~Xݱl` :GwQ@rQ;g`^Bκ6Ț!|wدC܇"޹OQJw+կK7+SuwIk.ƼZ y¬WƘ4Xҵ^MV:̳v󵆝Rl4O_yo'a+iNbMs5]s5]s5]s5]5];Οne>;W\xJ)"ÿOhqVG &8X %67q:kTϛ tT:Nn1V8RJ(Ĝ`P3K͆J-|c +jS=*Q(U c*q`Dv6ܘQ K%$sg\C5P]Nf+C,EXE.hfX7g`_4-OףlRDӎ5rW*AX?|`9P+Ldptwna]fRPډ/ˏc4vu"ԣ Eu%- `گac|P[wdu1Qqe0#u)fЋo=`<`'9 #,BU Xv!W$D_5]bg C0ޯI1 qKaF1'n' Dms !t~sHsE:6x3VB<˯>ߗ!'䰆+.ؓ|+ea.QMb8zҒ~5KVz'CQj-rb`[ PNJZǸߍ<`h{e[gP7Ύ!S^or=Yn9,ICaQSy+d6 Ѿ}ͦ+KKOc&Tyl~>m~y)ү)ү)ү)ү)ү)|)Z~͇/]_F~.݀>jXMÄw~Y'J-*>])O/Qhy6nM~vrl}rS%u_jھ ġ1BE!Rع Y}⻜aR pNm*r Ո}̈́h7cyr3ډq Ewxb'gK[j-ݰꙍ]YD= GX tԥDY@0zsg#m'%.k-BS%wz(k=KݼmRU:5,4^{ K91q.~>؁pa$ Yx(HG4|hXdd4|SRsJj2ߥ(w, OL".1%4Q;]+Q6A\9x9>1Տ1qMO4`9w  Bgz3\@t)1e|uʓ pt9Oڪ..d <Åo/adH/%w^UAHafkГAxC^8sƲjOO>{<3w˯gAnB,-5z@ ^R&룤R'&ɻ.ogwй^Mw}\^Q=^?: &*/7K+߸:`n%Qy )MA߳"Чk>k>k>k>VӛޡO=֧'q-#De=1#_׋.ZR{QwL] Ԇx~!it80؁q; lYxf4xe$ :|.HsR#ݱM  6bDʘ8\{Gmf:E`eVc$xrZhLhS!=QUriWve<@GzWjWj4uGHy.&ťPVW<ծ='Y3Kg;jnG-W8X9T> r9WZU稛}f垡ݾڈY :Ol+$rK]:K:ª{!uXd]x<{]xn4mcj9oX{{zv7,{ؘQm' xoG}eHXwW Cv2l zvF9ǩCq|> E|pv#t'(1U| "}1+FȂ.4ߎBdܩӦ 2v ,aTSqb%9W7o#H0{*,qLMh-^sd `w8M\Dǫ*[Lx3j'j``QW,ƒ9E|R. O ] )2v8S GXz-jCTOWaϝ]W>yQc6%_uWF}m9ÕЩۤ-o!^Y-M ֍V_L+ ZtZmݦj+A[]YZR)[e˧o4㷓֨S'Goݦ)뚲)뚲)뚲)뚲ߚuMXOX2]+.4u Ȉx1'Tkk$6,r{[D5m8v64 8ʶRD׬iJqԖ6FZ8WG70e*Xh/`̕Ҕ0PkJfۊxU6'4[ nUf1v-g52f4_h\iw2%TTF|Qd>oC;`3Z֚3NG c]D LquADZa 88+j⸋H8l|")&%wE8tۤͩ02"TB rƖM|]SÁ ꔁðтad0"nU|x>8 ttvE6n}xKAףu`c4lGQ+:$bWq! eŁF  b̋KuqCvŊCrbEE >rcqxÁ8ɹX*:T9i!pYqǩ 7Iߕ=pwyg>ˍΕxTN2s, O`͍q1QYIw+i%QnV\kX[nR*^>rd8P) %|M;[yvZO36 zm5_5_5_5_J7}*8r[?S-Q5{n\"O7L<ƲR J0U1ܱ G7u,fԮx?8SK繰):6Aĩ$)eHQwL(̘Hm[ NKxTFvT1 KQ/oU.^MuL_p\R,*;zD.B2@zsͥP]78`J51J-W2A<&eOcH `Hψ#9 xJ:Ň!՝ww ]{WR|i{Owddurdraw-0.29.0/durdraw/durf/linux-tux.durf000066400000000000000000000220741476305210600205320ustar00rootroot00000000000000̡hftux-fetch4.durOsHz| Δ؀ `^RM$f砱]Wy-Nd=DZ 6EϿ}ټwvw~/kwo^>|Ŷ{7?޽|^ݽu~y7x˗Ƿ>ysۏ_{{w;||}w/cOx#wnr7ǯx_>~ۍ۟/WnOyurk >9s{i_Û/noƯ8oox}zd?Jy_k3lFue;reԟlߵ/1-yD66g>f=9|S3~lw֒27j"Ƶ4oƹiߥ]w-ku. k_7zm1?$m׹Msۧu59O;3U&ާd]qks|4 3cdC\1nBnW|b|tY<k~snu X,[l_F 3NoB7x<Ofh4g+CmHNO'|n6HZ/}~Qtf#ς!1|tQ>WYMېn_ZbOѯ?ˏ?v/7W?qxyXO_mz⧗TY^UU5Z:ʚ*j:%&MQ_t l5kӫ]scEMeoϵװ#w'!^% ۹lk; BtM/ GP`Bퟷ{;khܥЈ1+'d]F:dCgE:cQu%%nYI~Fr']Y:9=h%,H Ap"`QKQd#LZan 1kՅo,9SV~s;>zE373gԣ)&O#NW^mÙSєѫDNGg˷Ri]"Bty\~=Ju`dF?K|Nj6ȿfP]c=vPLEWPv 2Cc/Y(TlGC %&mF`k}R)юZmOzU kŹGVI~ee+ וwvx֎eM² br "d ,Y>+,o6]n?,,,,YNR_g, 78aQVoD>ўA"bd9a/|rK3HqNdYU/q(Ί,he; ,"]ޮxMXY{J| \W_ǩG(\(cWW>N+., _]#NUW>vDѕwI#%PW1#a]1(UN!OX>S2{rx6!*I{*xgxgxgxgxgxgxgxgxgxgxgxgxgxgxg᝗˙gso Q2X2{NW{Q=箉= 3䳒&SN9[I'G1,}IQ ?g*?襟k)8ls昸s$aU.- z~8U h+ zN~K ?hhhhhhhhhhhhhhh傲rZ$PV@('/@[R 2%hdPU-ޒ ^R^Ċ=ht򒔁9zIM&t(*݅j-C˧`C $ǡס$shG& -^Ԉ4S-P#F[[JsLj֔5yKBAvt9q2ВG- z$[ixM-,EZEH 1I҂9"yKqt1JږR$E HHzR˶|@AZ,'= mM)ϏΟ#8Jӣc\-eSdhkOrtq8R-W#F[sSd@/\fFKȐkDhᑖN"E-)Y0b.HFEEEEEE+S              M\4\4\4\4\4\4\4\4\4\4\4\4\4\4\4\t hb%ɱhs9+:kI\^ShI)8b-əikcZg)Ms4ֳGbY&Qzy<$Tc-qL-Igc,)$:\XԾ<.sEz|g|g|g|g|g|g|g|g|g|g|g|g|g|g|g|g|g|g|gpd|g|g|g|g|g|g|g|g|g|g|g|g|g|g|g|2|UՅgN(KH4\Dg=mwV"O2 $iqY1|ْV$BsV$!$&pȥ=X&#!wN8pٟB;Wc1nκRْ&WwekZᏌ}g 9Eﬥ%O!+$𝽉Ws67%bDvDp]Yigs fy;vvD;_A;[36y|Rmt! vFMZ"9f-V,kJpUuxk+nrtv\MV8V'h 1v]JZii*[ϬCiئ#^HsY)G]f!MڎFtW3{[ڬљ=:/pf%^6GJ]?YIJyyV9RI~99+2k W5Ux5D̞i,NHn2LfLfLfLfLfLfLfLfLfLfLfL՘*:M.*Mf+μZ9U&3&3&9I=111111UB8e~RidJHkg(P5v*)5ֵO|]c}#6.ء6.gTaÍ})Zu,7V uƅu#d;BoXS9888888888888^plU˜CSp p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p\p|(8(7֔f|G]h#6vEa徿RXe +_ոRӸQGM4.yn(>^rӊb5c>XEG8e\Vy8NGvؖQc(Û1VHhs7Lhdurdraw-0.29.0/durdraw/durf/unixbox.durf000066400000000000000000000124531476305210600202510ustar00rootroot00000000000000ifunixbox.durf_o~ ! LwCe-u(1ghgONQLy%,|3=ճ/n֗׼Z9rr}uvWW?ܸt~-oWWj:mrs.׼Zeߎ?]_,^]v:[_qwx8ۃG!CB,?=GQ#mt5_ȶG'GG_mқ/.NzTJUu}>&W[>6S>Lvnۇh{+2 \dªϚqeGLjf¾۵߈b==G} nUSdd\Q%TI~7?tt%yljmj 2ˋېߖ7g_݋tJN%SI$ TXX*IL%Iל 4.f:Ӡ&ѠZS28L䑖3S[8D)\"Q-T SAURiP(pppJJ8 *KAU|iP7" |7pppppppp69ZQQQCzԶ{TfmJ=btzTɧUӣRTpzT8=KGUQuDzY攨FqqqqqqqqqqqqqqqqqqqUk;h;5ZxJvv*}=^kT}iNm|R۩T(i;%ppp E8m'Sv*<"mgml\m 6XXXD2la֭?k]쩉LsDTn,8ꈑ.B]#hF`FbAZ@PtAw8y:/ t-7\\A#a.*XqvQ4wMMmY!xxxxxxxxxxxxxxxe ,d o5Z1V6Bir4GZȪ)M7]#jd4Y:::,WDK"+UJdYll[DDDDDDd_>='>: ""FN.[Nogi_ۻT~+ ;CN. \뢮oo.46g&kmTx-e&fNoťmH*eZcRϟ?#< 1}{JJ7D٭(< p]V+v~~勺6ܳג\}>7X/玮@us X}>WcJUᩛUt ]::)BWЍЍ?Н;s;`Tߺ8wn筛v[#U0a{DlZ}l r}$Ĉe_7[DDDDDDDDDDDDDDDDqLk\q8W*sۉs_aDMs\qN+SҩsԦ\\u:W{puvWL]jԹ:W8?;bhIJ_.= zADDDDDDDDDDDDDDTnܧ\Ksչ@u8WR̃LWU_չ\):Wo)s\ynyc@t2un:<~?ss\a)bωU^ш=s\s0yn:WV;z.5]+sS:yO'Хr t]nn6"UZKWUt5]ҩtt*]IJWȉO˥StUL*]JQU:uQ[ҝtUtUtE5]tUJS:bOgOt%tE"]&HW(Eꈝ(ݎtEtE"aDyŢtE2XHWhJ' LDxU]..FWjt5]D `5u."""""""""""""""b[nѵFW5]Q"OU*MT&<<<)JWK':ŠtE"].HWE"tEIG tE"]Hwnt"] `%])DW'2œj`\ ]BWeR*t寈 ]DBqB2 ]BW;B70])jBWU*tUt ]"ȔN% tbX@W~_EDDDDDDDDDDDDDDD Н t6P ;Xy:q l= \r]k6uе5ZKәr`_ԣK,m=.N>I6(^ޣww Zok=b}7 v{r푙0>%Wi-,I}Y1#An ̼W@ҟɨ!Neдj]e?‚䝩ld@Cqia+J"5X=xO]O^0+An''dwLF6K%YQ.j"qE2og%)`V4"x3nNLsf&!Ix<"hfӈqxui2ѐxUP8ȊT e((LT7$TQx+W@iS9/]YWp+<e2]VhNض.9Lkw0TҍEbaӝpś %MaƒgV ޡ1ӝYƼ;Y ( J2Qx3άM6i6=߿zԙy:>ܒ3tc35K^!c$>$oTC;[mƪK>/@pF+c~~q"L܉ǐ s%Hjczz ۙx)HdDa_/+2:rX3Z׿1y%,.b|V{<,jz@l5VFym%ygz@OHH纸 lEbad _F`޺>0}?A#8NCRet^zȜa%;&,| 'ZH/TyC; 傜jxQ[q`v^&toeֶ3׸א, |>|gVÕa~˗n>6m+f+}IR,ŪjS7a(+#NdVrەD }/JH$8n ܏Ի /k?*ϗ8a=}t՗ޯ yX"ЖC 3\cyc m[78ՍC/zPu%S)NuYl.}Wە/k/Fu0 +2ނs 9[D=RlX`\8=' eZJǭS3|gf"a/ѭ\#,6cFvLAZgၤs-̠ieȭtڳҒҩ4\׹( :GY=ĥ3yGeQ[(\8.Byw䮒vs^' ?n3`Am 4wجkG,H? u z+\}LqqЏ+Wҧ9?q|xXb OԻ LײӜU"5qX UtKuma:S~ Z AA7r{-zr w=x dw֓\w ͱ EJje -267׾wDDHوt5 ;tQ^¤OY OV-Bnb$ͷ|[[=c;RO`ea ! =!tJk|v= #ʔt"Q~пaKpRc*+[J>.|4 4o0}<jcQtdRHBXۄ;p_=AR1ۨ\zXNǰpȺ&1?ˊk2GslFj>1 !;TSLΟ:ÞƅaQA٧$6H%ugD R(ӾD̝]38ح/xG;߰+wh/(_n)7q Ktܨxlac~ЁK촡 LgUǍF$.MFQI:aX;qprۥ۠ 6JZ4`caauq{ܫpð WCo/ޝ;˓NoayANj请5˹ i IGk mZ)nI} y@CHͫsjso-c` sW \=_%YO|+GB羿g FZwYfp__ p5r,qla:[4 ϸƥ+cAh;'g/?}:~G=QcaI*[i8JxEtOURD=_)nu$n.YG~6ҷܣNnS&L'*.O+n̎6tu=LZP=4EJǤ .f8/ ^q; ֻI)fʜjy %4O,@UV*kxq/"& jz>Pl]MijUj@17ZsPS@q)uV1OZa@)LIg::9ҤVzIK4aA0O@;hN:d)i$IB ,(&){(穢~BtSǻJRj4h4Usƌu2 [L3,3c$+ Mm!ʥmHpߤ"̔Ú1kUg8l"/̋zRzZuVXw8lq(AQbMmzڊ6Ц Шh{šmm(m+mZFѼgk8ap@VJbGt WXm  $Nh ܤ㼱x(rq¬i>jl$9u4~/ Ԇb }S/ɳv/m/QqP+zsYǬfzIJ#c?[Y2yE:.2鸳Am"{o]8B+5m{v3Ǭg],xLSc(f3!n{M?Xm2NwβCKNwnj:fu81 $UhzЪ\U䥙rtqX5q68geusV整v1x5ʄs){uØkCV֔ әEh4yMaL c8^b B >yNb!=%=ꌘ$] pBx%\Dޛ/ \pԞ#:xbOVSS:"5U oiv,T¸*:T+Ǫ8VoV_e:5))Q UŪp5IuU~}Ku+h!~JoVajXS#1Rzp`DP0SuW{񔳰X8ElYV xҞ&;(T%R \u${T9L*>=M4,0u(W9LU5Q7`w*x𸤢u1?UG(DNP!7NrHESKnZ^Fyj`lqV4us!vs5"=Oz*m=L[h';B*޸XQ@jO=QƾpW(oqLx8SDm_Qq,Z×GXn01RUG=3zO|RyXn+^`U}lˋY3/-Uap\N\u,C+rXbծZrVQcHc,ࣳ}gd!ySmie khzfYM?"]i;!Ī1% _\KɼCK uVٍFۢ!86ّ;zw(SwHM4.44߅Ź,| d]$l,Z/>=k}i݂L cqMi7*Ĥk~RF^әzSWXsmpL6 NJOȌ±]&L23P/5W(Ą@8xlcRm]{:V e, Ȋ Xa&3`.δ]L 3d Mob`+,) "Uf]ܖU25G $'!kȐUV܋TpƘA VD{I^OXL'0@jCX(VQS W 4d'@,NƞI٩֐H^BTtY*dmRdh.2+=)ޫ!$5|C$Hw&?p:|3J`{}=F5aT]= #D@ Klazp:HVb Rݣ:AK. G5iwHw#@'H8 =5 YU Mk+#S[̌63=Tf a'ws]\mE6a"° E2X/#}0@o]}پ?A}kd!ҏtr:TKd/= gd 0}ˍg-|D÷rw9l\v^&toeֶ3׸א, |=Ype򥛇q5㢕m~VVX8KU5զJo,PVGX Y+!@̹_>IpYHw훹*%~#=w+RI_X~T+nٿ;$/qk*{- u/ {_AE-fJ۶Fop_>,ַKSֹdY=ؖ] R3s:+_H_̍` LKT-WeI@r/sz󷲉zD*ؾtqzNL['g"nHDB-^[kFpYlnjtb;IZA4[>g% S{isQu\zK^/g /ZP6pF]z]% O8՝mY0 N6i{uh;l# ̅:[EGԾc 8ǕRM@t566+V\Qxz7}wA#ZvW|Jf9N5+YnYbTm_B:Lg {xan<+g#AYl|Zwa=FofK_=sl3Y=$Hr 3iad|Irlki:3 )2;]/>>O߾pk5&ȵG?71`1]ZOScr-78)皖10BO\7w\\jd9K?#O>.Kd#I(Ez > f&0>Y}O6އm]lMcxHI?%6\&V4t$)!|,"2(SzЉFmJ@އa+M>^{1n7He=ƪ1q| EV/Iῢ11"EASJz^Fo]o>a*_ etڞo i_ҧkh(?pDg#w:6A3p4X^II2н7m~(șQ/1}WȮf/_#UO\R)pci쟆@xT ;N#ࡔ2{&шruid~<9}Ǐ;,~&A?l/ЦJ'a:A ixWzt3-ۢA 8e l==D~W$)? <ӒOOgĠn5>Ǎ86jC_pG IH+xP`8@*f+^20Y$>FqY{۟ہ|sx~*،|b@C7Jɯw$+ Mm!ʥmHpߤ"̔Ú1kUg8l"/̋zRzZ󬩀kƙutmG 6WEi+BB6ln[xtƶ]Mk66-Uhk쳵[kU 08 o +%1#UW⊇-w[)9ĩ0ɄZ78oz-t60kN6nX6ZSe#zԺ@Aa/i]v$EYk@ޣVcY)Hh񳅜%W_#;&;A {mrvdК6=g9 Z%=OȟC,J"Li^C~4Vۃ4,;v|w̨cna(`I4I*^`#j8x,Wp@ykc9pM6@,E~ ֮r6.Yhcݜy']$M2:@^eNPvTm5CLOYY[LgA $f623zM)2u 0;o{TUwͪ3b|t 0 㥗u\zop •S{j%a=YMR9%DVY,JQRm~pPXq[]|}=f(װ(FG-D/@T gkr7$  V-K-*֭*=[S#1Rzp`DP0SW{񔳴X8ElYV xҞ&K(T%RK\u${T9L*>=M4,0Tܘұ.jMg `.3x U]㒊\kUv:A\8I?ʵ"AM-ajy^穁1izQXͥ\͵~PӫsF?5x_O[CM$R *3mEp[Xb_ {bEFjɪEJU{Kcq8x5U//rf.RdϼzVqsfK8Qpձ aEVkiEE[ Ϊ}OQy虙Z3ge7=d4tufzXfXǸR$@|EXKs.!&.6Z9f7 PV`n 6`ڤ gGKeJNSL!ʊ"5`LnP~|*$cswh=k`v 2+Aq70^nՂzJz{VLgJOq\c ϵ w2UH48+%?!3& w0uː$jN@qL_ɣ6+>IZJvX%jt +2`̀ź8v3&̸4U,\+4`ȊV) vs[KW1tl%(!CV}Xq/Sc P:!]a 6'@Ca^v_ltfuS$Joq'b:sȔy1Z:0$T0bT(ՄQ1tP:2Hp CVD*$l^/E- iXZՂ&H-wI,sfkݕ"Ul C>0=`17=>p|V|;ߣS:W4pkB軁N{N-qCb%cpVr&{ψ?6]ƭq}{eώhݳ0>%t=23$eǐOg+S|2 ^AO2J&28AӪyDwJ~ w-J3:uq ƥu(`?u=y88U a3,dEe$ӻK& VmcȨFjMjZ(912A6$'B;3O#Ʊ qNr>3p'zDC.QHº_q!|^VCoG7H [}Y Re_-m9057P߶5*~S8nA Ug]2TΝ%}]ҿFmdn\SgZzj/-H:"}'~AӛM#*U=[ōs \f/tz>9<=q ϗ~Fj&moZ5b>fdǤۘ(qH< VJ=+-Y Ku{Ls:C\z9swTPuɅ3+~G*i8~©lŴIqHIk/CXan/t_ $`.)p-:S0A?,jҧ9?q|xXb OԻ LײӜU"5qX UtKuma:S~ wKa_9k ϯ/bf;,Њ\+1S6[g!@\#hI %;L\cXKәaHI.xgީJ|||݇]5!(F=bYWΰzrnA\9vaHI-8״E&~撼wP#A\~"tY"N2Fa.*Ki5 7JE{M>ܞobkclGJ),43=$'Nwٮca@҃ND0oS> ]i)ڃt)HFz(1V/c[/ڰ~I M<@EgoN( :^~Pk5z|RW~S_}Vq\X/sӰxN=LK*>u\DD#:ɿ8à.HJt由o}DAώzGv5w8?JʖO +M`4|2 L_Zqz<$c5F7=ǯO M#O'?~RHBXۄ;p_=AR1ۨ\zXNǰpȺ&1?ˊkSq?f4QJ~SO95iP3i\X} Z_zy9}Jb$(/}H_Rwy A 2kL58j q.wz ;rb1HEfr= HǍ硫fa>&헑4N:?P0̔~]uܘaDҹdEq8'׸] 2hOO.nڥ s^`{=/Nr\x{YtIy?[F$x567>?IJ]YRNo{Ϩ$4V~[] o_L{*enDj3| ыORݼ$g,*:9oK΃Su z: ZP=ͮQ-?jѥ 'PgJ,@U V5;1acV4ѡ4PzYA+0 |;4I_=MT)Z2P̍Xn&)λ -i&P Sx'pb/23;ozf2:DK*)Q!<-f>RN!8`&Xz1t NZ2 Unm'lԩM)dٮ1]%t6l)3\3cnֽ.[X c#2RsN0b=!ݲL] pBt%D–/[lu:xbVSQģ:5U ohv,T¸*7T+Ǫ8VoU_e:5))Q U©Ūtp5IuUڱ~}u+f!~JoVH^0X#QgaT2*|՞b<)V4Nѯ[&U$~0 s8UjdZ%./y84UShʺOOyS#M2L;+7t,KZu&8Llo'C| 𰤢 u1W;UG(DNP!7NrHRKnZ^FWyj`lqV4us vs5"=Oz*m=L[h';B*޸XQ@jO=IƾpWߪ(oqLx8SDm_Qq,Z×FX n01RUG=3zO|RyXn+^`U}lˋ5Y3/-Uap\\u,C+rXBծZrFQcHc,ࣳ}gd!ySmie kh zfYM?]f;!Ī1% _\KɼCKuVٍFۢ!76ّ;zw(SwHM444߅u,z d]$l,Z/>=k}U݂L cqMi7*Ĥ׽k~RF^әzSWXsmpL6 N"OȌ±]&L23P/5W(Ąˠ@8xlcRm]{:V eս, Ȋ Xa&3`.δ]L 3d Mob`+) "Uf]ܖU25G $'!kȐUV܋TpƘA VD{I^OXL'0@jCX(VQS W 4d'@,NƞI٩֐H^BTtY*dmRdh.2+=)ޫ!$5|C$Hw&?p:|3J`{}=F5aT]= #D@ Klazp:HVb Rݣ:AK. G5iwHw#@'H8ܒ3tc35K^!c$>$oTC;[mƪK>/@pF+c~~q"L܉ǐ s%Hjczz ۙx)HdDa_/+2:rX3Z׿1y%,.b|V{<,jz@l5VFym%ygz@OHH纸 lEbad _F`޺>0}{y2TKmgNCRet^zȜa%;&,| 'ZZoS>fK' /$ rEn\֝L0mgqO!YNzB+0K7} k: E+%Ipj׫MބY8ZYnWC"s +5+#}਻ /7s?*UJF{RV24WܲwH>_׆U[8 HV_2{TaW@[- r%m T7A#}CYoL8խsgɲz-@fux_=DoWl$Zx ΁H _oeJU}?`փ5sq43j *ޟOOmD󥟑r[3Dr:ش1i6&w>ϵ0i"}kJKH:p]ޣ/,g;>^_Folrጲ ߑJ=yAp;ۺ3`Am 4wجkG,H? u z+\}LqqЏ+˥!')kml\=%Vn'nbo+G<ӵ%:4gH͸sjn-V:Cݸũ~]۾tlk7yWG˳2, {1͖zfzHf#&f`:6tfR ew*?_|8G'?}8}xjggM knoÕc3c Ǡ岿[oq-RR 75-c`h$o r'*~F}$]F$Qء &}tM`|ov#mۺؚ ۑ~J|m+ +Mh w}I S]CX9Dd=P۔AW} c,=n zUc#6_Ec4<$PścDǃΣ5~ߠ7#ԕ}jðUx?\4l=!ߢFҿ O"р%QG$u0m,f0hp3v .9e{#oDQ3dz^b>f]_$'O?F!ҟSJ0? LW_M%xOtnokf8 Z_/EA p@zzzaHR~x$%2`/ψAݴk|q|mԆʧ<<־1f97A!9 (bca A+53 0i!hby5|sc\Mrv Aw;!ᑫc2 3r/veHW,ТޔHZ8+z n0C{`7Qҝ%2Lf׀ոt<ce,DŞOgw6=}h}rT<3j>,Ie`3疿vB<hJy8L/yB⓷T7/I2 NN&57uے|{]vF¤TquTK$3Zt.6rtV2 %CԖ˘;iEB:o+uRjA8qWr\E[yprKљx WxJ7]hq%R? n+g[,5aj,R.EPhS di:\)V5Y|H>)W*BK=,ې IE6)5cr*Dq (iG(D"^YS [3aK mG ڎ8mbnV6mݶFi{šmm(m:S+mZF+ 0Ѻek7al@FJbGt 7gݤ}SrSQ 'nqZ<98lzaִ`MkX6ZSezԺ@Aa;JyKKtxcQw27{Ja<Yb bp^|ѪL:+BHhdo8B+5m{S7ht<~z$1>5b0S¬nה?(c? טXr;fԱl0 oίюqۤA왤L/] Z5U<+: 3SX8xXx"? EVq9Fib&fZ- RjG(:AQ)H1=eem2ZL״0&35! 14_Q3VbS"5ΈO%'4^G!p5*W^GL٪9G-d5J̯ 3_wO9 S Ȗ=o0*ic,B>8NZ"5;+x.p^'N"o7MÔ;`SH");Sb|TuKZu&8LlOzW K*ZsISu:ڍBr{$(תY4䆩gTz9؞v =a@S7rm7Y+BMTר~=m 6XPH)̴Mo6~bc*B썻e$*)e W-қǤ3UAՌ=/qr8|I~}d!Y #Uuc=CGTǻ-UWXUƹV Y:?k"[F5Ρ[-DU2"Y:%g=n4v>:wF>FVgf:j͜/ՙcBaJYa].˻;ĻrPwhh4@Y-2،i/ɿ/9w1+9M 2u(+:ԴO1ACA]XAfEƢL`cL֗-ȴ0Q~ܔ{8BLzW +eZ1I+qgl?ibd|NuB_Pծ :%:ztztv`)Zɹ6ѷ?#zv>;F=Y SbLg[Kû/cY=C_+} xv[zB?u'3oA;'n2,sS4wAtDGO yg*ۢ?3YP,b\ZXH V=^aSדJۉÉ3Y6RIVTF2K0Z*lȠke\F9j[aI ؤ:/ތ#YlsH.ļ34b t]rZLf4$ޢb2N<"El+B )6%0 k0>p;ފaTjWVmD"J0<;DYm Dic5ڱ mS݇9>i e>t#`nXDt9\f>0;Cxc$jbwhtg=EV1:Nd5J¶RL0ތ3kMM/"u&b.bj &L,gɻ;'*&[-fF^hfsqޙ*3;ҹ..붂0GXaXE",>>l^ Ҿ5|YӐTYh:|9*%32kFɎ?3_~Éo>TyC; 傜jxQ[Wug;k/@2Lk[kkH}Sxx2oC߸q6m+f+}IR,ŪjS7a(+#NdVrەD }/JH$8n ܏Ի /k?*ϗ8a=}t՗ޯ yX"ЖC 3\cyc m[78ՍC/zPu%S)NuYl.}Wە/k/Fu0 +2ނs 9[D=RlX`\8=' eZJǭS3|gf"a/ѭ\#,6cFvLAZgၤs-̠ieȭtڳҒҩ4\׹( :GY=ĥ3yGeQ[(\8.Byw䮒vs^'ζq,t't:46KOB­ ע#j1S\ryI }JokWw[h+vh.(DXJt-k >Y%R3[PE7,_q_׶/!3[=wMDz,fb;??nql9n~u90X2Aäu9M4wFJOΧo}8޵Yn#[p {0?.a'ǩ1hścgyAfMsM!Zd'm.ɛ;o}x52ʥ'BI%I$kvI]p[Hwo.&1{F?`%}t"Q~пa˗RHBXۄ;p_-4mT{X,xcX8td]_eEmn͵8z`c3Vi (%ީer4(4.,-@/=sڼ>%A>/;s@<} oBm&b n8|;`=]ko~@{Ap"w3L]X FՀd[0Ȏ|]d efJ?î:nL0"Quo2"8LQ ډk.Q'w7P~HMŹqOH{=nۏ'^9. xwrB,O:M ^Ф}x}c`rnpBZ#GsQphVjf`D`yC8Rj7Ƹh[ Xw8,B#Wd<xgV_ʸйﯼYEǽ)֝pV`W\͇&n;K\3[íe63n5ƿqx$XP3Ή/#=Olz$,x|7zýgX}XVg+j?-.ҷ/x&^=SczE2Zq"5^T >S'on^J3х`ik'$'\mypJ.;#ASgaR ǸUUU:%qG-tQR֐9]DP* T**c'&:llŠ&:j"\ϱS9K4ho&9૧)*UK$75ywSO?Ja NE&ufMϬZVיhIR}80 'ULX@ X4'LSU4\K/ƒ.iXKfA<Ѻ :ޭ7U:uVs)@,U34fKDΆ-eyC zagJi^Ub+'8KKXv}@v)؜h%j$Iiʣ)Eˊ4mlj |a}ay qʣZQx\ X;Zl!a:L+ajeʋX7:V)5 W8+9$+ Mm!ʥmHpߤ"̔Ú1kUg8l"/̋zRzZ󬩀kƙutmG 6WEi+BB6lC6F 5*n^s6ܶiq>n>[X #R#yN0c!=%=ꌘ$] pBx%|Dޛ/ \puԞ#:xbOVS!S:"5U oiv,T¸*;T+Ǫ8VoV_e:5))Q UŪp5IuU~}Ku+h!~JoVajTR#1Qzh`@OReW{ ᔳX(E|lYؓV͒x& ۃTR]\u$r{T5K*:=M0,,u'W9KU5Q7X΄w*x t19UGDP!7Mҏ!r!HRKnZ^Fyj`lqV4us vs5"qOz*mL[h';B*޸XQ@jO=EƾpWߚ(oLx SDm_Qq,Z#WFX n01RUG=3zO|RyyXn+^`U}lk%Y3/-Uap\칅N\u,C+rX2ծZr>QcHc,c}[d!ySmie khzfYM?\c;!Ī1% _\Kɼ=KkuVٍFբ!56ّzw(Sw/HM4.44߅e,y d]$l,Z/>=k}Q݂L cqMi7*Ĥk~RF^әzSWXmpL6 NOȌ±]&L23P/5W(Ą+@8xlcRm]{:V e, Ȋ Xa&3`.δ]L 3c Mob`k+) "Uf]ܖU25G $!kȐUV܋TpƘA VD{I^OXL'0@jCX(VQS W 4d'@,NƞI٩֐H^BTtY*dmRdh0+=)ޫ!$5|C$Hw&?p:|3J`{}=F5aT]= #D@ Klazp:HVb Rݣ:AK. G5iwHw#@'H8 =5 YU Mk+#S[̌63=Tf a'ws]\mE6a"° E2X/#}0@o]}پ?A}kd!ҏtr:TKd/= gd 0}ˍg-|D÷rw9l\v^&toeֶ3׸א, |=Ype򥛇q5㢕m~VVX8KU5զJo,PVGX Y+!@̹_>IpYHw훹*%~#=w+RI_X~T+nٿ;$/qk*{- u/ {_AE-fJ۶Fop_>,ַKSֹdY=ؖ] R3s:+_H_̍` LKT-WeI@r/sz󷲉zD*ؾtqzNL['g"nHDB-^[kFpYlnjtb;IZA4[>g% S{isQu\zK^/g /ZP6pF]z]% O8՝mY0 N6i{uh;l# ̅:[EGԾc 8ǕRM@t566+V\Qxz7}wA#ZvW|Jf9N5+YnYbTm_B:Lg {xan<+g#AYl|Zwa=FofK_=sl3Y=$Hr 3iad|Irlki:3 )2;]/>>O߾pk5&ȵG?71`1]ZOScr-78)皖10BO\7w\\jd9K?#O>.Kd#I(Ez > f&0>Y}O6އm]lMcxHI?%6\&V4t$)!|,"2(SzЉFmJ@އa+M>^{1n7He=ƪ1q| EV/Iῢ11"EASJz^Fo]o>a*_ etڞo i_ҧkh(?pDg#w:6A3p4X^II2н7m~(șQ/1}WȮf/_#UO\R)pci쟆@xT ;N#ࡔ2{&шruid~<9}Ǐ;,~&A?l/ЦJ'a:A ixWzt3-ۢA 8e l==D~W$)? <ӒOOgĠn5>Ǎ86jC_pG IH+xP`8@*f+^20Y$>FqY{۟ζmY|8I#SCC>lehƵ+Nn%҉,jI*^x@J 6@E2]3# hy$}~?GSlFnGC$ߩer8j4.]m@/=kVmm &[җԝ9rBwH&$sgL0k,O?! x˜-si50&33ej`/DiC~Lgfm pAx.":Rh6ت8;pHVY+f-\ _l)W@]85RbA-KWG^VEV!զF D.@P 7n׃( ~u?҂03T=Dk! )+ xJקFSv8$~R5@e-{_,w&`[, @=8MZ =1S$^2-yU$r;äNڦNP!L2iPt=&GA&>gj$|\^ⷦs4P)yq~ċ@(d[aWW_QLO5m5N#Mlճ4t{kҽ¯&FL噗6Ӧj6~ae k5UHU RUU]Xw8@˅ٰYtCa'Eu%Ta_"~01RCJ< `~ >+^`?0"' Zbxe5ȖСUᶹl о 4lzHWKn$*zi|iV/2dz2 d5m=Ϭtؘ9+{ 㗑՚c-BLϚK+KkA "leWM_tܧLAl{Ȩ1,ʚ1Y~(ȾV,w)+3 A"61S.L_Z TH",wDK`C GD\Ř K8/o.¤S?5PX)V-`׈!&c{(ZUTbod[Ҥ5sJ־ȏȌW r~/ &hg5xa0g!6=\mMؐZ{> ͫJbLe"2UafRw u,ԑNL̬͐%+K aai$~Ɍ***8bAK #S% ZRmjmAʿ&zatG#qT#Gj0 OZ6(b"Ev j1 ܊$q{ROR}(!@Igi$TI>=$=6 0B sh$a(f1EPs%T9A]ն$F$=uD,ĚTNodV)ASICCgѬ j'dž߷\έ~}{ONwO¹ǟ&2ta?!e=x,o'G כ7f-h@iN~Go4atY3hA90[p*nHѕ)ߛ+Y' ΩD>-VTizd0@nNdwzR|fr]%PcSM}W7*Gэ<}<',MԬRgQtLA%1p䆃A?2 bޕEa%c+ۗ/ׯ9 I(:`pw-ECwÕS s̜7!09K29C̱ 1yOUuGх̶@IVVi'qfoIc%354U#tqnl~ 3r݇ $Ǽ!]-6gՀ͞H6 1v8cp)??98é[gs%Ԍ=c(υ5"ɳC3A`L]sGC+\"?<Boe+gX8toj>똝%).k׫mUyq!grSý]Jҧ2Լ).nj@?_eo%nTFAui>&NU8Xxw$;}Y̗} qgף7ԍGЈ激K-ַKs\ʲzk@gf]ҿne7 %H N ^@%a|FoN6QMH=޻SNO0c6SF<_DgҸuk3QlkB[ ?|38qsCNzH}YTǞ;;:GOJ>^N=oGolm7U2qJ>wpL]t'9{ x,c>_^{b鰟HY\Sx[^D;`cr}i$>7'96AQxO$ |[?L|]/VҌ:åfb%3TѝXp硄t({ox9+򰯼uh/Of;,ЎVSwr4aW ?JVj_LF, rM1ᦒSwƯ߼;ݷ&Eu@A8soS@> lN~>쯱˗+4a$! ӰN=J*>|DD#:"_0;h8< dNNO7Lcf|`w3/_R?%ҿqgKɧŏq? /c;z8>RxEG#+x_F~y'T?%֓xA OixWzrS\-ΟQo]@`%5ob"Hi\~ʀ~gI=9 O.Otz q寃%b=qmR *A`\H5xČy #_@Fjǟ/ fm"8LQ ډk<.F 5goN7PvH wYOH{=g^y> |svF,O:- <hHyٯ_o7,(d4rBcF8ʾXXa G`ސ41Լ:dcMsv ^{!ᑫS 3r_vH, W4ҢߔHVxw`W=lj-q3wis{ 1]MJ+1V΂ʝʏ2#§Eoxϋ۟ajKJ}[RNo{/z$޻`⬨k){3&;+7GR۞Z'oޢo_ MۮImRXroHZa՛VuKZ' ~ ԰x/ e&U"#T&BQ j\M;J5tԖ4V$vMƬ8+K|r!z7v&LLV27˦ID}&ʭPj.V&K8 lÈ5{EChB`ҭ P0RC IZ R>Տйc~GlIKUaf*`U2T2D@ 3M̐BAV d<[T"*Lg\8u36n k֬ndQ9D 0LE0m8O4 x\;ay ʓJAKd=8Kd3z3t qBb?sdbb!Dxy=DƉʨI SUJc SGV̓2% 2B #[ej(pVα Ulmjļ%,SHU_hJ Xٍ~\Q7UCYp<^L) 6G%ʕ eH`?d̒)kxUfY1;ɯCљݶغՁV3h@ H08vB$u&ej* !MƞՉ/Ϟ5ZmǶ& b*FfT_lՆik/ C"2Q82{N=~n:! YTT[Ab[Ik*uȑ H <$F]EHZ$6C,@/f r8A17߂SqC4wMI\:apN% iJ%F(Yw;u:u%ӓj4L ,j22V9n[9ai ؤf:c /9'7U R$<)f,+x[پ~~iHrD Śbe(PQydmT72L#LR7$Q(T@ip_^joC\)W'(̞moeVXv*{aOkfvpxH,"0B^Xc 1YAbe{B;.d5JʶJLT?93{McL//)u,b3vcs=[^!c$>O 9 jA9lmDfK1Nr>+pfDC.IB]=ǰ)ύiL\| dD8ˋܜrX3F׿5E,.bc|xW5 y5!C 16{1uB3KP O^@ n+(z s .rL`^xƻqoOquwCV,4*#~+-ww24pMۯm1v» b߯ԇE-.f+?Hng?0~?5!(Fb™k|s<ǟbO<Qpd`'kośagyAfCwM!d'mR0p=(r'K OxO,N*6 ݦAa,>D'/0~č?oz?bgC⃌s-XXiM3. zB,"2(Sz4Vm[@aT|b,=lzHtC9Wjg+R4/N왦as9g%4^4^gd{`}==[%KN a\(v$';taVW$$JW#s:YDq4a`$#Mvr~f?(ȅ7]}:?/Y;[J>.~48^o0~=cItd=2ǣ(r?1N^SD|V72ߟxӽt? v+&ĻR@U<_xJώcϟr4hIuz[4{-=~ۯ)@OS,#> 1hxvCA7jCqG )H+Pqopt.*7z gaБ{}Mc8.+~7gHdx~&،Y MI~SO)5qԠ i\}3ڀ_zó< >&AL> /;sDs(FMI*aX.zwyޏoXݍ+̎,(݌KnGDxP GY#f\τ4N0R8̔~]u<^`D63ԥwh`RjN\q6jH~??|skFXX]Ϫ}B2xϰ_`fSto{f'wuͿKk@jR:^rTV~>-r~ONj^W[Rzޒw|<{Aջ% lgE7\K W4LYe^I8ՂoT>!H}}h$LnRvMjjǒ|{]vG4PT޴X"Ւ8l3P]f{V-s4v)Q2Iz`HTjP0l †$|u8m2f Y]ߟ=-4WֻŰ7fd2&vIĜ\6L"Z3QnRs4Y2i `+}wF,BÔnnBJH$j~ݰ-8bLZJg3S3HҐ&jlb4dtZ ٢Tg8@ƩS7qYf=@v+ؘ%$`*iÉxi;@lWVgY ˃PT Z"qY",ћoc@C0dJ#U!! 5NTFMڇ]2P:;4=ʵ:d,I J:(3PsDSrebh V#p,fBBSn**?JleJQ>*Vtl(C3!`WLY30rUI~ufAW@Zřep j'5W-PSAmAn4ixxtij;5Q>2m[5;:^ղacL[c|iMi7Xői p{MK&r(M'mrݪ~5ݡǴm XPdlr`!UԚ$i8t|}QV2T=*nj~*,@wmaQ]Ej &9;huYg*-ytn$fPLffMUm2gTxeg @@,ώt(/8;pj4=Tr!!hz8Ӽx7 A^)X&^eʯª3bJbV6X5feBk2*L{dpNJ9F@ +k@!f6˜-oi5 &23ej_/Di#~DeLm pAt":cRdh6Ъ8 ;"JVY+f-\ _l)W@]g4R bA- yW]VEV!զF -@P m׃( ~u?ς03TÀ{v#S0p!]YV 8}rӐ5P8ڨob[a eFB,ǥ8@oHR Q4 6vՖ߆HR3N*Q^[=ʬޱх56%xUwr nXD:yc$3+ )ӝY,^w]lkmef~rg&4Ƙ^_r=SSJYZsojHV5;w;۾% Vʹr&ߊOC _;ooI_ o鲺F }vM\rV+w95UxD }-NJ"8 YiUFV[Feht$}^_qc9.xXewAӗ|_QqWH[]̠;W(v=*~Kxo b}dJ?ǥn,G Nzf6k`U,F_v̍a RJTPH_gdشJq|c; 3n3gT:?σm<_降P~Ktƙ/[w6:؎9 Ŷ&*E3w177DTׁ,Nu]@Q_s{xcQAF{&_v~Cy]%纨qswԅIqIwp';6'E:GEO4c Ƹ9/+ק N"cz2;c> ' |TOBA#w%z2o*͸3\j.V:C zJH\&"[~$a`=5~!w[Of-}uduF U d?0/ ].'CJjn*98_.}0|aͻ}xkBP4\3_y? x ZOSףp,78.i12BO~`d{Q Oh~ALY66"-T|mMäY|O^aH6>~d?DqՏ[r$f\1 =9X9Ed=Ph۶4{QXk{6$'=·śϤ_?&PW-^3Ms<Kzi|i<2^zH{JP: [.HNwìBcwI4`I8G,u0h)HF t?FQ /ofٻv7?%u:-~^*w| \Xi3q`"z00ǎ8z(eGQ ~4b^2nd^?;{,~"A?/С3WMl=w{XxǟqǞ?-7hВh5 h["[OOQz_/+Re XG|b4j;>Ń8nԆ>ʧR]tO'=߰W:YP"ÿm.,UTG̸ ":uitaq)x0lfKn[&088mԐ^~~vt%o>̉pxU d'0a~yF7gg؀̃0\ޟJv~`nBF#'4f$x51p1} y@CHͫNfx0D;]`箾a?% H=*''eW pO#-yMyt oGpW ~}|<ͦFyN]j}:V8׀դtc,)/3"|[6֯w%4FxwK &ΊڏnBiP%R<(-@e(Pմӡ4Q#aJGmIS9l Ib7pdW?{.gZ hޭw3aoBd oM,9yCljDg bid@Vح=X@X4&) 0 0sa!5I Sa1F[jq\Uff]e)C!M0c i!d@ƳE%2/:δq6̥Sn0cfzV1KI$Tӆ+AӀwخ5j <D㊳DVY>7Zq1Lǀ` Ɍ/ 3G6*.vQMBCj)0]eHt:v2izkuH<)Y * 0RuH! QfjPŖF PY"2TݨUuS5T~5ʔb|T\~PfC&,ٯgXa:`mLQh56) 3I.hj')NR8jb_j[֡4iZXӬvl+l0^}!k4kvN5XlҊ0"m %#Ӻ6i=)ȡ4KGpQqcwzqr֐2j^f Xڀu`.Xk k A&H.@RE9A1'j icY)PIJUoB1 ~gU6 Κ5 pF@vuYg*-xtIbf afքY)c?|FŋQ~j1fv̨c~aY(BVӤA왤 ?@Ӄu\i` L9 &Xpհ5",S~VgV9Fkhb&:Z% RbO([AI)H1eem2aC %v.2qFTb B- =^;(1mُ0L֣M./TGDPgpYpM[GxbGV]D P*pܬ++- +Z#FJÁV:ԡxZ=uR˪(*ڔڨ*pRUvz8V5XU֯.]XgZPs ~-D!8`@Ot(xJ'5oZ(̰eYb᫛%ΤLwEI'fK%/JDbgTI1*I& ^Ԣn]Q;`9DPL^R^kTTqfC S %3NOxe̢x++*㩆iz;xYZz֐&ԐnO` c^WuDH"f r8A17߂SqC4wMI\:apN% iJ%F(Yw;u:u%ӓj4L ,j22V9n[9ai ؤf:c /9'7U R$<)f,+x[پ~~iHrD Śbe(PQydmT72L#LR7$Q(T@ip_^joC\)W'(̞moeVXv*{aOkfvpxH,"0B^Xc 1YAbe{B;.d5JʶJLT?93{McL//)u,b3vcs=[^!c$>O 9 jA9lmDfK1Nr>+pfDC.IB]=ǰ)ύiL\| dD8ˋܜrX3F׿5E,.bc|xW5 y5!C 16{1uB3KP O^@ n+(z s .rL`^xƻqoOquwCV,4*#~+-ww24pMۯm1v» b߯ԇE-.f+?Hng?0~?5!(Fb™k|s<ǟbO<Qpd`'kośagyAfCwM!d'mR0p=(r'K OxO,N*6 ݦAa,>D'/0~č?oz?bgC⃌s-XXiM3. zB,"2(K,DD-?o0h*M~>^{T1 Ie=$!+os`9nWc'S/N왦as9g%4^4^gd{`}==[%KN a\(v$';taVW$$JW#s:YDq4a`$#Mvr~f?(ȅ7]}:?/Y;[J>.~48^o0~=cItd=2ǣ(r?1N^SD|V72ߟxӽt? vh|&J'1nok#妸 Z? FmKd)jC|E #,K~yB ]ǧxߍg\B@ 6TۃBӎE7^,q31,:rI||eElɟ}#Ѹ?<|)wI=9 O.Otz q寃%b=qmR *A`\H5xČy #_@Fjǟ/ fm"8LQ ډk<.F 5goN7PvH wYOH{=g^y> |svF,O:- <hHyٯ_o7,(d4rBcF8ʾXXa G`ސ41Լ:dcMsv ^{!ᑫS 3r_vH, W4ҢߔHVxw`W=lj-q3wis{ 1]MJ+1V΂ʝʏ2#§Eoxϋ۟ajKJ}[RNo{/z$޻`⬨k){3&;+7GRZ(ޢoc a&,["Bz5]5KnSu- :Z @Q=z`TK$@5yQo[̍D>JxDPZdQ(ꁡ"Q?iCiFœڲr5 ;7vI5%~\";\{f.ޅɓ i&sr2hDJdݱ[{f#hM Sa a&Bj(! PA:wbگ3i)>L RJC& aƲҐyC(;jgJSe^tilN` d͚٭`cb,j7GH& ')V]kXb1,$ByR)h|Job5_(@glT,V]좚"/(8Q5iRvaʐB|d (ꐒyR&^$AT&@(apB@C9--X¡De M +Q׏4j k+)FX L Y_1e ϰ*,U1f'u(#2:[י:jmHT^ifg]6NRpľ^նCM!5i$@U/Ϟ5ZmǶ& ZmvfT_lV1lik/ C"2Q82{N=~7[c D)^2?ӸZN[ÏI4V%jCѱ\ձ {l?`k]C0$jHhI8:j#y\{0+*IY:1yt _ڶuEF-!4@͙fN{(4YlF㬳gm-xt3Iό"̚0ڼ4eϨxq9؏[Y~b QŒQG;I*T40$Mgr2ț2+aYeWīLUXpfƪ1+ZIV dsZbNJy@ (+k@!f>-si50&33ej`/DiC~Lgfm pAx>":Rh"6ت8; qHVY+f-\ _l)W@]95R bA-KWG^VEV!զF D.@P 7n׃( ~u?ӂ03T5D k )+ pJקD3v4)$~R5<-{_(%XX,>\=8LZ =+SL#^2'yٴU$;'NڦMP!K2)Pt=&/;&Z>_:$L\̴^q4}P(i~ċ>(d[aVW_QO5m5N#{Mlճ4t;ҽ&FL剗6Ӧj6~a Ek5UHU RUU]X8@˂ٰY+tGCa'Eωu$$Ta_"~01RCJ< `~ >+^`?0"'ZJxe5ȖСUᆹl wо 4 lbH)WKn!*zi|aV2d/z2 d5m:Ϭtؘ9+{ 㗑՚mc-BLϚK+KkA "leWMqܧ$Al{Ȩ1,ʚ1Y~(ȾQ,w)+' A1S.L_Z YT2H"&wDyK`C G\Ř K8/o.¤?)PX)V-`׈!&{c{(ZUT_od[Ҥ5sJ־ȏȌW r~/ &hW5xa0g!6&<\mMؐZ{> ǫJbLe"2UafRu u,ԑHL̬͐%+K aai$7}Ɍ***8bAK #S% ZRmjmAʿ&zatG#qT#Gj0 OZ6(b"Ev j1 ̊$q{ROR}(!@I'i$TI>;$=6 0B sh$a(f1EPs%T9A]~ն$F$=u4,ĚTNodV)ASICCgѬ j'\s durdraw-0.29.0/durdraw/help/durhelp-16-page1.dur000066400000000000000000000043511476305210600212570ustar00rootroot00000000000000~ddurhelp-16-page1.durAs6+ݩi[gz贓d-g:ݴ^"2!6/[`=1M!x`fG_Ӓe>$ſ`WBZ:0A`% C tFRσ' 62_Vk7(.,CZ%t"+_X7-|Хf*huvvᎄ9/YD. esďrt{{42'l4\MoKn %cNڱodЕ6cZB5[sdV1F)sG}n|_ݹ/Csߕ|4+sCi w& ^M]y>ReG+ Y?W<}$O&jೢ2JѪK7;V%M]) }C?)иv<3KbG5/J,ʸB!1 WLu1Rg 7q Gk#;*g٤gbokX2G Cԃv9֑rx&Mm*9$@;gWZXV+oiy^+i8>-A?L':?zx׀9ڼxG R9m{#voKӊ&jL# pSo>P }%jmI( }ɦ|ȵƔ"^fVN|k :tuޔٺ-#|:_5yG@.D]]ZUq*dB'Tm+@W/*R9KItLS:?r|dZʽ#H2r? ydNT6nt>ܜZp'@3/'-@k2]FĄCv\e @["?iy"l1FfvE.1;3N!ySIut~[0Q9"ZB{2Un!X E[`?ϟ}5X5[l 5[l 5[l 5zm vRgb ` (&t5HeVmkp`k `^n >Aq"]l}[B=Y, k k8(kCYd 5@Yd 5@Yd 5@Y5Wz+& d R&A^5@Yd P k k k k k k8 ?4tl`P[V嘐?,RxDgfӓ&b ~%ո:;<l 5[l jm `k`k`k`kxI銵ӳ\mnk (&t5HeVfk.[l 5x`k`k`k`k`k`kr\p']E(3L-kj" t κt;A0ѵxh?^.am6йi.[l 5g[Û7gfIdurdraw-0.29.0/durdraw/help/durhelp-16-page2.dur000066400000000000000000000062601476305210600212610ustar00rootroot00000000000000@2ddurhelp-16-page2.duraoF)(Z^.{&N)Hײ0`a6̐?J06Óxl <仗\xǢa0|ޠ/9 ëJQz O!A{|X/ZX>M_d;FtPl2쎧EFR:{Z.(/xtU#Qv3{%j/OCAt>~O5/4Dfڭ^ޭJ:{:vr=I57D*I~jwig^u{~өo/,d2ju{^d4EokU1~xvG[DQ6n;>A /վj^?czwb'b^cgs_(']V!6}>džGXYUv1<5jo-k\e׭]=~uotm^'B]2O"SzˈW/^QxZe{qabc_WeCҿl "}|a^P.ڿtݻq<ǐ ڿڿďϛj/WMKow^}Ưb88mh.R߮ǰArYjvzt48~h׳.+VvjZ?3?Fg|7=DodW+^mko7ߙH֥tqi[JYEӒ˫>ӗi٩EqFŷI7}?ڗfIĞx}ΝD^P|zj6ӇǍq[Ӽ 5TŢ Y^ٗolFWO lP{Qj\..zI~J7tYkm{cFO}B.(K;a5o5a-]N@R:zhvfS3l HRniD #G uʡ: f[f n;%w9 ^p鰧\zڴcc_;\oɭ:WfL#:c[ɑ푷LǞ!о4O5 U;v!\;q ;=pn@KN\N cٜ/c/_GXM?Kָ5_)E:eAZq?wkxE;~JYv0W0+Hn {])q U{㼲?:yu/ķ.SdKfkkmhli]չ}E[{sߔ2ԛGme(?:t\{=L;d2<˳&FBۻF+B*7sv@{$Q9E9]Y#9mXF5UEՇ7ERGc+?-Q@yi߆ksņ=DQΦ6k WJtflMkv_?m1iuAZ{_OY*Y*XLo/Kj`zZ+Ӓ ͥ:RRRRRRRRRRHMRA 7v ^RATTTTTTPT8 f9ؼYj;V~b[&ﱍ.kyo7U , o{ q\AWWpd\A3MRG%NI++hX́`O\WWЀ++++++++++ -i K jic cWP3 jڵϦGqs * "ٵ>H3!- "{Z- "WfWWWWWWWWWWW׀͢] B7 x ]@-)KfX+P ,J N'sbAMs|rk2 ZGhC`],,,,,,,,,,,,,,p,PR,k`AMV```````````````;`"Xt}fut | bN\X,,,,,,,,`R=i,x_,hg#Jɢ63 XXXXXXXz XPρ3)irɬ|C@o0 b````````````AE`zڿebAnv`hXn X , ;<Uw ,8,,, ,] Lwt?````ߩ[ ,8+0|S ,ϭz_ ,>^nKu,ȉvΖ/68˚s\\\\\\\\\\\\\AI\{״˛x;VPV+ ͡XVVVVFgx++++++VP4𾱂]qoL+=r *DtҜ@{ATA-[>Z6U~:TTTTTTTTTT Z[A\9HIED'@@@@@@@@@@* * rO******xVy9 XXeY!VVVVVݰ ߶b``hL>vZbVA&ރUp.gq֞isq] u*xx-wdurdraw-0.29.0/durdraw/help/durhelp-256-long.dur000066400000000000000000001130141476305210600213040ustar00rootroot00000000000000J5gdurhelp-256-long.durowF^_Ƨ$m@fDON۵sRF* n3HB'1;a>ssz2|+cZS,xukbC|O;3۸7l )woaTFݘJpc=h횎zކ `ǝ14\.kkpe:LVe9p{?3l$ 32& \\WSwU|:兵 ' K ? 麽G@]_N?}ϯog ܻo8Wb < bN?6WSQ_3ŗ .~5n 7a~3V//>Bp,օvͺp'*NmWx72]kGMM*LEx8{̩c@A8A b]? i͝XDLӍ0#[{ƿ څZLi/w5Ku;xaf_\Y>EA&Hq.#.=f3c:ǙQm>]?CcC#jpN>G <}0|p%.-s#(,X;QYӨx|m]]Bk֮3CMXR&4`W~gt `Oqff`!&UwV&i h=l)OʖyRGE8K砐˃m%&wTW3߬VN(z.Er<_} 0݈ ytttn#WBXW͜ t.ЖKLPŵTHȵⲺ^r>w Ț8)ePmxO Ț ϧ#+mSL/褔 By*:S~_ 4!oFp*:|&l͉յpszo x=75 "P 5 !( l}ܽ7A:,z- 2 }GN!Q Jga> ^/ %|Ν^OBЅp=Aׯ [7Jj!BN҄c?b/ͥoz-j#jz$G P}܂Z6ĹgM<v~m ?'Vh ~3*GB !e{](_ݾmt>" >>>J#1>'x4Qbjoփw;7(ۯ?4IQ C8goϨ8/n=> ۼw$ܷ^q5b[<;y:8)ŷv-b' G^2Ժ~{H@3n _A }xvP8vN_8FL/`ud@Ee#;S?s F}~ $1ϱ +HK%TtȎqʏe9S>D,ZMby uѬ4潩3:T|w׃fY?ZP~B13>gePB/0b0% qeב~4:n M6+4I%P{^Apj=/ ܳDpقb 9E^::>}4F)6n0X3d2T#ء[ϱvlh^J{#[yoezJ@+<9n3N 2UPV< Ӥz^䍸{#ZG!?¯it#t,pQ45 ?dsW;!Y5l >??w/[ %. ]#fḻ#٣/}^/N؄aMtxɴx#My_bnrT;GfX]\ܞjym /YdɁ|@}DI'A|/&@I-R(GOB#3JCe-)"@`QE\(P3| h$55 fF"(Yt"|qBNb'*#HEq׾³y+vTfBHRiiu0eⒿ)rn6(쿮HB"1i.E0<闄!Qnb¯KyDe]Ayx EdPi,`OFuE2UinlQ^ KՅFLm0^xvєK4ۙTviTf@()Ƞ iLL"靜eAi3\D3${2u-8l2@x$JcP.Q m k, J+95~>&Y;2!L*}٪0Uq3C{|iwJ'r($d1He9e4 .QF!9cY(TkpySatN5 \4rX2Rx1C@4Mf(!9 45"HW31)/ TķSƾa$4Ɋ1RNIڑ4O>X9N1YcKdA"s`yT[R;0#ѨnTcd2[LH՜0pHUE M_#fط'1V"(6L[̆bi@LyKHbaTbHl9l9âC[YAՍ㰥0tsWԆ`8yGu!y>FEr 3ʈ6]䱺82NKxuBT*9|sY: T/EYn8H{'Wa|r,ē\VP}S}iQ$ZR ߪ((*1$VbiQFtkHd_D"1 Kb%T$1Wf+b@3OH^S%ZoUaڰnӊl,RI[BߚM}Tab1eE?2ʊ\Y4 Ǚ5 <.fT9'{aas,m85X7R2,y5/ Rb, vԌ]ivnS"qsn trMe뺌^:6g?îQfRGIg`-R|QWZ ] ?[ut[1zv-u(n)h1eZ-vG2G(nT3U|Z/D3Ǡ16Ye/87?ۮ/jmb>MO[2$eGVPEe>FĒ$e-}\~jУSc4;foN5%e/ߥıcK@ژ6V\}.ړ;As^nS 孩;MN[RS.\Ji29~[e]f x L\0>dv2Ͳs+yEkP5㲶]-Q[,a% 6X$tqߘaU]bptH$wMb$(/x- ՏJ:NKR#)[ܲU![QOȕ@8]-]Eh#E+.a/c8W>wHԉDARŅ*3S4UT'Uq[@Vv%@E1ƣVJUV FIXH?Lo5\>Հ[t`QJjr`d}\Zh:nhmx?3콁V5oVڃ_V>V' K ? 麽G@]_N?}ϯog ܻo8Wb < bN?6WSQ_3ŗ .~5n 7a~3V//>Bp,օvͺp'*NmWx72]s_>lj>jiM`Qa(9s>lMw ;5 G? +HISZE5'uE6Lk" wfn3PԮf]ǵfL{Y5laf_\Y>EA&Hq.#.=f3c:ǙQm>]?CcC#jpN>G <}0|p%.-s#(,X;QYӨx|m]]Bk֮3CMXR&4`W~gt `Oqff`!&UwV&i h=l)OʖyRGE8K砐˃m%&wTW3߬VN(z.Er<_} 0݈ ytttn#WBXW͜ t.ЖKLPŵTHȵⲺ^r>w Ț8)ePmxO Ț ϧ#+mSL/褔 By*:S~_ ؓi7]jᔥ%W[ }潿kO#Մ殐tRhh/+'vo7:Xw uNe=7X~AU2U@0i<.|ɽ1խ"AT?]|w5<={7\KE5wP% rgi=G*]r(3>ԝZS~6Al + N uSU8ȨX glٮ>wAm)Z~KԪȪ*ֈp7x!r^ Q~(O~KKiϖ d (,).YEA6(",|g! I8={뷏V&|L[/ZA\e*W r&^~i.T~skQuQSk޴Ġ$C6PCyJs jOSSi 6zcsfzDYI+7X u_cͨrq ".$ ux|%v"oN3P `8c(4<coxGC!*:j=x}^N~"j!9}~qAOj'>oC~og4{ c+V灠M&o TT=r1EMO>m!z=- 4WT9AHbcV񡗜KZ }ws!|;?/ }a:U#o?\ S|РLYPHs1r3'x2 T{۩![P*VêTs1ԾG³1otB w/Z{Y($G3|vcם9nw䕟e?.Ɠ1fPc|~ׁl`g=2 -t cE~aDL`#w_c${3~x۟= p6)/o)KMQN{ +Ý]͖k~"oniGO*uzC<%Ŗ.`ęΰSR6~GP$ȩI4 eؑ aPViڌKs(dV<)@!yx%A*`-1HpɈ27 BZ#˵p_vao Q+Mry(1GcJ'EiTQzC2sxiڱ%N qiN0nr$.Ead֐an' MEb8ռ0柔Sv$4F#yNcKdA"S`yT[R;0#ѨnTcd2-Ԇ1@wC*ln'&(UH15 ŭF1 jp1-m -MRBl9l9âC6"+q栐n2b 6o4-1*/ViTΏIn%*>sk/O{mBH֨XㆃHK2Dw$IǛ ƚJ%f`03sfGHCУX#?zTY['0|hֲaC/¹ ( xiC[rnOY Vh;jTS-2i ]\5.:Zy8kFa+^4jnOZ= @Ҷ3^v9zhQZΨ*_U$^72gy'31Py03JFk6?%$l-i3zZTذdUEh !>u3G&4@"G%k(bՌU}m$cuqFk5eQӗ5VD12$~UrXnuNO%Xh^ͳ 5q"quJwLOF$D1h Y6'˭yd1q*O"oDabIC L$ϻaXDHbg9i` 2Obs|X_A'3y Hes9UcCQjQlUbIɭT |֐Ⱦ҉EbcK0I2x5cc*1Vŀʹ#f 9Jު²a3ܦ1X@95N% fn7bcʊҗd7#i&3kxr]1.rO-vUa,o=qjpo^OeY j^64|M=nXr̓ݦ.E䚠;u ul~].2̺:I[*$+4:~u膷cJ u9zѕ9Z2 LQRV^c-L0[c͏dQ L9fTg8(!_J/BfActm.>_Lqn []_>j۬EE|4FDe}-{IIP`N)T}%/H&m }Z|Ghv۝k4Kb^Kcǖ51 .m+*&]5'3v6(5,ѽqݴq} fw:֝>!ԥRVA] er  ʺS%A"Q`j}ɸ"meq38eV֠dkemW oD[j1*6X8Jk-m:I(㸿18H㛮H|qQb-^\ZЫ/N}euGSTeBD+[p>=ػLW[jF@W&] ^JqR}ˑy3* U g8h2O$x9㶀KbGyM8"(TǓc't;߱Z~iH)k:y5QR}fnuz׹RPum9l 6+ЄS-oڣ~xg{A V5oVڃ_V>V' K ? 麽G@]_N?}ϯog ܻo8Wb < bN?6WSQ_3ŗ .~5n 7a~3V//>Bp,օvͺp'*NmWx72]s_>lj>jiM`Qa(9s>lMw ;5 G? +HISZE5'uE6Lk" wfn3PԮf]ǵfL{Y5laf_\Y>EA&Hq.#.=f3c:ǙQm>]?CcC#jpN>G <}0|p%.-s#(,X;QYӨx|m]]Bk֮3CMXR&4`W~gt `Oqff`!&UwV&i h=l)OʖyRGE8K砐˃m%&wTW3߬VN(z.Er<_} 0݈ ytttn#WBXW͜ t.ЖKLPŵTHȵⲺ^r>w Ț8)ePmxO Ț ϧ#+mSL/褔 By*:S~_ ؓi7]jᔥ%W[ }潿kO#Մ殐tRhh/+'vo7:Xw uNe=7X~AU2U@0i<.|ɽ1խ"AT?]|w5<={7\KE5wP% rgi=G*]r(3>ԝZS~6Al + N uSU8ȨX glٮ>wAm)Z~KԪȪ*ֈp7x!r^ Q~(O~KKiϖ d (,).YEA6(",|g! I8={뷏V&|L[/ZA\e*W r&^~i.T~skQuQSk޴Ġ$C6PCyJs jOSSi 6zcsfzDYI+7X u_cͨrq ".$ ux|%v"oN3P `8c(4<coxGC!*:j=x}^N~"j!9}~qAOj'>oC~og4{ c+V灠M&o TT=r1EMO>m!z=- 4WT9AHbcV񡗜KZ }ws!|;?/ }a:U#o?\ S|РLYPHs1r3'x2 T{۩![P*VêTs1ԾG³1otB w/Z{Y($G3|vcם9nw䕟e?.Ɠ1fPc|~ׁl`g=2 -t cE~aDL`#w_c${3~x۟= p6)/o)KMQN{ +Ý]͖k~"oVO<'O:Zb OZ`0uri٩O? L *?rd(P7J 6r.- Ǥ-h L$$cw('NCfIA aEM2DGMŰ7\dk}9S)’W.a[i:LUPEVqŤm,=،r _;QE*SjiJ q$R63]M9ԊcKe7DauE2$dgOtI.Br1 4fT=S8K5RQ!DWG%4הҒ9''Di8R"Rbdw4 GZG U1x,bjR5#3y4lY@7rn4zN5iaN jүSYpZ`1NJSj16ǟV6XBs%T\Ub]7T+Fv#(mRLywH)kI,D"g"e闭(B}< RiBܶ@b>$Щu]4 e aJvB 4#Tm`HaI2A $~$xgDePmqo&b.ERH,GiGxLP7(jYʈ6]䱺82ΥKu:QE L*_9֬[cv Z`zvFMH\ҍԓѫ0 Q ?Z>G9@MI.r+AdWLũʓ#1QXP"9Sss6>snvX%&Q@bi Y$fGbӥo:em(־WL>ô(y\oPZ[}gr+UbvC4(}5$/r"eXG%1L|n{k^MJ̫b1rC'$*lm iE 6@$-!PoͦS@0EeؘeE,r\xL ēf]U9[w~,S)yK`<y M)_S[1v\cjƮ4;d`97:&IJu]FABaר nNR30J)>(c +tqݭ:-RE=yB]t^te ScUט2z Sm-;j##vC7SN*>NJ-EҋP"c]2SŸm򗏿cZ6kzQc1ͦ'ѭ`E_^R2#+S2U#}Gb I[B_>.mr5_ѩF1ev'͒زRر% f m̂K mwlɌ n ˹@tu\7)@܆TfyYĝuu-uԩUPa.%Ǵx䭲.ikqV' K ? 麽G@]_N?}ϯog ܻo8Wb < bN?6WSQ_3ŗ .~5n 7a~3V//>Bp,օvͺp'*NmWx72]s_>lj>jiM`Qa(9s>lMw ;5 G? +HISZE5'uE6Lk" wfn3PԮf]ǵfL{Y5laf_\Y>EA&Hq.#.=f3c:ǙQm>]?CcC#jpN>G <}0|p%.-s#(,X;QYӨx|m]]Bk֮3CMXR&4`W~gt `Oqff`!&UwV&i h=l)OʖyRGE8K砐˃m%&wTW3߬VN(z.Er<_} 0݈ ytttn#WBXW͜ t.ЖKLPŵTHȵⲺ^r>w Ț8)ePmxO Ț ϧ#+mSL/褔 By*:S~_ ؓi7]jᔥ%W[ }潿kO#Մ殐tRhh/+'vo7:Xw uNe=7X~AU2U@0i<.|ɽ1խ"AT?]|w5<={7\KE5wP% rgi=G*]r(3>ԝZS~6Al + N uSU8ȨX glٮ>wAm)Z~KԪȪ*ֈp7x!r^ Q~(O~KKiϖ d (,).YEA6(",|g! I8={뷏V&|L[/ZA\e*W r&^~i.T~skQuQSk޴Ġ$C6PCyJs jOSSi 6zcsfzDYI+7X u_cͨrq ".$ ux|%v"oN3P `8c(4<coxGC!*:j=x}^N~"j!9}~qAOj'>oC~og4{ c+V灠M&o TT=r1EMO>m!z=- 4WT9AHbcV񡗜KZ }ws!|;?/ }a:U#o?\ S|РLYPHs1r3'x2 T{۩![P*VêTs1ԾG³1otB w/Z{Y($G3|vcם9nw䕟e?.Ɠ1fPc|~ׁl`g=2 -t cE~aDL`#w_c${3~x۟= p6)/o)KMQN{ +Ý]͖k~"oVO<'O:Zb OZ`0uri٩O? L *?rd(P7J 6r.- Ǥ-h L$$cw('NCfIA aEM2DGMŰ7\dk}9S)’W.a[i:LUPEVqŤm,=،r _;QE*SjiJ q$R63]M9ԊcKe7DauE2$dgOtI.Br1 4fT=S8K5RQ!DWG%4הҒ9''Di8R"Rbdw4 GZG U1x,bjR5#3y4lY@7rn4zN5iaN jүSYpZ`1NJSj16ǟV6XBs%T\Ub]7T+Fv#(mRLywH)kI,D"g"e闭(B}< RiBܶ@b>$Щu]4 e aJvB 4#Tm`HaI2A $~$xgDePmqo&b.ERHlycJ߀'EiTQzC2sxiڱm% (&= TĪ(l[$2#HV`T,1Lp$ʳ6hw Q'2z!" 2C3*1Xo>W:Hq̣’| &>U7CzlvL%U P9ẃ!T[@X6l۴"KT(fSߩDA fXlLYQLf"Wz9MqfO <UN^Ů*x؜G[;N ?메u0KD͋†ǭK;]15cWysHbsܜ\tGbٺ. ϰkìbGgaҭR|Q_ZK]KGut\y2v,q)[*څkXFf] ##w7SN٪>NJ-ҕP"+2SwcZ7kQ!d1Gͦ'ѭc_ _R2#32#~GbI[W__/tT-ϲxӓqwR %!ŦmK .mxlɌ}n%˹ At1: nCykQ-NSǺG:ĖT*K0cZLο%ydVY58<*Nm;7]-1;nyFIJl͸MjmhKTm=FƲ  g2qXIzmúI}l5:8ṫ0骃.Dr:F$z k1_\XWKS_Y%oǑ-Un٪'QV O.UtZ""ЕI°RT}{r$D hB™)Lh*^θ- +qcQ^S`%+J#U$ f|VxyiRGN^M?Tj-:(%5[02~j64[-f<8;`孽{[{4 {o 퍌z+tkSׁ7+Q/+}ya{uo gHutzw֣X/_X'AW7XLY7+]ar^A1'HSI˫C(G/_bЙŊEK|7b0cx!_|;f]+9}  655״ɁP0bޜ9u Gk~PSWP$ك$)Qp-|"qЃĺ"pwfO5wb;3M7ďlHg(jWݮZ3ai,zIݚX03a kj$ Qĸwh1 }Y(6Ʈ1aII' ܄L>`(iT<..{!5 kיX! &,)sS~+?峉WN'8Ed 33M{^*;+Z4Y{ 6B” eؼ_#"%sPÊ ;*+oV+hXx'C~RQR> mJnDCm{a:@L:I 7^ u+To!Bq,yJfNGc:hK%@&YYhZ*H ZqYD wq9;pdMuٶ`'e Ed^ք ӑ6) g~tRmY?/\qE62 W\]} ^sݵ'Eۆ1yߵjBYsWHI:)oV4׊ea;PT_p7hhً;:'c QXW** A4_RޘD m*] ?p.Rkޢ̚;udžx4{#.9tkN)? e 6ٕJѺ)*n`dT,3lWΠx-j[%jU {dkD8^z- 2 }GN!Q Jga8Į ^/ %|Ν^OBЅ h~^o7o!(?G9qJDD 4N*뵨:P)5GoZbPeJ= #ܽCwsׅ_ *UeXc!hvJ=1ʕЇdz@&7**9tu߁"tǦ'^6F|~ $1ϱ +HK%TtȎqʏe9S>D,ZMby uѬ4潩3:T|w׃fY?ZP~B13>gePB/0b0% qeב~4:n M6+4I%P{^Apj=/ ܳDpقb 9E^::>}4F)6n0X3d2T#ء[ϱvlh^J0񪑷)vh>hPDP <=Ԑ-CqQ|uXHaUj_#٘7:Mzg@ވp=Quy#ڙ>ΜA;ODz EɘX31PP>@6wu[SȖPsnhh1X"U0"`M1=[_w|ϿMkKL7єW%VM('@ŽstmŕfKf7+V'jʧq.lӚw[y yG'K;N-}!gbjV#CTjp$v騐nq1D<&nGa&!q"#C9q2 fH bKj!>X*a"{P̿iƟJuJa‡*u+&m#eq_WOfSd؉,RwluPDxLVJ#JejʡV3\_' +!%;АȠ}ZKri1{)q$7ҐQ_;'_!87.Y =?Q$JÑɐr#a8:b- _q=cC5,V3i̳abs~I+ۖvfP~"ղ(oMVR9V6XJs%T\Ubm7T+F#(R,DL]Kb!9ۧV(kSl}(F > _Ka! En{ 1|ƺ.d2)d2ї ӌP';!=DŽS<2AM<4$~$xgDePZ8L/};47s(ʕڦG(1GcJ߀'EiTQzC2sxiڱ}3wcLsʄI=u#mD<`$4Ɋ1j`˜&OIڑ4O>XQۆV#R7H?"l ȭT!M֏f(xfH5R&آX11ADÔbQ!" Cϯ3ƴ)4抭q'pl107eKDZIJ'o7vƖ,MXՑQvkT"yFE$r 39U7CzlvL%U P9́!T[@X6l۴"KT(fSߩDA fXlLYQLf"Wz9MqfO <UN^+Ů*x؜T[;N ?메̵1KD͋†ǭK;]15cWysHbsܜ\tGbٺ. ϰkEYQR'tKd1~ƕBB:Vb~L].:G/2GK])[*kLfb;)'Ռl'"KE(1hMg)MObk߱Z-5ౘfV̢e/) qT)E#ɤ-/BYK_/TβxqfIlw)qb6fzWED򶋻fdeF{ : nCyko,NSǺG:ĖT*K0cZLοydVYY{58C<*Lm7]-1;nyGJvl͸jmhKTm=FƲ g2q|XIzmúţ1V' e7CtAX"\9#I=z|ӵ/.Jŋ+^KzҩHb*lUVԓ(re+Χ{*CwKW-tHʤKaX)UϾu9u"oQeuTqJMU&UD/gt PQ(@|Ux}ng`q3;V+O<4)#[zM@'&a/EE6˥xK.ﴸTuhCc<8;孽{[{4 {o \퍌z+tkSׁ7+Q/+}ya{xuo gHutzw֣X/_X'AW7XLY7+]ar^A1'HSI˫C(G/_bЙŊEK|7b0cx!_|;f]+9}  655״ɁP0bޜ9u Gk~PSWP$ك$)Qp-|"qЃĺ"pwfO5wb;3M7ďlHg(jWݮZ3ai,zIݚX03a kj$ Qĸwh1 }Y(6Ʈ1aII' ܄L>`(iT<..{!5 kיX! &,)sS~+?峉WN'8Ed 33M{^*;+Z4Y{ 6B” eؼ_#"%sPÊ ;*+oV+hXx'C~RQR> mJnDCm{a:@L:I 7^ u+To!Bq,yJfNGc:hK%@&YYhZ*H ZqYD wq9;pdMuٶ`'e Ed^ք ӑ6) g~tRmY?/\qE62 W\]} ^sݵ'Eۆ1yߵjBYsWHI:)oV4׊ea;PT_p7hhً;:'c QXW** A4_RޘD m*] ?p.Rkޢ̚;udžx4{#.9tkN)? e 6ٕJѺ)*n`dT,3lWΠx-j[%jU {dkD8^z- 2 }GN!Q Jga8Į ^/ %|Ν^OBЅ h~^o7o!(?G9qJDD 4N*뵨:P)5GoZbPeJ= #ܽCwsׅ_ *UeXc!hvJ=1ʕЇdz@&7**9tu߁"tǦ'^6F|~ $1ϱ +HK%TtȎqʏe9S>D,ZMby uѬ4潩3:T|w׃fY?ZP~B13>gePB/0b0% qeב~4:n M6+4I%P{^Apj=/ ܳDpقb 9E^::>}4F)6n0X3d2T#ء[ϱvlh^J0񪑷)vh>hPDP <=Ԑ-CqQ|uXHaUj_#٘7:Mzg@ވp=Quy#ڙ>ΜA;ODz EɘX31PP>@6wu[SȖPsnhh1X"U0"`M1=[_w|ϿMkKL7єW%VM('@ŽstmŕfKf7+[U'jʧq[@OI-` -,wX:q^}aҧrٔ^92 (IGR9] gSNc V~K&/R7`֏| & k"Z꣍զbF.ŴfaɫYA04*|^Nib6RWE~Dl 9J毋"|ǩ_ VW LǴl8["Kj1S|"^3 | ڧE$qoWGr) lE%|Bu)+qUkJAQiIГE4~] )G1;P##j׃ <1Tk5c"Ҏ)B$ܵ$"}aIz ևRi 0aJP!N `IisLe+4#Tm`HaI2AM<4$~$xgDePZ8L/};47s(ʕڦG(1GcJ߀'EiTQzC2sxiڱ37cLsʄI=u#mkD<`$4Ɋ1Xto2ڑ4O>XQV#R "@eDn~Lid8%͠jLFñebc)hgHCVE,_-f-iSh[Nfla1-q1L806I7000V c-;Ccb,ʨ ps;6*<_"r9lFf NJ̆a<<%I- ?;}a6&䘱jJ%;#2o+ڒHAssfRHHCXN#?Tk-Tp$H1>΍Cߞ=jm3t,3v\ \2#ZZZθ,U4jpZ@c9mgrtrQEA aX\_1?&F+Ej-eRŦl ÌFc)d^?̌ҬqE!<.`8fݫ:gu]!H ^~j/r; LYOYRky<骔CPػ(ViFkO (&rی޲V!U0<eUDh1?ڦuG&4>"G%k(bՌUmm$cuaFk2ekӗ5uBT*9r.i>=fg`M:( 6ͪw73AG9G?MI.r+AUtrUDpĒA1I}*0J-'+Lr 1<d.Ŧ9,kC;͟3LKL"њjV EEU'q&R%f7Ks2ڧ[Cb-⠬B[A4QaIL!slWS=6;*lX [i kD ,[6mZ%P*@yK[T jv3LQY,6(}%q3BY+ a(F'Ă*d/bWv$a2;!CK,n:Z%%RW.ip[q$1E`K[*d+IӃtݜmteҥ0c*g_v:7:ZPpf**ZD3n J (xj Xc*Š?HUp<>vB10'^vw8_LbtO'grf$&=]bc<Ɍ?W $!^l J^]^DRiܢRP# M]Jˆ@Cq+5ج@gZڻְR72&3\\ZSט\^jY kW/w[ux \[^(8/EB70pƻ]}"8#?fp\iKr?&9AzJX?H<_\^Mu'5"|z˗_/V/_{(}^Hǿv/_^=}Z|Y ϋuOTx%]t=|MH^5ME|8;̩c@<ñnCװP}p_!Õp%Iv?I8zJ\ Hܝƣi͝XDLaGA$3nq4ӟo,ö“Ckb~ seEXS#)_'Ɲ{?F͌H2fZDy?v I  MJ:Q&dÕ(ύ`D!|d=MuߵCy;PXz QLiM.&^9id#4BLxwmJЈ Yt1$ x6F6u/Qű*-9\--(dek"1keu*|5q(R֡n֓C(k("k&O?ķIL83hRn*9O~`y32hS 7QekNP> 7wVpʏ'cP -r}ei> 0GJ2<u{8nߙHFTF=|\d"چ1yz-!2 }GN!Q5JgiЗ ^ .^J:[t  z%z%-^KBu}.")L8'\:ע@= iAImӕԂoCy04Gc6#OMZG ToF%Wq)1ToKؿw{=/vW6۹j} dL]||TGc| Nhpkz^ͼӹU_Hrg\H޼?;~Oj^fn{H7PP; ZVP7ΰvlh:=;/ }a:U#o?\ S̓yCA5JcfOdLUA EU({ԭb!UPBIOԿѪ =/F>+rg#{ٍ]wW~Z}(ĚAm =;>Y5l >==n>@8Kp]*F ;q9FG_6,2ze˪ND9)}AE T[x:։빇γ ~( fU~GPG2~^l33qߵC8f[&Oz.OBCϑAL<4.McJ@Hn:!LtPO.Bt9q[BzM!(3<'$PT.O(5Ñ#nUxai4PL#ϒ %FF^O4۩&-l[IAMu;Nˢr,FXQwJ-r?)lJ6* E]7T+Fv#REJ]Cb.ۧ(kOl}FZ# > _Ca. ea[ 1|ƺ.d2e2ї ӌP';!=Tu9(dV")T@!yh&I~$xgdUePJ8L龝jƛ\4j鑼s<* 7mI^:xސ;^vjMzӜ*aCG@H|;yad"a MEbQgI HEkhH(lǭԎ! HC4j"8FԒDSi* S5b}Ĵ)(r [bfCĔ$6I%666a qPA!ݼD 6.$( ®ZL4[A`Q>9n9J7BVpv uk&p`FD^iY0$7D3XXS s $UΝ6! uBbm ~~;KD÷IZf=6b`7/mwCZmF5"co5^"!Mz˯jSxШ=i50#oi[ęN;;Znz g|Q{JTMد*tGquBn[FU3ێǍ˼L^FkP; !lJ6u:zuq7rCu=rG Ƭvâ-Y͡Wiʈ6]䱪82xgtmfz:Qy L8|s9=f`Ez)r6w <Gs'Wg/u+dJLBCGsd$pY[ "b>NeDpĒAL-$ϻa^%'KLDgi`52Obs|X_A'3y ShrH5|†Ҡب8[Fӭ!}% !mb5_)G1Ev'͊ܰw)p|6fz9WED򶋻zTΆn { : nCyo,NSǺG:䆺T2KK1-^oDsUe֞: q5"NiGU₩M&㶋Ŗ>fǍgp4έiA֌*vDaTl,[opLJ6[<cuQ&pc1 HWtQHI㛮H| Qb ^BZЩ/N}euGS4aDDY([p1=ػLW*F@W$]={Jٗ@N *!T3%@SI~"Q%d%n]TcmJЈ Yt1$ x6F6u/Qű*-9\--(dek"1keu*|5q(R֡n֓C(k("k&O?ķIL83hRn*9O~`y32hS 7QekNP> 7wVpʏ'cP -r}ei> 0GJ2<u{8nߙHFTF=|\d"چ1yz-!2 }GN!Q5JgiЗ ^ .^%|έNGDЅsh~^o!]:t> r&^~a.T~skQuQSk޴Ġ$6PCyJs jOSs!<@ģm&ab?*Y~ B^X7y+ŋRZv*ץ~̻ǽwr+Cn>2 Ƀ.>v >*#1>v'x4Pq8@CGfܪQ$_.$oޟx}NjR'5:q={FyЇX)pH}A0_Ńתi]| a"nb}z%COAG{ 43n %T˰>'>oC~oglk c+V灤O&o TTr1EMOpnCzZ%h4?/[rBW<K /9P^!;fK?ENy k5ힳw,7=G44_89[IL0"z*Z-=kAaf̴ ( U~GPG2~^n33qߵC8f[(Oz.OBCϑAL<4.McZ@Hn:!LxPO.Bt9q\B~M!(3<'$PT.O(5Ñ#n]xai4PL#Ϣ %FFnO4&-m[IAMu;[Nˢr,FvYQwJ-r?)lJ6* E}7T+F#REJ]Cb.ۧ(kWl}FZ# > _Ca. ea 1|ƺ.d2e2ї ӌP';!=Tu9(dV")T@!yh&I~$xgdUePJ8L龝jƛ\4j鑼s<* 7mI^:xސ;^vjmzӜ*aCG@H|;yad$a MEbQgI HEohH(lǭԎ! HC4j"8F$QԆ1@wCJ1t~anNTJ$EnôAqQl(6) )oiIl8Jl8l8l8,üYAT㰡By\!mQ]HQ]hL2}r0?'X T rl ?p/Mᘋj*XԿ,CtG Nt,aEʐHBPiP'(Gj#4K>mwMz4ٰ' ( xiC8Ԋn+l{]rP{6qN a jBs<lԞXJq&^5_U Q\/AP]kFGaq#ƫ2/fưWA5NͥY7n>=F\:RøupۡԺ؎cVune.Ы4v&Oy*A(mS$4_#̊秀DMSrیF%U2S 6*c?UkT@m*zG/Gn樄HKnĚ; ZF*>ʈ6]䱪82x'tmfz:Qy L8|sY:T/EYn8H{;'Wb?Z!yM{zֆp`m T "PU~k6*Rޮ)Ɣ奏d0#4 Ǚ <*f9'{aas,m5XS*yan,YAR; ^L"5 iBN9,;hV=Kcǖ51 .m˹*&]ճ2v6vkѽ ݴ'q[]} fw:֝>!7ԥRA] \Ji2?ԭ.iq"H<Lm7]-1;n a2!L@K..NLjBRObt-FKkגNxit+k4-8" [%$B eU ]6R"1eUϾuunQEu4T ')Hh*Qθ- +q;Q\S`%* +J#U$ fwDyiJo^M=.Uj-:(502`ulh: 4Tb[[ 4ypooCʠ durdraw-0.29.0/durdraw/help/durhelp-256-page1.dur000066400000000000000000000061231476305210600213440ustar00rootroot00000000000000ddurhelp-256-page1.durKsHF+Ѳ,@YMDO w[%ce@]'AJ$"!oS8|`wqK!f_=~]k6/UOW^lBC}N=.RxKx3 xW71gq?֏,t6滛6~80fHfqu*84[02X`[id Urg{92nz볽,>"b(vq;[gͺO(t&Hl"w+Feͅ/ |]7OCǻFl,n`cl毟OO#l4t*XE5Rz?F ã^E߰U[9o=8d]_7]߸83ɯt$AJO*Yu}w<il_o4W!dC͒ 6>.czxUz3qP!̌ct36i`=fl^/ g%G>/*1s6Z|#Mycqpg*Z|d5Ie[XӁ1y21 euܽ]dbQ4B)YNNL/]X;_=ab{f҉ /m^'&5W c vaHMa8BSSoӋ&J[Ƶ wbΥbzv#$TG@>T% akc߉r iN4)|E+K(s$s%9lA)i]9ڽk.엿u@B$[RdWMoHI~%?-?[h;HD+̹%OqQ)ωot"dv +<' ='='\s <'s <'xNi[L'ⓑ_!Ä"^9]#@D܈"x4&&NnDI9R BFHQR݆ 9#hxьuSJz^S'T MB0v<@U"r_IEh#EJ1ʤ0T#B>Fޢ",,*XT$ġAmS.eP C@F)' GptFizѕIbcE ERHQ ERHQ h E)[]R(- )hΡ)f3 )\ԃ cK;)@:~Jd'()>q''$Of-%=)NO >O >O >Gy0kO%2ړEak=q4dM!^\\]O`x`>O HC '0| '0| '0| '0G|'r'D)rb3s1U QM}9_Mj2(۠9 #ߠ~7Dy7'o[y`9XN4M,'%[/ˉ&8YNx rKxXN`9XN`9XN`9XN`9XN`9XN` h`9rPXْ`9hF8Sx RV!k$r;˩XN0 XN`9Dy;0/durdraw-0.29.0/durdraw/help/durhelp-256-page2.dur000066400000000000000000000067301476305210600213510ustar00rootroot00000000000000ddurhelp-256-page2.duroo8)ĪU#vI׽Nʶt'Rt;gBHl: `&񆐓o7˫,>?s:a|bN'gbyIG'9!^.|;a%lK2c~x;f̧O˞dϧl>򰤋,d1 wW$5 &N5S:ay|<.kE]o]ü6O?sѴ;2N'm[ yb^ yemnSn~AU߄OZma<ٕ9IkU,/|Bηz7'>f'#.ǤuI}v 3.g1s`[.t2]WX ]Pīߐdv~O.-6] ]j6_5yc&s<&ЅcWVӗZ6c4*zP}qTtꎈʖy yz)oo-o?/6kRRuRbzx\뗴7sQѿӢ7p5nzD%- J<]NddџiџgQ V㯴_72~聆Zzuqmۢ{ cLWɶ|x8Պ|'ov r%%s՚_vIj|c]tO_=C质BÈ`(z䋾Nj}C.YyH'\\~ÄjӳpHO%T|sA..d_MtkWgt;#Û^Ȏw͙UyEDgd,bHa${:BVs?C/ʆYJ8$VS+/ϮC BO8(YT3.P'w4dsDHl s(lrQ!5_h%Pgdwg/]%Kʂ _3a;D44YJU'r _\)b$̶L5d\Cْa VCtgq-Cᎈ lKD7@3n"Q$^|A[>E(>YG1׳*~s>SEd" O" 'O?~"7|jp $7vz'Jawo@.LfԈ^-N{`Nop=  N: q/3>$r#D% nh&ȠmjDCT- IMMV&Z6=c!:L@2eʤU߇'1aDg DFI[,ݒbK|_gnl-I}WF} EFh 1[XϿSC1Ҹ3%$$ZDI}dE$uqy0Iar($F&}Ij$5d&L0 `$ItZ>AX5D %%~ȧ~#Ji*QA%JjhDQD % J@@ QD +ZvJt@ؒn.S%*dbLZLڕbRؚeLvS l @&L2d @&L &NK&d.XDD<ɤUCd2@lL vRIa@3$=yL5[F+Ei-D1om#FdJk8a{ "'7NX:!Ud'T=ld[ӽs @'N:trO<ǐnm5p&Mt̛MXd,ovʛDQM$J{@7n t&@(BnD&kI{1 &MC 5Iw1)ʶ„/$v?;]F.pK-[n %pK-[- \TK|F2j %AcK>÷S>|_-a?x 7W,KvI-ɡ`0+^)vh4JBbDU%KKj$QuIRĠKJ蒜ZX$pI%K. \$pI%KO "W]$w݅ImU=B&L2 d$IH`@&L2 d$P= @&L2 d$I @&L2 d$I @&L;Hzizǿn`0l!&iв^@'+ClW6}Rwhv/ =IR'aMұ M6Il6VQIvf&/PbL$s6dO&M6 l$I`o@&L2I_e(Y[oX+5ZAq9ܷK, X$`I@ K, X$`I%Kw,( H$ I@$I$ H$ I@$I$C$ɾT&&IqIV!=\A'Im&( hV<]&IKFH.9M"eIA$,Җ衑$a6.z7IFrM&ɨ& L$=2IBMQ7IC$9U4IN)3ID$H#5M2I& L$0I`$$hPI@%JJU 0GP`˒J*ZȫԈPI@%J* T X!PI@%J* T$PIG%%FL2 d$I @&L2 d$I @&Q&ad= 0I$.Z(L$Aa^&3+Ye<dR_&aホI ]_>2 d$$og_|?edurdraw-0.29.0/durdraw/help/durhelp-256.dur000066400000000000000000000043341476305210600203530ustar00rootroot000000000000001Addurhelp-new-page1.dur]sX3ًڢL7w^t'ͤD1ukAlmDy4_s? !-ַq\2{۰S˔jȚYR.=y{^3a2uc5̑5w3+L}3n;ӅdbMlq7*?LX\Oñu\frٕX_mְ"ޚ1Ʋ|l򖋝MY+dG!e-~\-XQVrG{355/TLv^]͋OJy֍_Zvğ#;o̩ rnnSZ݋nG\0䳌>r ^&Ɣ]Yx3J{-nw[ _ߦrS>nw)v~ȱ^8\[}5=ok',ӈk3coR[aq?w!t;[MM#o5|1؉ýKs;fGieYWBn:s3anKN=FېLغygĖ^~ƋW$ɍ,X&-]js4_~/7Fx0ƶ /S9+A[b;5H9׌G~q; *_o^m^G~/_]u[sݪը|j܆Öd<fGqŧUW~؝;LA4i} H <73a#7Mmް-qKc=[ Kdj|nۗ2o2gңzJJWdžDãf < QI}XXC!~~#N;~Ex^ݧD~ϊ0}Ͳ#9+MS$;{; 񉻯wFk9MOu,9uOxYm/Xt]ëR|@U~SdmjzN~C/n: M̐_=t'{/>v7! Ds"zD&iFXc4pmjwشƧ-Z A~j ~l7FpޟJCC/k!Wt݃A{=t݃A{=tݓ4M~Au=APpBA{=t݃A{=t&?A{=tOd"A{=t5G"#>g5BJ:WAJ>0{=t݃@Ox_g=~{=i~)&9ɛ<瑻 _ٕ'K ?#Fwz','ffp0x}>^'?^^?clBV~z%(&Db4??Q󿮷wm{6ͯ}4vt_9RC̏Gh~4?J?0~ ?Ïc1~cO~ ?K?ܤ.">v't=C#&Sߊ'No!^Q~&ɇC!|H>$ '?!|H~cst"k-~!!oVZ=>ݿ5wYJW#ѽ3 }|l=t݃A{=to,4ݧ݃A{=4M~A{rA{BA{=t݃A{=t'?A{=t'2t݃AuFkgEktܟA{=toC!}A1=׹C!{:'?r]C!uBC!ef{=np{=n&?p{=UMd"p{=ܾ^1__>3=| ۟|]<5ƽ^^p{=n5{=ɣMp{O~=Nn=Nh=ks{=t݃A{=4M~A{=D݃A{G/G^I>durdraw-0.29.0/durdraw/help/durhelp.dur000066400000000000000000000050711476305210600200400ustar00rootroot00000000000000;rddurhelp.dur[sF|ƠEuwdN`m,!wvuZٯlj@Z-?+ߟ qj.9~p vG}糅rA>X,f_rлLcgEz $\s&A/uڭbd' v6_9z|/aГ3hD{7/[Zfzw# Q﫜^ח[zz_b2}ޯ/!~fߗs.,D+ٿ!|u{l+,ۏ qlW|tvjG:K^׮I8b6׋6r0+Syy =7ɮڜ6CoN:Bm>Bh;v]U|6lѪy0 ᯶{|?Mr#:\&ړV6uGcX 9q2bn܇6-[ho70eYm|"uD]y8S9dh-7x9틣'aYYF#m*'wڎ]ϔwOcOdOn;4)~ޓ[%ņq2vT3[ys3>t7fgڦnԱ=zsrkfҏ&۱U;-*+p0Vd]] ;,.vH8C`[];_,oz.fE~' ~M<s:pM޹~b,:uLTn@=5l7(r5wyԉՐ+ ]SJ;b;ɝ1}[Lg+ֱd9'mK,UJwnx#ۮ7pa>|ރ/o`C*bސj|DÕk3?Ss^G1ny}Hj]{>E3pɨ~nR}ưĉ\Pg1Ko7<±ܱ2d.}fdzIR)mN3ge贴ϜgDipfNc;~:n/ohUhV_<k{TQn4r8iD֨>o׮;M Gؓa86@6@\P[`m [MDKhmiV* 0khFhTTC<*a; kqlllm5r4@deB^aNCd]b^hhhhhhhhhhhhhhhhhhhhhhhhhhB8~2@p VZtLr-dddddddK\d P0\J5s׬<90c /LK+>0R{hhhhhhhhhhhhhhhhhhhhhhhhhhhFc```PjOrlf|E`j͂Eo-lllllllK\l QlVcj|)6o8f`o* ؾH o>Sޚu<~y'(gDQoou?6X VV@PDE>!>>>>>>>%%.>%5|^ 4U~ A8\G@\ V9@@@@@@@5%.@@@@@@@VV1+[`.Q^/&@?p=8}`5@(X/G(y# (03,@}E Ǹeϖ~>?durdraw-0.29.0/durdraw/help/inferno.dur000066400000000000000000001202771476305210600200430ustar00rootroot00000000000000fmenu11.durMs@_Q .zL{2}DHC'>DCGRY_@KEh}++(0ϟMO篧/>}o~m~鿽~o맦=~4\x=ϗ/~=_g?]>==gonuT M z~:m}p^~?o U`?3mg_oU6HTE0kt%bX>%ǣU],oɢ{-0GYyxمGwϪ1t~4oU˾%u%gb\ix5-Ԯݮ\'/̆5Izu͆F}h TP}RJeзorMDZ.S0#/Ē@)ETwbЭ*ۆ鱞w+}!;;n9\wUv/'o'_DY" :o| gҝ\%hegw;'Bhzg8xWF\,]j'P{H!y ,NDktDP=pݝ$WudYu`@ (Pe [욖:Kȯ(^BegاRlf~-=0dmnYs|q}ho\b/mjcˬscܥ9SX):ɃO$c*3bgb ڀ0`/h({G7ºzM h&h!?bʀl҄}Eh *) vlә 2Il} ˸Qa'EdznaPL,ΠvITGm<tVhX vJp@&`5I# ߟ~,v!c?tY:Kg,tY:Kg,tY:Kg,WwlU/&_|{]6cqC(ˑ)M?Uܐ#?ClDCSG+ڼfFhFt;)crx`ւ 6/9td7fÌ̴c5/ahiJaeS7XԘm,Lh27#VmX7~K{\ITظquAsil}jBh>X[(_Bf=7ҷمSpxTr%H"F//n噳p0oVɜ;bx^,tmM+͈&^\:}uKЍX%oDJ.,H9o9&>uTm&gtV+:Qϲȅ{t+Rϰ3bs6ct3>}Qq7~̕tY:Kg,}ES\7w-3n`3}`'jn {5NxoDGnĈ^Z׵Wa<<},@U6ܟ~ViAZ L+O T"HԐa- *h#|Vd .Bۇ'<֝JMs${ ~dXp&.<ڼ{Vyηx#}Yè^-Aq跄Ͳ(nC0ƈ. L's}Dzbk LT0lXV b=<;xJ+kѪQj@nǚ6}rFq F3="IWySj,' xUUۥ;KN6ѝA#.>)3G\,]j'P{H!y ,ėy퐎h:9;g7I'LAH7&dR=+J?+@F*hfqاRlVQÐ%Z_ f́!Erݞ7YF+\fmZϟrXv-EG?yu9sLeF,:\la\@lDx6ºzM h&h!?bx^vˢ}Dh ) vl Il} ˸Ea'cz>aPL,Πv1TG <tVhXvJp@ `5?=p.Y:Kg,tY:Kg,tY:Kg,t3)UNsdfk.j?&4ЫJmbdmmi"1R 2woFvv-i5d.G8rDWH%ft*(IdSƎN7D2f⨫2N3BYAp< %Y"&ɪ(2[GE ,KRia0kPP[?^J`|X하*hN0]R+ANj_6\{\IWظuu7aNFms&@{S5y#uKBFY6t1s }V0jV7;`Ȓ_d2af4lA!ksKLX͚CC|[=o/{9&9Vڨ(9?.ŵϱHZv S5 espʌXu¹6 7 # luq!@0LB&~v1z=f,MPB`i8 S̑m.7~߀~^DVҍ JhdxIuvOgkpaDZMi Z# ߟ~,b,tY:Kg,tY:Kg,tY:Kg,= P:g}Y^ |Y6݅VDŽT[Bx_"yzzW>-M$|{.ҏ̅PNMFWHv GACr6#^]Q9>0bرIyP4Y]yUiBx2'D8K$"nӭXJ=Вш.=s?d\[EgWY:Kg,IfWY/f3>2'VF8yƶ]_y9#{;krx0_^-3.O믇鳐m?Ұ O@_TWD/?a!c\xw!t6ݳG`*t)fX]C=2y<;, F&=6nᘗM]xy oOgIFQ5m?o;[Z5h7JMūo eWQg]HUdeN zAذ#[}Ll?z":cy wV֢Ugs0%ԀnUY6L5svygŒ);7{n/E5.(Y6O𪪶Kwr9 lv;'BG D]|4l{gm? &X2 ]GO6%gB,5X /Q!!]ݝ{1pqƄ,YG~Egِ!-8tj)o6J(szaS/k@"VGnϛ^`{a.6-MKqs,`ݟ…]iu6w;Ц7h? |eR^ROn߅ѽGt̷ ʝθmuH| >8>XWh# m#຅c^61^vݳ*ps=% Fմ߿o j_p_+5m?F%d|]ENv1vgH;;e_c|fbòbT}а,%SZYVMF֎XlPUeu0=$a3;dv7bG- 0_4NQָHΛ ]sg =ApV.Xjtٍ )uW8b(W w=ڔPC Lc`))DH-W_x$4]sۏ:#ޘ:Kȯ(8÷E}Z-FiUeN: Y[bpm(B\Z*p(y|io4ɱe֦P19 w)}jRtiI([;TfBϮVaV_QHg#f2#6eQ¾{"U K;bipyseܢ01edx (&nyUgPBKM# }:+4S ,y x%l 8vMo0ĚuVxXgKg,tY:Kg,tY:Kg,tY:KgM_*'9k-Rk.j?&4ޢĻЫJmdmmi"aRߏ3oFv~-j5d.G8 rDW3H%f*(NISƎN;ġ2f⴫2N3BZA< eY"6ɪ82[ G ,&Rɥd8oP^[?^Ja|p*hN0]R+Nj_8\{\IZظyuX23ϴsl3qڛ #oanɢ|y(Of'R.fNZcRfZr "@f-\gÝ;üYuN'sa:c|Y =!>743|l<\|&qvnwbD;L1,H9o>&>°uT1W@zJl[d\Nb)DKF#,F81N7sqo>~^Y:Kg,t$-ߛ TT83^v^8 XQsXpZ؊w}ZNpFe쬕Q~]{LR<ӧ ߆*<<>4 {JJdB#>1ȅHBGm={3ߪ,w:bAE(1#cq^ɢ{`oaӏlyxمGwϪ1t~4o/\kU˾%uQu1Դ\)Zv:mHE#YeNXV~J ˊ=bì'3rOie-Zu6Z; cQB VmX@O>ڝވaw&~8y~\_DY" :o͝e3@Yjt'cif7~2(t@G ^qb+_-:jSB}v)R31p" \ЯꁎL=>YҩulT{cr~,#,m7Z-FiUeN:Y[bpm(B\Z*p(9|˙oo4̱e֦1 9 w,EouiI([;TfBϮCV"aVQH侏g# Af2񣷫 f,MSB`i8 S̑m.7~߀~^PVM JhgxIu݂OgkpaDZM) Z ߟ~/b,tY:Kg,tY:Kg,tY:Kg,=ӻ [P=g}qa |q6݅VDŽT[}_{zzޗW\-M$|۠dGֽldiGAB(&܎P+Jr$;!Gte9t[`F'LD"]1eX|@@ ,c.*4#Y!E[ɓPM%kba ,Ŝzά,RAaa0kP\?^e|߼`*hN0]RP+ȋ_7\{\IXظuuX2:E혓ge TMhy $uKKCFi6t1s ~V1jV7;bʒ_e%2eQ)xi9 w fY#q⌎9^,HܼҴhbU~̹raĩ ty.%>dD# VQ=AIjXL\Nb),KF#,F8rϻ>}Q7~ܕtY:Kg,}A@h3cٌ e'jn {5NxoDGnĈ^Z׵W a<<},+|Om4 .W&+A* ltj^8]){M םmub| >8>XWh#-m#亅c^61^vݳ*ps=% Fմ߿o j__+5m?F%d~]ENv1FvgJ;;acdòb}` %SZYVF׎XlPUeu0=$a3;dv7bG-睉6_4NQָHΛ ^sg =ApV.Xjtٍ )uW8b(W w=ڔPC Lc`)F)DktDP=1'9I:i RuF1! u_QYr6RAC>f*2-1b6k!-mY̴7X2kBt|>bk):ɃO$c*3bgb ڀ0`/h($Rdz[h@G3A Uƫ[%'B[X MQc L(昶O g?o_- c?/SF֋ bbWu%1xؤ:nyاB3W"Φ3nCQaO_N?vtY:Kg,tY:Kg,tY:Kg,tM`~r&B.&BcB*^As-yO/Q :=v+O֖&f+MN!}^6t k!OnG}%s9QА#? -(0DWTEq~O"ˮ2vu 6Qh*hٻ8 tX!E[ɓPM%kaa,CŜPzTʲhj)G^ZmM"7 JjtgґSs\ fK }%xqwFkO:+ 7k[fޙ6`NFms&>{S5Q4{ ,-Y/ !eVJ)8Yg\Ux9KnU$leYX`7dN1uTm&gtV+:Qϲȅ{t+Rϰ3bs6ct3>}Qq7~̕tY:Kg,}pA@hp3Êٌe'jn {5NxoDGnĈ^Z)yjџpy:= _=OE ߆*<<>4H ؕ {%IJdB#>1ACm={,B4-Foc)f ] =2Ny<';,F&=6nᘗM]xy oOgIFBQ5m?o;[Z5B7JMūo eWQ݆]]NeNqzA1ذ#H}l?z9cy wV֢UgC0%ԀnUY6L5 mvygb);7{@n/E5.(Y6\O𪪶Kwr9kv;'BG D]|4F{gm? &X2 ]GO6%gB,5X щ/Q!!]ݝ{1Ƅ,YG~EgVm@+Z-̏]-1b6_!-mYX̯7X2kB@f|Ƣk9ɃO$c*3bgׁbP0`/h($rdze[hG3A U4a=ª iJ[DfL1G}R<[9~2nQ x2^OXK3(]%&v>…]iu6q;&7h͏:h+ra? |e^RȆOn%߅`ѽGt̷ ĝNmt<| >8>XWh#m#຅c^61^vݳ*ps=% Fմ߿o j_@_+5m?F%d|]ENv1vagH;;^cdҦaòb<}p%SZYV;Fx֎XlPUeu0=$a3;dv7bG- -_4NQָHΛ ]sg =ApV.Xjtٍ )uW8b(W w=ڔPC Lc`))DktDP=)'9I:h PuF1! u_QYp6RAC>f*2-1b6k!-mY匴7X2kBs|>Bk):ɃO$c*3bgb ڀ0`/h($Bdz[h@G3A Uƫ[%'B[X MQc L(昶O g?o_- c?/SF֋ bbWu%1xؤ:nyاB3W"Φ3nCQaO_N?vtY:Kg,tY:Kg,tY:Kg,tM`~r&B.&BcB*^As-yO/A :=v+O֖&f+MN!}^6t k!OnG}%s9QА#? -(0DWTEq~O"ˮ2vu 6Qh*hٻ8 tX!E[ɓPM%kaa,CŜPzTʲhj)G^ZmM"7 JjtgґSs\ fK }%xqwFkO:+ 7>W[0'Z69 =FF,ʗIύm`+b3\.ժnv}%*ѫo dey,y,?̛U's2'a9ƠK+;@["qJν x{&8-A7 >mhZwZ:* ܍63bKDBGY-uK)wGXe1¹gG1Cw (?Y:Kg,t? iL?4YlG&57YÂ'V 7v"#7bD}t/cg\߫Eft0|p>}u ߆+<<>4H ؕ {%IJdB#>1ACm={,B4-Foc)f ] =2Ny<';,F&=6nᘗM]xy oOgIFBQ5m?o;[Z5B7JMūo eWQ݆]]NeNqzA1ذ#H}l?z9cy wV֢UgC0%ԀnUY6L5 mvygb);7{@n/E5.(Y6\O𪪶Kwr9kv;'BG D]|4F{gm? &X2 ]GO6%gB,5X щ/Q!Rx%3]&ۋAȱ7&gR=+J?+@~oK.Z-݆̏-1b6w!-mYX̲7X2kBr|{מ"k:ɃO$c*3bgAcڰ0`/h($2dz%[hpG3A qUa3^&;)B[XU!Mic L)ȶO g?o_ c?/%SFً bbWu% 3xؤ:nاBRW"ΦodO_N?ytY:Kg,tY:Kg,tY:Kg,tm~rb36ߙMwU1!@9 ޗPޅޣn;䕑'kkK4_^y#}^6 y!VnG}%s9Qΐ#@-X0DWTEqO"̮2vyRQF Mm1{^u "ŭI(&50OV !b``pgVQF3Lh S_s0do5(]~Pg/JN1oJrj 4'.)| b=`N,lܺP{1'#Z69xő%e!ds,}]JI9>+ K50G_d/db 2Ϝ;vyN,8 qBbie3hsBl$n^iV41*?yJL:<Ĉ|7_X2j+rwL|xaO NXKZъ|,}E.Y[s{%]#{~Ib{ (?Y:Kg,t? iL?4̙lG57YÂ'V 7v"#7bD}t/cg\߫Ef0|p>}u ߆+<<>4  {JJdB#R?1KHm={6ߪDw:bEX1#cq^ɢ{`oaӏlyxمGwϪ1t~4o/tkU˾%uQv1Դ\Yv:mx$)eNXVaJ ˊ=#'3rOie-Zu6[; cQB VmXO>ڝވaw&~8y~\_DY" :ox͝e3Yjt'cif7~2(t@G ^qb+_-:jSB}v)R31p" \\}ccO୷stttn?${cB~,#lߖ]:i7UA9=0dmnõYs|q}ho\Ed/g$ Y%Ǡܥ9]KO}&l~S =[}X8[}AcD!>."D: Zďخ2:`؀E V%HS,'29qOǔ}*XUA b /16.xL--H)8ہ6Ak~A{Xӗa],tY:Kg,tY:Kg,tY:Kg,gzU4}ADɶKɦЪW xz wSKBN.}ƓN}?<ܒu/EQ >芒(h]Y F`K+8'eWL;VB;)"6˘ʀ:PVjQݔPM'kda ,üŜzTʲhv)J^]kMv> ~)ɩ^=BQ%̾|;ʵ̕久WX0 1'+Z698=I,ʗύmvb+b5f.ծnv}%*ыp dڲy? ~e^RƤO sE߅ѽGt!ͷ ͝m0uN| >8>XWh##m#캅c^61^vݳ*ps=% Fմ߿o j_Ԉ_+5m?F%d]ENv1ƦvgZ;;_cbòb`}8 %SZYV F֎XlPUeu0=$a3;dv7bG-睉1_4NQָHΛ fsg p>ApV.Xjtٍ )uW8b(W w=ڔPC Lc`)F+DkttmTtPrOwwΚc Vgd[g h[8tjnH3?ʖt6T|9P>>ȷUQ3fb3hcˬ cs@O\{֮%h'>UPN?ẅ]> hCx1yFXUo C-djWxYmaU4Ҏ-p"3A#>)\o(|LY/'[^.agc K.^:θh g?}90VY:Kg,tY:Kg,tY:Kg,tYzWE: vl l ڏ ͡=.w!t0bرIF<4Y]vUiFBP2'(8K$'sa:c { 9!674){i<\|m&qfntbD{KO/,H9o;&>uT%mq'~ҥZhE>"nӭXʹ=ɒш.=s?nO@_{ve,tY:Kc|o&Pb6#zى{^p,`Eamglka+j;Ñ1>V.Guբ?3Atzz8O> ߆+ <<>4 {JJdB#>1DEm={0ߪw:ݞbE1t#cqຳ^ɢ{h`oaӏlyxمGwϪ1t~4o/@kU˾%uQu1Դ\Yv:mڅ!!eNXVzJ ˊ=S'3rOie-Zu6Y; cQB VmXO>ڝވaw&~8y~X_DY" :ot͝e3Yjt'caf7~2(t@G^qb+_-:jSB}v)R31p"C{o4{1pqƴ,YG~Eg-8tjo6J(szmS/ks@"VGnϞ^`{a.6-MWqy,`ݟ…]iu6};&7h=h.ra<͋tY:Kg,tY:Kg,tY:Kg,L//xvoC)tZRY Co{ } ]=y߫C^1y4`ޯG[ҷe#H? Bq5vԇ]Q2#ٹ 9+ۈL3zItEX$)cJ'^TfwqUh . ZJm,X dU`- ֣$ReFcL|S_kq7(]~Pj/ LNE2oJrj 4'.)8/ j=`$O-lܼ:ua srl31ۛ #o`nɢ|i(MfR.fNZCRfYr"@&-\gÝ;üYuJ'sa:c |I =!>741|r<\|&qrn}bDL0,H9=&>uT/V@zJlZ`\Nb)@KF#,F81N7sqo>}g^Y:Kg,t$-ߛ TlF:3Ƙ^v^8 XQsXpZ؊w}ZNpFe쬕Q~]{ R<ӧϢm?Ҹ O@`pWD/?ѩ!\xw!t6ݳG*u)ft]=2οy<g;,H F&=6nᘗM]xy oOgIF¸Q5m?o;[Z5v7JMūo eWQ݆jQ]dUDeNAzAٰ#b}hl?zB:cy wV֢Ugĵ0%ԀnUY6L5 tvyg);7{@n/E5.(Y6O𪪶Kwr9 lv;'BG D]|4n{gm? &X2 ]GO6%gB,5X /Q!!)\o7'pLS/!].aՂa K.^:εhS ?}90SY:Kg,tY:Kg,tY:Kg,tYzD9o tl l ڏ h͡h=.ow!\bie3hsBl$n^iR41*?yJhL݈2|^X2j+rvL|vaK NXKZъ|+}E.X[s{%]#{~Ibz (?Y:Kg,t? iL҉?4ilGf57YÂ'V 7v"#7bD}t/cg\߫Ef0|p>}u ߆+ <<>4 I{JJdB#">1ADm={-ߪv:מbE1\#cqԺ^ɢ{P`oaӏl#yxمGwϪ1t~4o/(kU˾%uQCt1Դ\Yv:m !eNXVw1J ˊ=A'3rOie-Zu6!Y; cQB VmXO>ڝވaw&~8y~@_DY" :ot͝e3Yjt'c1f7~2(t@G@^qb+_-:jSB}v)R31p" \\}ScO୷stttf/:#ޘ:Kȯ(>{Z-FiUeN Y[bpm(B\Z*p(ٳ|io4̱e֦19 *=luiI([;TfBϮVaaVQHDg#Kf2㶫 a3^&;)B[XU!Mic L)ȶO g?o_ c?/%SFً bbWu% 3xؤ:nاBRW"ΦodO_N?ytY:Kg,tY:Kg,tY:Kg,tm~r3/3BcB*kAs-yO/ G={v+&O֖&lhpKֽldGAB(&܎P+Jr$;!Gte9t1[`F/8D]1eX Ë@6 ,c.*4CY!ED[ɓPM%kca,ŜzTDʲh)O^ckMv:eʾS~ɩHM^ CBQc%Ҿ;A͵̕䩅WXTE՘ge윉TMhy $uKKCFi6t1s ~2jW7;dΒ_gI52i}Nipocqw2`t)R_dSC'BHޣmg0[UNS61>xzdcx,`w+Y4}ϑ6Lzm~1//hY89ߞϒfq j~vxٷ/jn/ƕW2Eˮ\' T3̉ˊ1XRiaYG>~tƒ@)Ea#kGa,6Jݪmk02P#DSv/'o^(k\$]AMQl( 8+UUmr,5:FwOh+~ALrteB珞@mJ!Xj&1Nc_BC:6*:&9;gI'Mױ b3 YT ڀ]:id7[eK Y[b*jm(B\Z*p(|˙io4ɱeF`19 'jגsiI([;TfBϮV`V_QHg#˪!f2c96ei¾{"UҔK;blpyseܢ01edx (&nyUgPBKM# }:+4_ , x%l 8vMo0КuVxXkKg,tY:Kg,tY:Kg,tY:Kg^M__*'9{-R{.j?&4bʻлKmdmmi"SJ%;ҷe#H; B5vԇ]Q2#Y 9+ ۈ3:ItEXG$)cJh'eTfwqUR A- BJl,X dU`- ӣפPFKTR_k0Vo5(]~Pܭe/ IN0oJqj 4'.)`/ P=`+lܺ:ġ-0'#Z69(=G,ʗύmva+%b4`.լnv}%* p d²y4y,?̛Us2ga9ưK+{@b#qJWS|fg&FA!F价’T+Xc [GE[q']ՊV{^!-r:݊ޓ,3N=[EaWY:Kg,1IfQ,f3>2'VF8yƶ]_y9#{;krx0_^-3.O믇鳨+|Om4.W&+A* l(j^]{M| Bt{JCocŁz%9цIO? [8eem=9Cjwz#vrޙ{NE`ۋ~e+) 5wgҝ\FNɠБQMzřzۏ#I.B~pWM =K<iKTrqUNI=MAu8h3 YT !=|[tqاRlVQÐ%Z_ f́!Erݞ7F+\fmZcϟrXv-EG?yu9sLeF,:\la\@lDx6ºzM h&h!?bxa^%'B[X MQc L(昶O g?o_- c?/SF֋ bbWu%1xؤ:nyاB3W"Φ3nCYaO_N?vtY:Kg,tY:Kg,tY:Kg,tUa~r'b.'BcB*_As-yO/A ;=v+O֖&f;8pKֽldGAB(&܎P+Jr$;!Gte9tqZi`F/D]1eX Ë@ ,c.*4CY!ED[ɓPM%kca,ŜxzTʲht)I^]kMv:ʻ~!ɩM^ =BQ%̾{;Aʵ̕䩅WXTEuge윉TMhy $uKKCFi6t1s ~2jW7;dΒ_gI52i}u ߆+<<>4 {JJdB#>1ȅHBGm={3ߪ,w:bAE(1#cq^ɢ{`oaӏlyxمGwϪ1t~4o/\kU˾%uQu1Դ\)Zv:mHE#YeNXV~J ˊ=bì'3rOie-Zu6Z; cQB VmX@O>ڝވaw&~8y~\_DY" :o͝e3@Yjt'cif7~2(t@G ^qb+_-:jSB}v)R31p" \ЯꁎL=>YҩulT{cr~,#,7P%"([zmS/Vks@"VGnϞ^d{I.6*QWqy,nݟ_IAR KF4R`E/lIc tV+Zȅ;t+rzd4bs܏8)[co>z]Y:Kg,t$-ߛ T`93^v^8 XQsXpZ؊w}ZNpFe쬕Q~]{όP<ӧϢ]pocq2`\)R_dRC'bBޣmg[UNGS6+>uzd|cx,N_w+Y4}ϑ6Lzmr1//hY89ߞϒfn j~vxٷ/jn/ƕW2?ˮ\' S3 ˊ"1LRiaYG>~tƒ@)Ea#pkGa,6Jݪmk02P#Sv/'o ^(k\$]AMQl 8+UUmr,5: FwOh+~ALrteB珞@mJ!Xj&1N_BC:y$4]ۏ:#ޘ:Kȯ(9 šcVKyQZDӃC斘j}1\5ʇׇ ?u{,_DrfMrpi!Xr :>]kcڵAڧj1гpՇsm@o4F)-B4`L*{x --J(XڱNd&sL'˳7/)#U@1t˫:^b芒N(h]YF`G+8'eWL;V;,4Y]uUi:̢ íI(&0OVe b`,=*KpMeYm4#/-fIt%J3~ȩ^ :BQC%ɾu;Eȵ'̕IW-)`N>mrs&>{S5A4{,-Y/ !%V)8Yg\Up9 KnU$WdeYX`7NdN1uTm&gtV+:Iȅ[t+R3bs܏6ct3>}Qi7~ʕtY:Kg,}pA@hp3Êٌe'jn {5NxoDGnĈ^Z׵Wa<<}, P 6Viyx|i+JB G65$|bX /.=ڦ{YhZNS6>kzdxcx,NZw+Y4}ϑ 6LzmV1//hY89ߞϒfd j~vxٷ/jn/ƕW27ˮ\' cQ3 ˊ1$RicaYG>~|sƒ@)EΆ^# kGa,6Jݪmk02P#Sv/'o^(k\$]AMQl 8+UUmr,5:FwOh+θ~ALrteB珞@mJ!Xj&1N_BC:6*:9;gIgKױ b3 YT /a[fqاRlVQÐ%Z_ f́!Erݞ7)F+\fmZϟrXv-EG?yu9sLeF,:\la\@lDx6ºzM h&h!?bʀY6ei¾"UҔK;blpyseܨ0"2e|x (&n|UgPB$KM# }:+4_ , x%l 8vMo0КxYxdKg,tY:Kg,tY:Kg,tY:Kg^M__*'9۲Q^3mtZR! Co{ } ] yߨC^x40ެř;ҷe#H; 2B15vԇ]Q2#y 9+ ۈ 3:ItEX$")cJx'eTfwqUR - FJm,X dU`- ԣPFKLR_k0Vo5(]~Pd/ IN1oJqj 4'.)`/ T=`+lܺ:q)!BɈֳMrio&4fő%e!ds,}]JI9>+ K50G_d/dbvpe9 w fY#q|1^g,HܼҤhbU~rę ty-%>dD# VQ=AIjWD\Nb)$KF#,F8rO>}Q7~ؕtY:Kg,}A@%h3ٌe'jn {5NxoDGnĈ^Z׵Wa<<},dpocp@2`X)R_dRC'FBޣmg`[UNgS6$>qzd|cx,]w+Y4}6Lzmp1//hY89ߞϒfk j~vxٷ/jn/ƕW2>ˮ\' S`3Iˊ1>Ri3aYG>h~tƒ@)E&`#TkGa,6Jݪmk02P#Sv/'o ^(k\$]AMQl 8+UUmr,5: FwOh+~ALrteB珞@mJ!Xj&1N_BC:y$4]sۏ:#ޘ:Kȯ(8UZ:id7[eK Y[b*jm(B\Z*p(|fo4ƱeF19 'iגsiI([;TfBϮV`V/QHĻg#˪!f2c(a7ªi[f@1Ǵ}R<[9~2nN x2^CXK73(]%&v>]iu6k;Ц7bO9h)ra~rDsƒ@)E]# kGa,6Jݪmk02P#΄Sv/'o^(k\$]AMQl 8+UUmr,5:FwOh+μ~ALrteB珞@mJ!Xj&1N_B U=)ɱ9:K:hMWdoLozW~VF]:j7[eK Y[b*jm(B\Z*p(ٳ|fo4ɱeFP19 *=juiI([;TfBϮVaaV_QHg#Kf2㶫 f,MwRB`i8 S̑m.7~߀~^JV JhfxIu|OgkpaDZMg Zsڃ ߟ~.b,tY:Kg,tY:Kg,tY:Kg,=k P:g}g6m&3BcB*jAs-yOa/ G={v+#O֖&iTpGֽldiGAB(&܎P+Jr$?!Gte9t[`F'0D]1eX |@. ,c.ν*4#Y!ED[ɓPM%kba,CŜzά,˽fRa0kPl[?^c|ߐ*hN0]R+!ȋ_7\{\IXظuuCX[Ŝh=+$LjBh>X[(_Bf>7ҷمSpҀTsE"H&F/ˎL+iX`7Nd1<'ta/V:c6'FfEsΝ $LЍSCws)%#V"}ć1OTBGZuK9wGY2e1¹gG{-`rOӮ,tY:Kg*CÜ`f|dl/;Qsa/ N9,pm-lŻ@?|`'r8r#FG2v(`^ZgF(\N_g]pocq2`\)R_dRC'bBޣmg[UNGS6+>uzd|cx,N_w+Y4}ϑ6Lzmr1//hY89ߞϒfn j~vxٷ/jn/ƕW2?ˮ\' S3 ˊ"1LRiaYG>~tƒ@)Ea#pkGa,6Jݪmk02P#Sv/'o ^(k\$]AMQl 8+UUmr,5: FwOh+~ALrteB珞@mJ!Xj&1N_BRtLr vnNGTdoLozW~V ۲C>f*2-1b6k!-mY̴7X2kBt|>bk):ɃO$c*3bgb ڀ0`/h($Rdz[h@G3A U[ (a=ªi[Df@1Ǵ}R<[9~2nQ x2^OXK3(]%&v>]iu6q;&7b:h+ra^@sJͫZWU!bNVms&R{S5q4{ ,-Y/!ӟVJ)8Yk\]9BKnU$WȴeGuy}7~띥tY:K;p|o&Pc6#c{ى{^p,`Eamglka+j;Ñ1>V.Guբ?3Ntzz8O>F·J/<>? e^RƥO u߅0ҽGtͷ ѝmpuV| >8>XWh#'m#c^61^vݳ*ps=% Fմ߿o j_Ԩ_+5m?F%d]ENv1Ƨv1gb;;E`cFcòbp}@H %SZYV F֎XlPUeu0=$a3;dv7bG-睉3_4NQָHΛ jsg >ApV.Xjt&ٍ )uW8b(W w=ڔPC Lc`)F,DkttmTtXrOwwΛcŠVg[gh[8tjnH3?ʖt6T|9P>>ȷUQ3fb3hcˬ cs@O\{خ%h'>UPN?ẅ]> hCx1zFXUo C-djWsl҄}Dh *) vl 2Il} ˸Ea'cz>aPL,Πv1TG <tVhX vJp@ `5?=p.Y:Kg,tY:Kg,tY:Kg,t3.?UNswe[we]h~LH,)%w}y%D{͖JvoFvv$.bj5d.G8 rDWH% ft*(ISƎN7d2fī2N3[A< Y"&ɪL2[ Ge I, ƗRa0kPj[?^ b|߄*hN0]R+Nj_6\{\IWظuuX㒘E]ge TMhy $uKKCFi6t1s ~V1jV7;bʒ_e2c>yPT˂VvJXyNb-疛ʒЈ>=SOqqio>flWY:Kg,AI[3 )k3Cٌ{F WęÂ'V jsՃHȕQ+ebp~==_~>?}O_7 qm|y++A* d,jHNyM|'3Rx{KÇ}ocىJMsb; d\r&Ƌ?ڜ{nOgNJNQ5o?zG* a/2?˼DNv1ƤqJ;v+JÆe=vcg5OnezL5b#ЬJن!!S|<"G[n=l=e'qY2Et;f ^vNRҀftW QM~k₽,KWZ!w) \M-dUUX QA!RVI:&9>Z;K3I'M7iaYԀT !D|[oSbr @!}Ke}7\;G?us,]Db!ks"iBz|g +:كOFmcj=bgNcvJ0`Eh($ާ[Qb2 a5^VOR*ea\ K-[fjlpys=dL/{aQZ{U?ʍ^b:l5إFu,^Wql8v oZ=(~>}&.ytY:Kg,tY:Kg,tY:Kg,t6?)Iߙm<ߙMw1!@= |׈ޙޣv;'cKH6/~}7ߒ/[94Ih]Y Fo2J+2";'f^9xD:jj%zޙq{NMze-) R6wg ҝXVNI.<8ZŅ~;L=Y.Bv5sSh3[ȪZ&1NB_tDP3i'?ZI:p5`uB6 _Y?r[xoSbb~-h*Do V+@!֞nA,P{2N\$T/#|v%9?{)ۈM9sLG,QR@ lDt6q[!@QLPB&va^UM*ea\-[fjipys=cLz aQZ{U?m^b:l5yإF3u%,^Wql8v oZf9(~>}צ)?utY:Kg,tY:Kg,tY:Kg,t%u-?)Iߐm<ߐMw1!@= |(ޙޚ-v;ą'cKH3/6}(ٗYtET$pd4䈮'~#B7Lk$a2/L; AIB^E9Wyz$K(p<؄Y"*IQ`- ңK'nT yFh6.t؋c.HOs삂d/D~ۨ!rq}%v\a#4,o+dDn}h?X k(]BF>7Jن)cU|%ʒ;p dIJy4;,0ϛ:pO 0ߋY ШhdnRՙȡ y.EN_X3);FN^:*?68F;iS- ZnB)-b:݊[n*KB#$FXL=IBy@_Ƴ]Y:Kg,t$-o*͌+f3N4\A g+ #9xD:jj%zޙ{NMze-) 5wg ҝXVNI.<+Z{;L=Y.Bv5sSh3[ȪZ&1NBtLr |vfNnBYU'$kò[kg/_4cP$(3fC4n2v>mY´*tEҪ19"*.-AlWuq(ğ=zB.N.naFjQHdOgSX%dm0j.2T¸BK[AIL} K{\W[ɘ^3ڣX~%3tبk=|Kg KYp@,ߠ{P&.<}{|M\b,tY:Kg,tY:Kg,tY:Kg,=k m~Sҝ3/y3B'cB,kA{ @!3G={v &O4Ɩ6l^n%Cg_##zWoGsh)R9ˑѐ#A-d0DWdEvO$ ̼0y(y'Mzm!{gG_v,lfꘟ'Eقy3[,I˾ ƘR1d0oPxSk/ LE26i\#=ƴ <;F5衅Wծ^ wcVtms"h{[qx ,5Y.!l ld3WWgheIM2jUm _ǧӯk6 NB HG2<5b '<ڦ{tt=l CG81vu%9ႝцI? [9yEm= |H3x%}\èw=^#AEMŰRm e^T' T V;%ac$dò`c3 Kك't&a\[ cC hVlؐHONk>ڣZ-awfz8{~sxD"a :g͝e3Yzt'cUz3v+tD &NVqSb+]Z֪~ ,` m%{ր ڀ,Zj@~Eg^dXd [B.x9P>>vȷA#fbq ^I+%Ǩ<6]I}6lS =t;CPmAE!>MeV}D"tq ]dv VuiRƥ4XڲdFȬ6O g>o_# m >WŪG^(܌%F]# ]:k4_WX uw#ne2YwmkKg,tY:Kg,tY:Kg,tY:Kg^M__lC=v=t:b `@ }iߥoC\x1Lr7Rю }ي>Ў_D͡AWHHrGFCz7xF]>0䱣Iz<4Y읝vԩGDP‹W_M%b~ ab`8=*O$2/6]JŒgDjSzANqn/$9xtBqk4.(`B෍(wWb6]=VfxDX&;'´uFGI^E2Q6PfNOsju=,]VLށ_ #na~I{bxhh^,uNͅFE#stCͭ DMЍ}Hp)ršH1rQA0IjYЊtNi+OVrSY'1ºg)N>:"߬7Y:Kg,t? iyk&P!w pfh1qϨ^v ^83XpXpZ[؊Amz)#}:jesx_\, ίOU;Z!./o?4}q?H%~E I‰;BdF;oO1[c"|qy1;qz]ɢ{Nl`galVyxGs:1ii6^I߉0]HPeQ!u1Լ!et[Bg(n0Ƙ6B)~NzE0qذlG>@n ƒ@ɭC/ F֖XlPUi40=6$aӚGVbKح睙-4;7QFH؂Ι ^sg =Ap.݉XjUތ Q!£ oU\ԃbJ+nW37=)>jK4J9(D VI:&9>Z;K3I'M7FNHdR+R=+@^hmž}NIyQ%2g h-!pe(}\;s qt9iU邋U csDR\ծ`g>eQ?{]-\ (pբHyΦ2n>"D:: JȄ.2>`XEUӓJYA`i8!Zb<)\oi(*xӳ^}NX{yr3u.xvL]aɻ2U5htdoOߵ ],tY:Kg,tY:Kg,tY:Kg,'zU4}A?uJsd%d]~L+!>7Lҍ6R*{J9:A7;1"I kFB") [GEӖq ɖH7m!|nZ-7%}#{\;"7Y:Kg,t? iyk&PYwqfv1q^v ^83XpXpZ[؊Amz)#}:jesx_\,ίOաߍ^xy_~hJKJ$B#?1ʅHGwm=4Ɍ0w:bE0!{#cvEhäD׭6uc>laT/&bXyC趄2/Qem8VfNeү1BRaRaj\}4h%[Y^:p-ȡ4hazlH'5HGQĖ[;3b|OIi=90pwVo~3EAβ|ଁWu]Ԫ8`i;:BԅG@Zo1ҕVݮfn ?zWS@}f YUkD?isPrFjF:394>,0qI:h4Y|کGD W_M%b~E b`d=g )o1TDyF:.}ڋcM.!HOs1i/!~ۨAsq}%v\acau s6slۺ#g`q$ɢtY(K_gV(Ig3'cߧP9WsM&+K&F/˞L;iwX`7Etga:zK+{@b#qsQ+|s)⿧3#t#s#=\fD+d+R>vuT$mq'vҦZ"|3Zʃuk9LFIzz7'tY:Kg,7HZޚ T^]_f3h' V4\G8yoPD GĈh_Z,뗵?M2Adurdraw-0.29.0/durdraw/log.py000066400000000000000000000132641476305210600160670ustar00rootroot00000000000000''' code taken from: github.com/tmck-code/pp/log/log.py A module to create loggers with custom handlers and a custom formatter. usage examples to initialise a logger: ```python # initialise logger to file: logger = getLogger('my_logger', level=logging.DEBUG, filepath='./my_log.log') ``` usage examples to log messages: 1. log a basic info message by passing a string: ```python logger.info('This is a basic info message') # {"timestamp": "2024-12-09T15:05:43.904417", "msg": "This is a basic info message", "data": {}} ``` 2. log a message with additional data by passing a dictionary as the second argument: ```python logger.info('This is an info message', {'key': 'value'}) # {"timestamp": "2024-12-09T15:05:43.904600", "msg": "This is an info message", "data": {"key": "value"}} ''' from dataclasses import asdict, is_dataclass from datetime import datetime, timezone from functools import wraps import json import logging CRITICAL: int = logging.CRITICAL ERROR: int = logging.ERROR WARNING: int = logging.WARNING INFO: int = logging.INFO DEBUG: int = logging.DEBUG LOG_LEVEL = { 'CRITICAL': CRITICAL, 'ERROR': ERROR, 'WARNING': WARNING, 'INFO': INFO, 'DEBUG': DEBUG, } LOG_ROOT_NAME = 'durdraw' DEFAULT_LOG_FILEPATH = './durdraw.log' DEFAULT_LOG_LEVEL = 'WARNING' CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL CURRENT_LOG_FILEPATH = DEFAULT_LOG_FILEPATH CURRENT_LOG_TZ = timezone.utc LOGGER_INITIALISED = False def _json_default(obj: object) -> str: 'Default JSON serializer, supports most main class types' if isinstance(obj, str): return obj if is_dataclass(obj): return asdict(obj) if isinstance(obj, datetime): return obj.isoformat() if hasattr(obj, '__dict__'): return obj.__dict__ if hasattr(obj, '__name__'): return obj.__name__ if hasattr(obj, '__slots__'): return {k: getattr(obj, k) for k in obj.__slots__} if hasattr(obj, '_asdict'): return obj._asdict() return str(obj) class LogFormatter(logging.Formatter): 'Custom log formatter that formats log messages as JSON, aka "Structured Logging".' def __init__(self, tz: timezone = timezone.utc, *args, **kwargs): self.tz = tz super().__init__(*args, **kwargs) def format(self, record) -> str: 'Formats the log message as JSON.' kwargs = {} if isinstance(record.args, dict): kwargs = record.args record.msg = json.dumps( { 'timestamp': datetime.now().astimezone(self.tz).isoformat(), 'level': record.levelname, 'name': record.name, 'msg': record.msg, 'data': kwargs, }, default=_json_default, ) return super().format(record) def _getLogger(name: str, level: int = logging.CRITICAL, handlers: list = [], local_tz: bool = False) -> logging.Logger: ''' Creates a logger with the given name, level, and handlers. - If no handlers are provided, the logger will not output any logs. - This function requires the handlers to be initialized when passed as args. - the same log level is applied to all handlers. ''' # create the logger logger = logging.getLogger(f'{LOG_ROOT_NAME}.{name}') logger.setLevel(level) if logger.hasHandlers(): logger.handlers.clear() # add the new handlers for handler in handlers: handler.setLevel(level) logger.addHandler(handler) if logger.handlers: if local_tz: tz = datetime.now().astimezone().tzinfo else: tz = timezone.utc # only set the first handler to use the custom formatter logger.handlers[0].setFormatter(LogFormatter(tz=tz)) return logger def getLogger( name: str, level: str = DEFAULT_LOG_LEVEL, filepath: str = DEFAULT_LOG_FILEPATH, override: bool = False, local_tz: bool = False, ) -> logging.Logger: ''' Creates a logger with the given name, level, and handlers. - disable the logger by setting the level to logging.CRITICAL - the default log level is 'WARNING' - the default log file is './durdraw.log' This logger will only create an output file if there is a call to write a log message that matches the log level. ''' global CURRENT_LOG_LEVEL global CURRENT_LOG_FILEPATH global LOGGER_INITIALISED global CURRENT_LOG_TZ if not LOGGER_INITIALISED or override: # create a root logger LOGGER_INITIALISED = True CURRENT_LOG_LEVEL = level CURRENT_LOG_FILEPATH = filepath if local_tz: CURRENT_LOG_TZ = datetime.now().astimezone().tzinfo else: CURRENT_LOG_TZ = timezone.utc return _getLogger( name, level=LOG_LEVEL[CURRENT_LOG_LEVEL], handlers=[logging.FileHandler(CURRENT_LOG_FILEPATH, mode='a', delay=True)], local_tz=CURRENT_LOG_TZ, ) def log_on_crash(func): ''' This is a decorator that can be used to log any exceptions that occur in a function. This is mainly intended to wrap around the main() functions/entrypoints to durdraw, so that any exceptions that occur can be logged to a file for debugging or support. ''' logger = getLogger('crash', level='ERROR') @wraps(func) def inner(*args, **kwargs): # run the function, return the result, and log any exceptions try: return func(*args, **kwargs) except Exception as e: logger.exception( 'durview crashed', {'class': e.__class__.__name__, 'message': str(e)}, exc_info=True, ) raise e return inner durdraw-0.29.0/durdraw/main.py000066400000000000000000000270611476305210600162320ustar00rootroot00000000000000# Part of Durdraw - https://github.com/cmang/durdraw # main() entry point import argparse import curses import os import sys import time import pathlib from durdraw import log from durdraw.durdraw_appstate import AppState from durdraw.durdraw_ui_curses import UserInterface as UI_Curses from durdraw.durdraw_options import Options from durdraw.durdraw_version import DUR_VER import durdraw.help import durdraw.neofetcher as neofetcher class ArgumentChecker: """ Place to hold any methods for verifying CLI arguments, beyond what argparse can do on its own. Call these methods via argparse.add_argument(.. type= ..) paramaters. """ def undosize(size_s): size = int(size_s) # because it comes as a string if size >= 1 and size <= 1000: return size else: raise argparse.ArgumentTypeError("Undo size must be between 1 and 1000.") @log.log_on_crash def main(fetch_args=None): DUR_FILE_VER = 7 DEBUG_MODE = False # debug = makes debug_write available, sends verbose notifications durlogo = 'Durdraw' argChecker = ArgumentChecker() parser = argparse.ArgumentParser() parserStartScreenMutex = parser.add_mutually_exclusive_group() parserFilenameMutex = parser.add_mutually_exclusive_group() parserColorModeMutex = parser.add_mutually_exclusive_group() parserFilenameMutex.add_argument("filename", nargs='?', help=".dur or ascii file to load") parserFilenameMutex.add_argument("-p", "--play", help="Just play .dur, .ANS or .ASC file or files, then exit", nargs='+') parser.add_argument("-d", "--delayexit", help="Wait X seconds after playback before exiting (requires -p)", nargs=1, type=float) parserStartScreenMutex.add_argument("-x", "--times", help="Play X number of times (requires -p)", nargs=1, type=int) parserColorModeMutex.add_argument("--256color", help="Try 256 color mode", action="store_true", dest='hicolor') parserColorModeMutex.add_argument("--16color", help="Try 16 color mode", action="store_true", dest='locolor') parser.add_argument("-b", "--blackbg", help="Use a black background color instead of terminal default", action="store_true") parser.add_argument("-W", "--width", help="Set canvas width", nargs=1, type=int) parser.add_argument("-H", "--height", help="Set canvas height", nargs=1, type=int) parser.add_argument("-m", "--max", help="Maximum canvas size for terminal (overrides -W and -H)", action="store_true") parser.add_argument("--wrap", help="Number of columns to wrap lines at when loading ASCII and ANSI files (default 80)", nargs=1, type=int) parser.add_argument("--nomouse", help="Disable mouse support", action="store_true") parser.add_argument("--cursor", help="Cursor mode (block, underscore, or pipe)", nargs=1) parser.add_argument("--notheme", help="Disable theme support (use default theme)", action="store_true") parser.add_argument("--theme", help="Load a custom theme file", nargs=1) #parser.add_argument("-A", "--ibmpc", "--cp437", help="Use Code Page 437 (IBM-PC/MS-DOS) block character encoding instead of Unicode. (Needs CP437 capable terminal and font)", action="store_true") parser.add_argument("--cp437", help="Display extended characters on the screen using Code Page 437 (IBM-PC/MS-DOS) encoding instead of Utf-8. (Requires CP437 capable terminal and font) (beta)", action="store_true") parser.add_argument("--export-ansi", action="store_true", help="Export loaded art to an .ansi file and exit") parser.add_argument("-u", "--undosize", help="Set the number of undo history states - default is 100. More requires more RAM, less saves RAM.", nargs=1, type=int) #--mental -- Enable experimental (not ready for prime time) options parser.add_argument("--mental", action="store_true", help=argparse.SUPPRESS) parser.add_argument("--fetch", help="Replace fetch strings with Neofetch output", action="store_true") parser.add_argument("-V", "--version", help="Show version number and exit", action="store_true") parser.add_argument("--debug", action="store_true", help=argparse.SUPPRESS) args = parser.parse_args(fetch_args) if args.version: print(DUR_VER) exit(0) if args.times and not args.play: print("-x option requires -p") exit(1) # Initialize application app = AppState() # to store run-time preferences from CLI, environment stuff, etc. app.setDurVer(DUR_VER) app.setDurFileVer(DUR_FILE_VER) app.setDebug(DEBUG_MODE) term_size = os.get_terminal_size() # Parse general command-line paramaters if args.debug: app.setDebug(True) if args.undosize: app.undoHistorySize = int(args.undosize[0]) #if args.width and args.width[0] > 80 and args.width[0] < term_size[0]: if args.width and args.width[0] > 1 and args.width[0] < term_size[0]: app.width = args.width[0] #if args.height and args.height[0] > 24 and args.height[0] < term_size[1]: if args.height and args.height[0] > 1 and args.height[0] < term_size[1]: app.height = args.height[0] if args.max: app.maximize_canvas() if args.wrap: app.wrapWidth = args.wrap[0] app.showStartupScreen=False app.quickStart = True if args.nomouse: app.hasMouse = False if args.notheme: app.themesEnabled = False if args.hicolor: app.colorMode = "256" if args.locolor: app.colorMode = "16" if args.cp437: app.charEncoding = 'cp437' #app.drawChar = '$' app.colorPickChar = app.CP438_BLOCK # ibm-pc/cp437 ansi block character app.blockChar = app.CP438_BLOCK app.drawChar = app.CP438_BLOCK else: app.charEncoding = 'utf-8' durhelp_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp.dur") if args.blackbg: app.blackbg = False else: app.blackbg = True # load configuration file if app.loadConfigFile(): # Load main optiona if 'Main' in app.configFile: mainConfig = app.configFile['Main'] # load color mode if 'color-mode' in mainConfig: app.colorMode = mainConfig['color-mode'] if 'disable-mouse' in mainConfig: if mainConfig.getboolean('disable-mouse'): app.hasMouse = False if 'max-canvas' in mainConfig: if mainConfig.getboolean('max-canvas'): app.maximize_canvas() if 'cursor-mode' in mainConfig: app.screenCursorMode = mainConfig['cursor-mode'] if 'scroll-colors' in mainConfig: if mainConfig.getboolean('scroll-colors'): app.scrollColors = True if 'mental-mode' in mainConfig: app.mental = True # load theme set in config fileFalse if app.colorMode == "256": app.loadThemeFromConfig("Theme-256") else: app.loadThemeFromConfig("Theme-16") if 'Logging' in app.configFile: logging_config = dict(app.configFile['Logging']) if 'local-tz' in logging_config: logging_config['local_tz'] = app.configFile['Logging'].getboolean('local-tz') del logging_config['local-tz'] app.setLogger(**logging_config) if args.theme: if app.colorMode == "256": app.loadThemeFile(args.theme[0], "Theme-256") else: app.loadThemeFile(args.theme[0], "Theme-16") app.customThemeFile = args.theme[0] if args.cursor: if args.cursor[0] in app.validScreenCursorModes: app.screenCursorMode = args.cursor[0] else: print("--cursor option requires one of the following: block, underscore, pipe") exit(1) # Load help file - first look for resource path, eg: python module dir durhelp_fullpath = '' #durhelp_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp.dur") durhelp256_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp-256-long.dur") #durhelp256_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp-256-page1.dur") #durhelp256_page2_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp-256-page2.dur") durhelp16_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp-16-long.dur") #durhelp16_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp-16-page1.dur") #durhelp16_page2_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp-16-page2.dur") app.durhelp256_fullpath = durhelp256_fullpath #app.durhelp256_page2_fullpath = durhelp256_page2_fullpath app.durhelp16_fullpath = durhelp16_fullpath #app.durhelp16_page2_fullpath = durhelp16_page2_fullpath #app.loadHelpFile(durhelp_fullpath) #app.loadHelpFile(durhelp16_fullpath) #app.loadHelpFile(durhelp16_page2_fullpath, page=2) #if not app.hasHelpFile: # durhelp_fullpath = 'durdraw/help/durhelp.dur' # app.loadHelpFile(durhelp_fullpath) if app.showStartupScreen: print(durlogo) if app.hasHelpFile: print(f"Help file: Found in {durhelp_fullpath}") else: print("Help file: Not found") if app.ansiLove: print("ansilove = Found") else: print("ansilove = Not found (no PNG or GIF export support)") if app.PIL: print("PIL = Found") else: print("PIL = Not found (no GIF support)") if app.hasMouse: print("Mouse = Enabled") else: print("Mouse = Disabled") if app.configFileLoaded: print(f"Configuration file found: {app.configFileName}") else: print(f"Configuration file not found.") if app.themesEnabled: print(f"Theme: {app.themeName}") else: print(f"Theme: Default (none)") print("Undo history size = %d" % app.undoHistorySize) print("Canvas size: %i columns, %i lines" % (app.width, app.height)) if args.wait: try: choice = input("Press Enter to Continue...") except KeyboardInterrupt: print("\nCaught interrupt, exiting.") exit(0) else: pass time.sleep(3) if args.play: app.playOnlyMode = True app.editorRunning = False if args.mental: # Enable exprimental options app.mental = True if args.fetch: #app.playOnlyMode = True app.fetchMode = True app.fetchData = neofetcher.run() #app.editorRunning = False #app.drawBorders = False #ui = curses.wrapper(UI_Curses, app) ui = UI_Curses(app) if app.hasMouse: ui.initMouse() if app.blackbg: ui.enableTransBackground() if args.filename: ui.loadFromFile(args.filename, 'dur') if args.play or args.fetch: # Just play files and exit app.drawBorders = False if args.times: app.playNumberOfTimes = args.times[0] for movie in args.play: ui.stdscr.clear() ui.loadFromFile(movie, 'dur') if app.fetchMode: ui.replace_neofetch_keys() ui.startPlaying() if args.delayexit: time.sleep(args.delayexit[0]) ui.verySafeQuit() if args.export_ansi: # Export ansi and exit ui.saveAnsiFile(os.path.splitext(args.filename)[0] + ".ansi", encoding=app.charEncoding) ui.verySafeQuit() ui.refresh() ui.mainLoop() if __name__ == "__main__": main() durdraw-0.29.0/durdraw/neofetcher.py000066400000000000000000000047451476305210600174340ustar00rootroot00000000000000# Run Neofetch. Extract data from it. Return a dict containing neofetch data. import os import pathlib import re import subprocess neo_keys = ['OS', 'Host', 'Kernel', 'Uptime', 'Packages', 'Shell', 'Resolution', 'DE', 'WM', 'WM Theme', 'Terminal', 'Terminal Font', 'CPU', 'GPU', 'Memory'] def strip_escape_codes(text): # 7-bit C1 ANSI sequences ansi_escape = re.compile(r''' \x1B # ESC (?: # 7-bit C1 Fe (except CSI) [@-Z\\-_] | # or [ for CSI, followed by a control sequence \[ [0-?]* # Parameter bytes [ -/]* # Intermediate bytes [@-~] # Final byte ) ''', re.VERBOSE) text = ansi_escape.sub('', text) text = re.sub(r'\d+\.\d+ms', '', text).strip() return text def fetcher_available(name="neofetch"): arg = "--version" try: devnull = open(os.devnull) subprocess.Popen([name, arg], stdout=devnull, stderr=devnull).communicate() except OSError as e: #if e.errno == os.errno.ENOENT: # return False return False return True def run(): # make an empty dict of Neofetch keys for us to populate and return neofetch_results = {} for key in neo_keys: neofetch_results[key] = '' # Run neofetch, capture the output #fetcher_exec = pathlib.Path(__file__).parent.joinpath("neofetch/neofetch") fetcher_exec = ["neofetch", "--stdout"] # not a full path. just the executable run from the $PATH #fetcher_exec = ["fastfetch", "-c", "ci"] # not a full path. just the executable run from the $PATH neofetch_output = subprocess.check_output(fetcher_exec).decode() neofetch_lines = neofetch_output.split('\n')[2:] # Parse the neofetch output into neofetch_results{} for line in neofetch_lines: if line == '': break try: line = strip_escape_codes(line) key = line.split(': ')[0].strip() value = line.split(': ')[1].strip() #value = value.split(' ')[0].strip() # Remove trailing " 0.020ms" crap. #new_value = re.sub(r'ms$', " ", value) #new_value = re.sub(r'\s+\d+\.\d+ms$', '', value).strip() new_value = value #print(f"found key: {key}, new_value: {new_value}") except: break if key in neo_keys: neofetch_results[key] = value return neofetch_results if __name__ == "__main__": results = run() print(results) durdraw-0.29.0/durdraw/plugins/000077500000000000000000000000001476305210600164075ustar00rootroot00000000000000durdraw-0.29.0/durdraw/plugins/bounce_movie.py000066400000000000000000000016771476305210600214460ustar00rootroot00000000000000# Durdraw Plugin # Type: Transform Movie # Name: Bounce |> -> |><|, aka Ping-Pong import copy # Durdraw plugin format version durdraw_plugin_version = 1 # Plugin information durdraw_plugin = { "name": "Bounce", "author": "Sam Foster, samfoster@gmail.com", "version": 1, # Plugin verison, if applicable "type": "transform_movie", "desc": "Duplicate all frames and reverse them, then append them to the end. |> -> |><|" } def transform_movie(mov): # Make a copy of the frames newframes = copy.deepcopy(mov.frames) # Remove the last frame, otherwise it will be shown 2 times in a row after transform newframes.pop() # Reverse copy with slicing trick newframes = newframes[::-1] # Remove the last frame again, for the same reason (when it loops back around) newframes.pop() # Append it to the movie frames mov.frames = mov.frames + newframes mov.frameCount = len(mov.frames) return mov durdraw-0.29.0/durdraw/plugins/convert_charset.py000066400000000000000000000024511476305210600221540ustar00rootroot00000000000000 cp437_control_codes_to_utf8 = { 0x01: 0x263A, # ☺ White? smiling face 0x02: 0x263B, # ☻ Black? smiling face 0x03: 0x2665, # ♥ Heart 0x04: 0x2666, # ♦ Diamond 0x05: 0x2663, # ♣ Club 0x06: 0x2660, # ♠ Space 0x07: 0x2022, # • Bullet point 0x08: 0x25D8, # ◘ Inverse bullet point 0x09: 0x25CB, # ○ Circle bullet point 0x0A: 0x25D9, # ◙ Inverse circle 0x0B: 0x2642, # ♂ Male 0x0C: 0x2640, # ♀ Female 0x0D: 0x266A, # ♪ Eigth note (music) 0x0E: 0x266B, # ♫ Double eight note (music) 0x0F: 0x263C, # ☼ Solar 0x10: 0x25BA, # ► Right-Pointing Pointer 0x11: 0x25C4, # ◄ Left-Pointing Pointer 0x12: 0x2195, # ↕ Up Down Arrow 0x13: 0x203C, # ‼ Double Exclamation Mark 0x14: 0x00B6, # ¶ Pilcrow/Paragraph 0x15: 0x00A7, # § Section 0x16: 0x25AC, # ▬ Black Rectangle 0x17: 0x21A8, # ↕ Underline Up Down Arrow 0x18: 0x2191, # ↑ Up arrow 0x19: 0x2193, # ↓ Down Arrow 0x1A: 0x2192, # → Right arrow 0x1B: 0x2190, # ← Left arrow 0x1C: 0x221F, # ∟ Right angle 0x1D: 0x2194, # ↔ Left Right Arrow (If and only if) 0x1E: 0x25B2, # ▲ Up-Pointing Pointer 0x1F: 0x25BC, # ▼ Down-Pointing Pointer } durdraw-0.29.0/durdraw/plugins/repeat_movie.py000066400000000000000000000012141476305210600214360ustar00rootroot00000000000000# Durdraw Plugin # Type: Transform Movie # Name: Repeat |> -> |>|> import copy # Durdraw plugin format version durdraw_plugin_version = 1 # Plugin information durdraw_plugin = { "name": "Repeat", "author": "Sam Foster, samfoster@gmail.com", "version": 1, # Plugin verison, if applicable "type": "transform_movie", "desc": "Duplicate all frames and append them to the end. |> -> |>|>" } def transform_movie(mov): # Make a copy of the frames mov.newframes = copy.deepcopy(mov.frames) # Append it to the movie frames mov.frames = mov.frames + mov.newframes mov.frameCount = len(mov.frames) return mov durdraw-0.29.0/durdraw/plugins/reverse_movie.py000066400000000000000000000007671476305210600216450ustar00rootroot00000000000000# Durdraw Plugin # Type: Transform Movie # Name: Reverse |> -> <| # Durdraw plugin format version durdraw_plugin_version = 1 # Plugin information durdraw_plugin = { "name": "Reverse", "author": "Sam Foster, samfoster@gmail.com", "version": 1, # Plugin verison, if applicable "type": "transform_movie", "desc": "Reverses the order of the frames in a movie" } def transform_movie(mov): # Use slicing trick to reverse frames mov.frames = mov.frames[::-1] return mov durdraw-0.29.0/durdraw/themes/000077500000000000000000000000001476305210600162135ustar00rootroot00000000000000durdraw-0.29.0/durdraw/themes/default.dtheme.ini000066400000000000000000000015261476305210600216110ustar00rootroot00000000000000; Default Theme ; ; name = theme name ; mainColor = what most stuff is drawn as ; clickColor = clickable items (buttons) ; clickHighlightColor = the color the button changes to for a moment when clicked ; borderColor = the color of the border around a drawing ; notificationColor = the color for notification messages ; promptColor = the color for user prompt messages ; ; color codes numbers can be found in Durdraw's 256-color selector. [Theme-256] name: 'Default 256' mainColor: 7 clickColor: 2 clickHighlightColor: 10 borderColor: 7 notificationColor: 3 promptColor: 3 menuItemColor: 7 menuTitleColor: 98 menuBorderColor: 7 [Theme-16] name: 'Default 16' mainColor': 8 clickColor': 3 clickHighlightColor': 11 borderColor': 8 notificationColor': 8, # grey promptColor': 8, # grey menuItemColor': 2, menuTitleColor': 3, menuBorderColor': 4, durdraw-0.29.0/durdraw/themes/mutedchill-16.dtheme.ini000066400000000000000000000012101476305210600225310ustar00rootroot00000000000000; colors for 16-color mode: ; 1 black ; 2 blue ; 3 green ; 4 cyan ; 5 red ; 6 magenta ; 7 yellow ; 8 white ; ; name = theme name ; mainColor = what most stuff is drawn as ; clickColor = clickable items (buttons) ; clickHighlightColor = the color the button changes to for a moment when clicked ; borderColor = the color of the border around a drawing ; notificationColor = the color for notification messages ; promptColor = the color for user prompt messages [Theme-16] name: 'Muted Chill' mainColor: 8 clickColor: 3 borderColor: 6 clickHighlightColor: 5 notificationColor: 4 promptColor: 4 menuItemColor: 8 menuTitleColor: 2 menuBorderColor: 4 durdraw-0.29.0/durdraw/themes/mutedform-256.dtheme.ini000066400000000000000000000013721476305210600225000ustar00rootroot00000000000000; name: theme name ; mainColor: the color of most text ; clickColor: the color of buttons (clickable items) ; clickHighlightColor: the color the button changes to for a moment when clicked ; borderColor: the color of the border around a drawing ; notificationColor: the color of notification messages ; promptColor: the color of user prompt messages ; menuItemColor: the color of menu items ; menuTitleColor: the color of menu titles ; menuBorderColor: the color of the border around menus ; ; color codes numbers can be found in Durdraw's 256-color selector. [Theme-256] name: 'Muted Form' mainColor: 104 clickColor: 37 borderColor: 236 clickHighlightColor: 15 notificationColor: 87 promptColor: 189 menuItemColor: 189 menuTitleColor: 159 menuBorderColor: 24 durdraw-0.29.0/durdraw/themes/poopy-256.dtheme.ini000066400000000000000000000010571476305210600216440ustar00rootroot00000000000000; name = theme name ; mainColor = what most stuff is drawn as ; clickColor = clickable items (buttons) ; clickHighlightColor = the color the button changes to for a moment when clicked ; borderColor = the color of the border around a drawing ; notificationColor = the color for notification messages ; promptColor = the color for user prompt messages ; ; color codes numbers can be found in Durdraw's 256-color selector. [Theme-256] name: 'Poopy' mainColor: 130 clickColor: 95 borderColor: 58 clickHighlightColor: 15 notificationColor: 100 promptColor: 94 durdraw-0.29.0/durdraw/themes/purpledrank-16.dtheme.ini000066400000000000000000000011241476305210600227320ustar00rootroot00000000000000; colors for 16-color mode: ; 1 black ; 2 blue ; 3 green ; 4 cyan ; 5 red ; 6 magenta ; 7 yellow ; 8 white ; ; name = theme name ; mainColor = what most stuff is drawn as ; clickColor = clickable items (buttons) ; clickHighlightColor = the color the button changes to for a moment when clicked ; borderColor = the color of the border around a drawing ; notificationColor = the color for notification messages ; promptColor = the color for user prompt messages [Theme-16] name: 'Purple Drank' mainColor: 6 clickColor: 3 borderColor: 6 clickHighlightColor: 5 notificationColor: 4 promptColor: 4 durdraw-0.29.0/durfetch000077500000000000000000000005231476305210600150100ustar00rootroot00000000000000#!/usr/bin/env python3 # Launches Durfetch from durdraw import durfetch #from durdraw.durdraw_options import Options #from durdraw.durdraw_movie import * if __name__ == "__main__": #try: durfetch.main() #except Exception as e: # print(f"Exception: {e}") # print("Caught exception. Exiting.") # exit(1) durdraw-0.29.0/durfetch.1000066400000000000000000000016551476305210600151530ustar00rootroot00000000000000.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. .TH DURFETCH "1" "October 2024" "durfetch 0.29.0" "User Commands" .SH NAME durfetch \- manual page for durfetch 0.29.0 .SH DESCRIPTION usage: durfetch [\-h] [\-r | \fB\-l\fR LOAD] [\-\-linux | \fB\-\-bsd]\fR [\-V] [filename ...] .PP An animated fetcher. A front\-end for Durdraw and Neofetch integration. .SS "positional arguments:" .TP filename \&.durf ASCII and ANSI art file or files to use .SS "options:" .TP \fB\-h\fR, \fB\-\-help\fR show this help message and exit .TP \fB\-r\fR, \fB\-\-rand\fR Pick a random animation to play .TP \fB\-l\fR LOAD, \fB\-\-load\fR LOAD Load an internal animation .TP \fB\-\-linux\fR Show a Linux animation .TP \fB\-\-bsd\fR Show a BSD animation .TP \fB\-V\fR, \fB\-\-version\fR Show Version information and quit .PP Available animations for \fB\-l\fR: .PP bsd cm\-eye linux\-fire linux\-tux unixbox .SH "SEE ALSO" .B durdraw, .B durview durdraw-0.29.0/durformat.md000066400000000000000000000142721476305210600156110ustar00rootroot00000000000000Durdraw File Format version 7 (draft) - May 2023 Durdraw is an ANSI art editor that handles animation, Unicode and 256 color. This document describes its primary file format, "dur." Durdraw files store color text art (both animated and static) in a file format with the ".dur" file extension. This is a JSON file that has been gzipped. It contains an object named "DurMovie" with a set of key/values specifying metadata and "movie" data. Movie data includes the color format (16 and 256 for now), character encoding, canvas size, frame rate, etc. Metadata includes the artwork name and artist name, which can be used to generate "sauce" records. "DurMovie" contains an array called "frames." Each element of "frames" is a Frame object, which contains a frame number, a delay amount (in seconds), a "contents" array, and a "colorMap" array. The "contents" array contains strings, each one of which represents a line of ASCII or Utf-8 text in a frame of art. This can be thought of as a flat ASCII (or Unicode) art file which has been separated into lines, with each line stored as a string in the array. Similarly, colorMap contains an array of lines, wich each line containing a list of foreground and background color pairs. For example, the pair [1,0] represents blue text with a black background. Each element of the colorMap should coordinate with a corresponding line and column in the contents. For example, colorMap[2][3] should describe the foreground and background color for the character at contents[2][3], which is the character at Line 2, Column 3 of the given frame. Here is the full list of JSON keys stored in a DurMovie object, and their purpose: ``` "formatVersion" - The DurDraw file format version "colorFormat" - The color format of the movie. 16, 256, RGB, mIRC, etc. "preferredFont" - The preferred font to use (optional) "encoding" - Text encoding format. Options include "utf-8" and "cp437" "name" - The name of the movie or artwork "artist" - The artist name "framerate" - The playback speed, specified as Frames Per Second "columns" - The number of columns in the canvas (formerly sizeX) "lines" - The number of lines in the canvas (formerly sizeY) "extra" - This can be used to store any JSON object that the user wants to include with their art, perhaps to be used in a custom way. It is not used for anything by Durdraw. (optional) "frames" - An array of frame objects "delay" - the amount of time to stay on a frame, in seconds "contents" - An array of lines containing the ASCII or Unicode artwork "colorMap" - An array of arrays containing the foreground and background colors for a given line and column in the canvas ``` Here is an example Durdraw file, containing an animation with 3 lines, 10 columns and 6 frames: ``` { "DurMovie": { "formatVersion": 7, "colorFormat": "256", "preferredFont": "fixed", "encoding": "utf-8", "name": "", "artist": "", "framerate": 6.0, "sizeX": 10, "sizeY": 3, "extra": null, "frames": [ { "frameNumber": 1, "delay": 0, "contents": [ "O ", " ", " " ], "colorMap": [ [[12, 8],[1, 0],[1, 0]], [[1, 0],[7, 8],[1, 0]], [[7, 8],[7, 8],[1, 0]], [[7, 8],[7, 8],[1, 0]], [[7, 8],[7, 8],[7, 8]], [[7, 8],[7, 8],[7, 8]], [[7, 8],[7, 8],[7, 8]], [[7, 8],[1, 0],[1, 0]], [[7, 8],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0] ] ] }, { "frameNumber": 2, "delay": 0, "contents": [ " ", " O ", " " ], "colorMap": [ [[1, 0],[1, 0],[1, 0]], [[1, 0],[12, 8],[1, 0]], [[1, 0],[7, 8],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0] ] ] }, { "frameNumber": 3, "delay": 0, "contents": [ " ", " ", " O " ], "colorMap": [ [[1, 0],[1, 0],[1, 0]], [[1, 0],[7, 8],[1, 0]], [[1, 0],[7, 8],[12, 8]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0] ] ] }, { "frameNumber": 4, "delay": 0, "contents": [ " ", " ", " o " ], "colorMap": [ [[1, 0],[1, 0],[1, 0]], [[1, 0],[7, 8],[1, 0]], [[1, 0],[7, 8],[12, 8]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0] ] ] }, { "frameNumber": 5, "delay": 0, "contents": [ " ", " O ", " " ], "colorMap": [ [[1, 0],[1, 0],[1, 0]], [[1, 0],[7, 8],[1, 0]], [[1, 0],[7, 8],[7, 8]], [[1, 0],[12, 8],[12, 8]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0] ] ] }, { "frameNumber": 6, "delay": 0, "contents": [ " O ", " ", " " ], "colorMap": [ [[1, 0],[1, 0],[1, 0]], [[1, 0],[7, 8],[1, 0]], [[1, 0],[7, 8],[7, 8]], [[1, 0],[1, 0],[7, 8]], [[1, 0],[12, 8],[1, 0]], [[12, 8],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0]], [[1, 0],[1, 0],[1, 0] ] ] } ] } } ```durdraw-0.29.0/durview000077500000000000000000000003761476305210600146770ustar00rootroot00000000000000#!/usr/bin/env python3 # Launches Durview from durdraw import durview if __name__ == "__main__": #try: durview.main() #except Exception as e: # print(f"Exception: {e}") # print("Caught exception. Exiting.") # exit(1) durdraw-0.29.0/durview.1000066400000000000000000000022031476305210600150220ustar00rootroot00000000000000.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. .TH DURVIEW "1" "February 2025" "durview 0.29.0" "User Commands" .SH NAME durview \- manual page for durview 0.29.0 .SH DESCRIPTION usage: durview [\-h] [\-\-256color | \fB\-\-16color]\fR [\-\-cp437] [\-b] [\-\-wrap WRAP] .IP [\-\-nomouse] [\-\-theme THEME] [\-V] [filename ...] .SS "positional arguments:" .TP filename \&.ANS, .ASC or .dur file(s) to view .SS "options:" .TP \fB\-h\fR, \fB\-\-help\fR show this help message and exit .TP \fB\-\-256color\fR Try 256 color mode .TP \fB\-\-16color\fR Try 16 color mode .TP \fB\-\-cp437\fR Display extended characters on the screen using Code Page 437 (IBM\-PC/MS\-DOS) encoding instead of Utf\-8. (Requires CP437 capable terminal and font) (beta) .TP \fB\-b\fR, \fB\-\-blackbg\fR Use a black background color instead of terminal default .TP \fB\-\-wrap\fR WRAP Number of columns to wrap lines at when loading ASCII and ANSI files (default 80) .TP \fB\-\-nomouse\fR Disable mouse support .TP \fB\-\-theme\fR THEME Load a custom theme file .TP \fB\-V\fR, \fB\-\-version\fR Show version number and exit .SH "SEE ALSO" .B durdraw, .B durfetch . durdraw-0.29.0/example-charset.ini000066400000000000000000000005451476305210600170450ustar00rootroot00000000000000; Custom character set ; 🭨 🭩 🭪 🭫 🭬🭭🭮 🭯 🮚 🮛 🮜 🮝 🮞 🮟 ◤ ◥ ◢ ◣ [Character Set] name: Cool Characters encoding: utf-8 [block 1] f1: 🭨 f2: 🭩 f3: 🭪 f4: 🭫 f5: 🭬 f6: 🭭 f7: 🭮 f8: 🭯 f9: 🮚 f10: 🮛 [block 2] f1: 🮜 f2: 🮝 f3: 🮞 f4: 🮟 f5: ◤ f6: ◥ f7: ◢ f8: ◣ f9: f10: durdraw-0.29.0/examples/000077500000000000000000000000001476305210600150745ustar00rootroot00000000000000durdraw-0.29.0/examples/cm-doge.asc000066400000000000000000000020531476305210600170770ustar00rootroot00000000000000 ;i. M$L .;i. M$Y; .;iii;;. ;$YY$i._ .iiii;;;;; .iiiYYYYYYiiiii;;;;i;iii;; ;;; .;iYYYYYYiiiiiiYYYiiiiiii;; ;;; .YYYY$$$$YYYYYYYYYYYYYYYYiii;; ;;;; .YYY$$$$$$YYYYYY$$$$iiiY$$$$$$$ii;;;; :YYYF`, TYYYYY$$$$$YYYYYYYi$$$$$iiiii; Y$MM: \ :YYYY$$P"````"T$YYMMMMMMMMiiYY. `.;$$M$$b.,dYY$$Yi; .( .YYMMM$$$MMMMYY .._$MMMMM$!YYYYYYYYYi;.`" .;iiMMM$MMMMMMMYY ._$MMMP` ```""4$$$$$iiiiiiii$MMMMMMMMMMMMMY; MMMM$: :$$$$$$$MMMMMMMMMMM$$MMMMMMMYYL :MMMM$$. .;PPb$$$$MMMMMMMMMM$$$$MMMMMMiYYU: iMM$$;;: ;;;;i$$$$$$$MMMMM$$$$MMMMMMMMMMYYYYY `$$$$i .. ``:iiii!*"``.$$$$$$$$$MMMMMMM$YiYYY :Y$$iii;;;.. ` ..;;i$$$$$$$$$MMMMMM$$YYYYiYY: :$$$$$iiiiiii$$$$$$$$$$$MMMMMMMMMMYYYYiiYYYY. `$$$$$$$$$$$$$$$$$$$$MMMMMMMM$YYYYYiiiYYYYYY YY$$$$$$$$$$$$$$$$MMMMMMM$$YYYiiiiiiYYYYYYY :YYYYYY$$$$$$$$$$$$$$$$$$YYYYYYYiiiiYYYYYYi' cmang durdraw-0.29.0/examples/cm-doge.dur000066400000000000000000000020631476305210600171240ustar00rootroot00000000000000ˮ`dcm-doge.durIo@{?`!3 CeFj&w/.g?MuM)8lK`&Wi~ftdmy:H< bh`?z4zþ޹hpsȒSwTO={|$OFcf=_7sn#v٪;q}'ΤW6cOǧi.~z|V_ON2F_y/Ck\jM^}lmEDZm;Җ wAqliKZ6\Nl'.{pRxM *v[_r $o+u0V u)#+A`PHc]v,Js0Ƽ-ke ȍTUUTtfU~8] ɼA`ʇix:ٓ2Wn]BbWde2k7h"H[wG4gPGs?KAz\I4s?]6k,h^E1y/4ּ}%[Nݯ'GS+YEᆯ֏ϑ-ZhѢE-ZhѢEpE-ZhѢE-ZhѢEE-ZhѲC%Զvu_T+[>[FM}:ԧS~ _|H9=ܥ>tYFE-ZhѢE-ZhѢE-ZhѢE-ZhѢEނ w\p…K7ZhѢR[TRXc*թTRJu~IJu*թTp… E-ZhѢE-ZhѢE-ZhѢE-ZhhѢE-ZV5hѢE2mA?loOQN:eꔩSN:e,zѢE-ZhѢE-ZhѢE-ZhѢE-ZhѢ]v ?݁ .\*~ѢE˿hѢk7͜durdraw-0.29.0/examples/cm-eye.dur000066400000000000000000000111201476305210600167620ustar00rootroot000000000000007ldcm-eye.dur[o+À-ĐkݢOEI`SH.+˖+937V~T _hvG?ΟV`-xfϯO~eo-Ϟڗntʧ/]>^~||q1:~yh]O.<.:˦hRq G:f~Q6Z|˦S~ov#uc>=>iCfW7˂cMcU*}.>M.xQvfbWxLq(tեM{bϯ:DPs6)گ׿oMS޽+O~p -B -B -Bmmh붂VɈh4h"\3/]oVI[?)qiM1pcPԸ#{+J3Bf}@ Dp9"vuP_4;+X>fסSoV&}j1:wVy^o^f;[I~䷱h~2<2>ޤM3qCmf\D.z|WJoxUz€UzzM/LzMGy'#$"VD0hT.<-B -!yadGL<<<<ZhZhZh@m{ٲq"^y?|J=o&FϏy }>W>Z"zyi%V裰?Ģ\BzK;ؼ'k2SazILk~zl]4`ϋ-Eé\B -( }BB?,( JoWV蟋S2ڿ(!z/ZhZhZhMm*._lx"*]_7"FMWOʟoOd8ښ;Z Myyy<&vu;z&}Ւ'@pKSRׯ_h2w]r.G;5sOQ+ۋ`(H!N"B -ТCBP(g#Uَ@U6@>/[=/QnxWtC:ZhdB|6o͓yݜO!|Myu''|S@Ů z^pzB -B -B -73:3PuZ+(!< ܘdӱa&U|| Qc2?s~aW*kr7w3A|BJ!GL+w>ggv|dg?;~?&^ןĎ'<NhZhZhjs؅.boY-x몇 ln*9;;;;IyRzjN|Z!*aڱ -A+.S F~YS ge QhZh'XᆳwOj光AI]wwQ޽)Vz}({d|_sߐ݇7TZhZhZhy*WyW1׏3DzlQhxh9b mޣݫve*5uwݽg{WAbtwtw2>ӻZxAxxxwOv+hs]qm7Cl{_iO>lC -BKn{sw<Pݳv=Lb>m%׏رݱ#R"(ZhZhZhZC=-;xڹ݅׽'7VBsZqqdvD?~`Ajw\wRݮ貓Sʵ>evBt'9Vs$sCJqu!ji٦N>"B -2:@k Z}ՏQdurdraw-0.29.0/examples/dogeblink.dur000066400000000000000000000041561476305210600175540ustar00rootroot00000000000000`ddogewink.dur]oF+XŪr ?n\\Z!Z;S!%Ia3 &x0gsa ~8oh厝3kyl/f͚NylwwvnW˫:9ZfGW2Zx{}yX/nbįo6"(ͶcKd2Q*ܩlEt߉Wf=r^ug:36/ 13x58vC3i}Mc' ~Н-toX/B?el|t1L~l_ǺMΝzLy:#;ZSs'F^ͶFp,˻rd;l}ZYu}}je(61yk?'ɊI"2b?xTw%6t̳$I`|lsOH]X*OٹWմYa ,+e$$\h}odahŷqw@-B-B-B-6? rG:_:W;" +v}<._|bߞ],Ryp:Fl} Y3; ƞK}$Ҝ)~T<Ayb \+ nk !+kJURG|RutIcJ9!- fpP3%gl5GZ>$JZ P:E/L(@%N'AJK`{PFZѷ6c'>샡$S59RRʼnhě|IHVJ/{TR:]ll2l+(JdH2bFGps@E\{І|J^S8ARԦ@(ots~w J- P P 箖Dpi=g{?:2VJ+R?+%tc^GX.eك`)X P P P P X `)X `)wK!9uR`)X `)4K1$oIGh/,EjXEQ3:ꅥBzX `)X `)X `)X `)nX QRa]%W9?~xCܢוMC٫&Ջ w?tkrW*fbSG$Op|:?;'pXa<9,MCX7/V,rb%La@XSpZZZ@Xa@XaAX*e{?2^aR Knه4 ,a!,؇OhS@Xa@Xa@Xa@Xa@Xa@Xa@Xa@Xj\ , a X8ave IJ@,G>b t* 8,P P P P  8,ఀ 8,ఀ'`4apXqXX4a-T}^tpXa{ew9, 8,ఀ 8,ఀ 8,ఀ 8,ఀ 8,ఀ 8,ఀ 8,ఀ%Eŏgdurdraw-0.29.0/examples/dopehax.dur000066400000000000000000000076251476305210600172520ustar00rootroot00000000000000`ddopehax4.dur[oW3,2[CQ L)fE&).WW(<ɖ>Q$Zgr˷׿_?>?/Λw__>+˥g˳vo/S?nՊׇy[׳WqKk,I{\n]_m}/}?ڣ=۷/jO5-²(OJH&ޏtoō+-nfbn?_'_?}>?[nnCɺ6-wuɏY~^n&/+ߕk*s9uXoUzݮ;PeSbD_4_O.Icv]B&/c6T[_qQRm\ffcVhRNyrҁލ!Qn[voFϙ|<a)y\e8?}B3[ &ͯrxxO&U~k?=piay[?__jyV1z1>0z0n7ݛ;f}g#AB_"ifOWs3H [WѥwQ?_b\Rxzgr%s us8+qe4ᖤL֝in p wQvhl!"x=|mâ߶;gm_*e%O6|^ؿ䚯 mtDbH6]h-h %ǖ̓= hmOz)!gZp[pҘq;F 41A֑ ⴗB_g#TxcD.RD$巸N|1W YUA.ǫRK3LbT(zΐ}gCW5ܧ UԠWmyXS3"9eѦ) SUl$'CA-p˳hQnRqoP۶тٽ1sً\ K q3+̃ہ=." Y{POa)vn@MK|/xq F,;*W&ߊ΃ht |tIs>ffd4!\$T|VT8,%ZyܡM PӖ+{BĮ5mz8榽HpS; , ;ƔUҴېf0J5/f3*,ep"67`N4_].z~ȨO+mk\ycbMxm"о+Zj7sRҍ,\3 ;f3I=-LG6'\{\'q"NЊ#>fvqĮY8Fn rNWџYFG| P{qBb0g L4G@_&$z#- Lp<_@{(+$Rj,CɆLRT1$ܧ8ΰjZiO$lhӅ6JDز6}l2 X&~RP^Aa{k품X52K32Kra"&<$gD;$r/F%Tpb* |$tdKgqNR`* jLnxl0%j>MM=M6\\v. `ƭ}-ƽ2'nlR7E7&7~\. ԡԋ@fzDYYU\QôUIdaFX7qIdٵF&ldIIc&TL$h xU(@ka^m  N I6 zDII`6Am|TIu ZgbSM j7k m}M!l.$2B$l;_$pz$+/&L%"ݳ.ӉhR2s.C 4ԛMwjpIM=ޛMUpj6 $Dt6d$ M]$3p0&!6$ Z\΢:!gy"vcD6dP$F"CbJ00M>#G'}D?&;$& 0DgH} Jp=`GM>P,E>CHq G+dXhA,'[6GMTBkCZ c.m!5:ZoGR#pqD=0?@<5+?" ?72S;E;H Y-A4a0 ÌއAffzxubDg$RX"sS7P D 9mymcF+F!#>G=w@b@6G0O>+c{t|x{h (!++A~3屓Gd/@(ڨaH'RZր#lT Vw:<ڀU( J!=54&(p-&px/@) 1 +{V(.dTZz!n./<";O\߃pql ']Gy_<":yC+G(f򈊯zn' #*<G' z\ \$b@XӚ'& %!Kb5ȢH"P @6a$!1 Ѿ3Fo BGٝVwr@o`4 } fvsc $ܻC#ly#[/G ryc%H0k}O8 ZB^pWU%1P#eAq菸X?">ĮGG8LeUHE5?|J>&wIWB}:B=03#G3M"qPI`$zĢ𩓐_[IY)/1Hl;tlZh :ne-e1i٩&co8/Jx-fCg㛎O p#%ȑt-kvMSQ %# %8YPcitJ':;9%t5WXCԮB SjPZj%1\~rb 3`D;E(W3 ~KH֭&6Pm> K> ˲FzI8Ff`̅TIm-6`6 㼚&TmRNp&4Q$\QM!IdtIQ$j%\%ۄ Cۺ$J/뛾e0I;$kd*I0yyMo$" s$$ 8V4aXsjAT~ȨO$k3IE5IBb$iQ] Uv$u4I\{_aT3.^Jӏ~zQLdurdraw-0.29.0/examples/file_id.dur000066400000000000000000000026101476305210600172020ustar00rootroot00000000000000ugfile_id.dur[o6+8a/ mQS؀nؖg˫D dKW俏Ew]bvA#%><EΗ+FM)F3?u=,_l ǻezfoB;r'QUE(ˢ4)S"QLWqOQׯ)HE#x9 WyQ Yv~+rq;%M>g${ZݖϠݗ>e3ޗggSyK %9-_pu78O3;@hHu'H-{-ZVX4 ip ~xX|Ĭ/9x;l>~ /TgNmug:~ItTAVKɪĝ2,zUごBsj4K':mJ9ICr7I=Z9p'_3UUi.면 ;hyNZjI¡::ȺQEaz(>%mAd^J]]&iiCⰃ(sqQVU l$=a.|Z21We 6'K>b{GpYп|M[ab~bd|@N/I\'1^)%4NX\y]9+^r>>Fx 5b_SY,>1;1k]PH_}~޵\˚ױ:Y4'~ȏn^ cv8,-;X8 <sM@ !g T? y5jKi47CFH.9&& z>sҹӵSvFg&mtc`73y^{sGGį̻t0%ᥒ륯`Ȑ]*z%=C;iZ$>!Y]|NF񚓓 `(`q{t+}&9-8MW8T=ˀT 郆&HAD`r^ rB]7D/ J~gC/V@wX$>\t@J2"w.=OJ|h |SD7W:$x$${U` A0 ` A0 ` A0 0X1 `OmPdurdraw-0.29.0/examples/indyz-dopehax-256.dur000066400000000000000000000075231476305210600207140ustar00rootroot00000000000000edopehax8.dur[s۸+XWƔCT_;yCNXq'wN{) ssТ( |M\<6b?voOn9?y??>z8>tpګۋa_|erZ\\W=oYWc5ož礊߾4?}oZE ܿHL Ξ\ VsLϧwf/f|pd|6ޕeC8l,N"MR2vq_k[+ϻ=ǘJ2?;\m}>\8l޴/cvl˸lvQt=Dg%ysE0Eߋo):Y2Ygl8^'9Etn_maa }ERvn]=$]PnonVb[s_nyZnRv_?A)k&]5a uRϧkBVT}o9Xwa۠' [Oh  vZ/w}Λ&ݺ]fݪC֠Y9wtx9?6<]V&-wO%R/m Ņm7jںo4.=9sl#P(&Χm̾v m q |38>¢x{su c[}>{i!j}7pD-" $qohC qЊqЦ/nҼXNϸ5`}UYR: ⬟4Bb]jM!ׯAO07=d6hq1װyڜ\D&Z%;D3rC/MflSf/i87<: -<3sK3y7{[unQ 샽 Eq(IkkDa|^[>K5k$wof )*R[/pk 566K%S`"FbM= m( 0\ {E\Bg"M|P}VI.I 6hDY?c}'A7+[2J200f0ePZ4\ss DT*^fW';mUݿ x-C*bHіb;J%N䮉@ #ݭ#n NL_J9UG LOIFV 4V1 _śXqS1\fAt7NV3(Ô!'K 33.~)&,nW=#:bRGħx[#2# Ur ):R%GqXTPGT+:BY*u %ɐ?v8-48ɂOQGTZ?¼x?8?B>?¸F40ֈeB'H&WDWuy=֫9N%#i caڋ(q8h\:8Ż"N,_7{(sP=Un2-h;Ey=hOE-ᾊiOdy+u@qHT$QYZş$3=k􉱝7\Pdta@5t|:@=UϒQ{<5A{!K{BL^D ^!. Gk/krbҾ]u!G v\W{(xeڋfMDk`%U3LHf*"ąS^IVk/. Q{":bSi/6 ^J^]Y%hhڋD7$Pwet.rUv]Tave*\TkC{.**zM,0>/Dj0F/Tk}8] E{I{]!0v9qiD;zZLIQq"$SU{KV~ZRH$Si/_2 xxd]{̭dVl/ڋ@N r^d@jfMDK`e'LҦ³=qEy麗(뎒~F7gZWëN\G5E6)&5EtjmxЫ)ŦVSl;(UGnxk^5EA-bl>BOq=`?T- EJPF"7UKOcT#0,8cP!GFke;qw.VlݑN _ U4&Z#I\1FZ9юֺ4ࡾ*.ֻW*eT:Lr Ȑn@N~Y(e$:gei"v2"noEܟ€ d`~8L{xⰰ@u&D@n;NR0Vòz7,MXl>Gc6>"%-bIsGa,SEzm2ZDoaUkZuUV(thl[>%aj\zkf1QqU\4d²e,䱪/aD2kW|y}, #K0"ƇLp㔖[h ` O !Z LChE eD;2/|=a?/qoEyZC%u_DWb_D8.Tzip]\T})dH}`/:KҡzKuE^ GlWw\0 y^&tT_(#v;B5z; {hH͉u!^[1ʉv\AW6~qC.S7M]fx]V~]HK(bߎ]Z.FؙId)dѫRL@h"MQKZFA' S>ky\^pz΁-D e),q5ܴ.1S;B4jIu& zi ?zEd7~;> iSKq>4T3n""#a7Uڈ ]kXq'{cA닠kۤWۺX H9qܾXChAQmfU"hE*߯-jkO ̦Ц)gnY14D1aʒDVa*e#bld|c;LF Uŵ첉h[㓞ys ieÙ J0SN*<WWEЬ;JZ*ii xS,{|Ndurdraw-0.29.0/examples/indyz-kali.utf8.ans000066400000000000000000000734111476305210600205450ustar00rootroot00000000000000▓▓▓▓▓█████▓▓▓▓▓▓▓███▓▓▓▓▓▓▓▓▓████▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓▓▓████▓▓▓▓▒▒▓▓███▓▓▒▒▓▓█████▓▓▓▓ ▓▓▓▓▓▓▓▓█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█████▓▓▓▓▓▓▓▒▒▒▓▓▓▓▓▓▓██████▓▓▓▓▓████▓▓▓▓▓▓█████▓▒ ▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓███▓████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓████▓▓▓▓▓▓████▓▓▓▓█▓▓████▒▒▒▒ ▓▓▓▓▓▒▒▒▒▀▀▔▔▔▔   ▔▔▔  ▔▔`"▀▀▀▓▓▒▒▓▓▓▒▓▓▒▓▓▒▒▓▓▓▓▒▓▓▓▓▓▓████████▓▓▓▓▓▓▒▒▒▒░░   ░ ▓▓▒▒▒▒▀`            .`           ``▀▀▒▒▒▒▒█▓▓▓███▓▓▓██████▓▓▓██▓▓▓▒▒░░          ▒▒▒▒P`          __▁▂▂▂▂▂▁__           `▀▀████▓▓▓████████▓▓▓▓▓▓▒▒░░░             ▒▒P"          ;▒▒▒▒▒▒▒▒▒▒▒▒░▆▅; ,._       `"▀▒▒▒▒▒█████▓▓▓▓▓▒░░                 ◤            ▒░░░░░░░░▒▒▒▒▒▒▒░; `▒░f` ._ ▁▂▂▂▁`""\▒▒▒▒▓▓▓▓▒▒░     ░░                        :░░░░░░░░░░░░░░░░░░i `▓    .▓▒▒░░▒▒▆▅▃`'▒▒▒▒▒▒▒:   ░░░                - ` `` -. :█   ░░░░░░░░░░░░░░░; :   `▝ █░░░░░░░░░▄.``▀███   ░░░                    ..;;... ▀███    ░░░░░░░░░░░▓; L.  ; ▓▓░░░   █░░░▆▃ `██L   ░░                  .``    _.. ░▓████████████░░░░█░░░▓;. ;█░░ ▂▃▄▅▆▆▆▅▂█░▅▖`▀▄                          `     ▄░░      ███      █      █i▂▁▁▁▂▃▄▅▆▇░░L ▒██▒▒._`;                             i░░                   █ ███████████████░▒▌ ▓█▌▒▌-`▚▃                            ▓░░ ██ █████████████████ ███████████████░ ░▓█▒▒■▒▒:▓░░▄                         ░░░░░░░███████░░██P`.▓▓▓▓▓░░░░░░░░▓████▓░ ▒██░▒▌▒▒▗▒░░░░◣  ░                     ▒░░░░███░░░░█  █; .░▒▒P"``T▓▓░░░▒ ░▓██▓ .██▓.▒▎▒▒▝▘███░░▒  ░░                   ░`░░░░░███     F .▒▒F`  . ;█  ░░▓▒▖▀▀".◢███░░|░░░██████░▒▌ ░░░░                ;  ▒░▒░░██░░░░█F J▓▓F    L ▓▓█████░░░░░ ███▓░▕░.░███████░▒█ `░░░░░              `   ▒░░░░░░   ! ;▒▓l    ;;;$▓█████████████▓▌░;\.▓██████░░▒:   `░░░░░             `    ▒▒░░░░░░░ :▒░▓:    ▝ .▓███░░░█████████░░ ▄███████░░▒◤   `. `░░░                   ▒▓▓▓▓░░░ T▒▒▓l    ;;▓███░░░░░████░░█▒ ▄████████░░▓◤ ░░   ▚  `░                     ░▓▓▓▓▓b.T▓▓▓b.,i$███░█████░░████▓▓█████████░░▓!'        ▚                       ░ ░▓▓▓▓▓▓▓▒▒▓▓▓▓░░████       ░░░░░░    ░░░░▓▓`  ░░░      ▚  ▅▅▅▄▄▄▃▃▃▃▂▂▁▁         ░▓█▓▓▓▓▓▓▓▓▓▓▓▓▒▓░░░░░░░░░░▓▒░░░░░░░░░▒▓`  ░░░░░░░░░   ▌ ███████████████▇▇▆▅▄▂▁   ▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█▀                ▐ █████▓▓▓▓▓▓████████████▇▄▁  ▔▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀██▒▒▒▒████▀▀▀    ░░     ░░      ▝▖ ███████▓▓▓▓▓▓▓▓▓▓▓▓█████████▇▆▅▄▃ ▇▆▆▅▄▃▂▁ ▆▆▄▄▃▃▃▃▃▄▄▄▄▄▃▂▂▂▂▂▁▁▁▁   __ /`-.__▌ ██████████▓▓▓▓▓▓▓▓▓▓▓▓▓███████████▆▃▃▃▂▁███▇▆▅▄▂ ██░░░░░░░░██████▓▓▓▒▒░░▒    ▓▓▒ ████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████████▆▄▃▂▂▁  ▅▄▃▂▁██████████▓▒▓▒▒▓▒▒     ▓▓▒ indyZ███▓▓▓▓▓▓▓▓▓▓▓▓▓███████████▓▓▓▓████████████▇▆▅▃▁ ▝████▓▓▓▓▓▓▓▒▒▒▒       ▒▒░ durdraw-0.29.0/examples/indyz-linuxfire.dur000066400000000000000000000041541476305210600207540ustar00rootroot00000000000000gdlinuxfire3.dur]o6+-$9Vv]w9lI@b3t+'ےE"%R<9jӔD_'?hkef5%Y.cֻycUlv2fz=ǃe$gb^n{߯CNio3qvϦu,Lk:ڵc. .\x[ȫGK(.6j+;gUڏ\|L.1<TNf$.씩@בZPGV>yۚb?H$W]>ӺKDꥼh@Ϫң;cFNF|qJ֨S5fi .➯\ͼ_ e@'~:N~NW޺EMQ|7+ _wSi6>&2흥}~꣱$PP۫=Œsja-6$QZXWrYRoKgeVtpυJ81Hh8HeF8[smǒ(Qzo bڣTz c)BYhhgMeO ;pƙC(21,W~| Wຯ㋷LMJ?#o~Q<#1|N=E䆰BWici@3eľn/V#BY y}yt:E OxDo)u ?)4ߍT/QLfċ$XAn WWށmz65 SAy7LAM=[ֶxSyX4yr;Hr]{I)لt2M:lIK#Y$K'0`AxOݭξsrja3;Ϳ~\s3,v^ݮ͏n^V7սǛ?^x~W_^_}~Ts֛=77yכG٥_(>qIfM/)l^lVdV_q_bOl:}jeo4=m ihqG ;QؕEZl^[sS̳irЅR32%K[YC[^73)gY푹f8p}r\o\,i*A.ۭQAӲN.aɟ[%]n? \^eVة-3͹޽ ߢ%!},jtH|ϺZ0$o{Pm`Y2s*hT՞u[!d]L&&`+WB\|GqO5G'i<$v(SKG/6Q٥$[{Ѱ zЭJF7 onцSC ڗs6өۅi}&WkT39}վ{}M?}xOďhi|euJrEӸ=T4o*w K$ӌMS?M8Gb&T6I9(*JmkU!*])Ei^&rɏqϹZZ6/+BhV%o4eڒ&e%PvBUK.]q*@Y;L簗/Ӝm]Z"}z~t!P-zu۸Ou\Ԃ{r𗣥vhlop;b]Q#ˠ~3|vl*B/>@ , `?:Cx?:g~?|h F/ek<|8p.|8pQm8-4e 0s&@Nҁ'X_`!C‡O\—G j"hn4|;៦_@çZ"Xa‡ .] MgEWO!\h)AMfzFNxWp膣'фu3G>)J7;p{A&7${2ϝ.m >ߏQ? 1=1u'ߜS4 k_7˩C{M^~§,nBG>| @F>|@@Qm 6|TwL |# |e?x%X)gD7e*uuΚ|0`H_O > :(_ $(>PF80_G/5񇏖F(s[F3+( f Iݧ&G.PA? GA{KY/ߖ? _:#8|pr>ɵL{Y`>j1O]o!}Xx$d*,X hg,htޠ^(RbŸL>L&zGK#Yu2"I^kY齁ocoXa Z~?_7Z2,=l/[NOU>&%;qC@: }:;\Xa‡ >dXeGaڧdWf(HM8gv| ~,D"@iX'++>x @>|@hp2 >p|U> |@m,X?Ҋ-^7:>Gw:_ 0cDO,Wp^"(ty JaGaþ}Tk4j0uT{_*֒G߻{(Yx:9y9:+=5CA5 2^ Bˆo%cr"%_ ߷߰W`[1#&06&t-pQ~(3 >8|p1)8|Lc M Al!Z+P 0]H <{L {8> |@>r`?PI0 ~bH W9;< 1-={}L _?`ދH?*>> |r TJbHbO<> K,|Xa‡ >,0-| l_-OoEFWR{.ZW_fTurQ@ 9^>wf⿫n}{gHdurdraw-0.29.0/examples/indyz-xmas.dur000066400000000000000000000160001476305210600177100ustar00rootroot00000000000000 jeindyz-xmas.dur˒ὯKk% u;gJxHd5RvRG"# ")?}Bcϟw/$G~|է><㇧_~y}~g~z|xۏ{7g>݇Oo0^˟_=~z#o^/cџoOp_>=zzoc?Y|M{}/ g<cO!>|<΃ױ{>~?hq/=?lMm^x@qv׺QhxHxO ?+/qޭ꿨mro6җkVx|z !/MU-k֝X}N/plGoqt} 7: QN(7G 9Jv"wºpgVϴB~M_/uOx6ocqv 5z/FG:'*f<|@U]+9<5=e<7/ۺ]ۍftNl>wGwgZb2AE6&wSy?f.f([L', ^;~N;v>v?;*>dͻ_a5Z{R3/\??kf%_θiI,}4+ޜHᨨn\/̖U:06(.<mS뷞w[i[0WEv=U|3WaE>jqBɱUqc;h_1F(|ʾ4Rv۲`md["r~OQـ]'VU׽l#/6.vw>LְjqrX,rɣU2?$7tQo{q`9 ;ˬhAC~vÛ_>Z]W?>~տB~x~y{×洺={;K4ZSeC Q$puds#@2T?tD]N.gL_ZW =t#[ D׈^M60v!d{;uU.0{#"in/Zo /e3%"iKYuh]~Nm2LwH bdOm>j4@=~نltCm6-)9fnN myOmMÿtm;M;]u}zTq=^حKnq w=Uԥlދ}N>Ms]69`;Č!y8C\c7?Ȅv“]%&Z$-"pz'3[[:邧%UtRYֵ|qlzbxk-_H>ug]}1NdR׹s]>A뜪Psd~,ݍ-/}֕˖Ntl$ᬍvVxQQt_E_G4kXk8箸CqԵ]o3d3g sRyan5t1>OBJmֿ#0=r}B6UH$_GHE%吹#XEL ,Agv\ǡl<=;R)6[nv'ۭqO϶N"]}'Y0¡kj9j9j9j9j9j9j9j9j9j9j9j9j9j9j9j9j9jy{jP֋N'e3B=O+"!@h1Dtn҈kjj9j9j9j9j9j9j9j9j9j9j9j9j9j9j9j9j9j9j9j9j9j9j9j9GrporrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrW<I-zZݩ@Cs-------/ -VCǚҡWz_EF ;4ƒVF.I.}ϺkY.~Cښ\H+zF%hrX9Mز\b)S.5Ѣ򠁴| 4EozGlP.kyӠ\eU&FuSd|NmkW'9\v$% @Vly//dC.)=!.jyR\?9.wV:L9L9L9L9L9L9L9L9L9L9L9L9L9L9L9L9L9L9L9L9L9L9L9L9Gr4orrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrk`W)))%WNydXS<^;^92'k; [w iqM0bsNy#ZPTaVjx{$vʓ-8 JAvS Wk>Tks;b]a2YJuѥrYIJzmHYURy )/\_zY\wVyK2E[akVfu@9\(U%mXwʥ]b%|'\W6l A̱AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA傘Gnb9y<RM6؊T3P%b#1ϠGg?wע)[Ạ1Uj{;*nl7GCkD, N kVxZwhal|6[&*nI;5`#ᮀ'H43pZ4:_O|RnO2a.{,\ΰ[^Dl_ƚrC]le@|------------------o- zqvy&\iE$d\; ].͒------------------------[ [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[~lyzP8<-^Ėa +c#%停DUly>_[~ZިZ0]-wjyXZn vv`4!* oIBZe.Ҥ Z;\'ro@\=ә{D7>&#Ҡi#\zډ #,pi0|fbIz}&$v˃4Hrt| 4EozGlP.kyMO'+jYUFro'[\˧.c/ܑ/TQ_\#9,lxgC{ya'$rI tQ˓ja7uaaaaaaaaaaaaaaaaaaaaaaaa9”yÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔo)\O.)&Ôlzo!{<?<|B|><P <7_}ͻ.|>4}ӿ?!۾yxou_g>J緝~ ׈>/w?7O~xZ'MBtô.q鵹Ê"޷iyi?}i?=\&` ]fOǢݹ@|˽lO ٿ-;>n} .;u-|ꗾYSuߧ9+ 6ep5؁ oA68ҭry"mo 0T&>Q" awWdE*efB'Ui[b@+_@`-Atj[RaR !0ZY$0Ԍu8':DE#0JūGVcf,~({"~Bkڰ+`X&p9u*,\eTIOαhOhoZXƣ\-^nm^I5i?yxI k.k!^JV7'ܣ0~XurObE"?h2覽oiz`aL#,Nc^~G{>Ϟ*+FB{[ɯlK="4a¹/7G#ٽz1tOy'=V/j7a:4Ͷ80wܝe ^BgII|M6Zg.a`q:ӵ}V|ynS_JRgs#Q퓤Oi4qmX%D%)pې:S=?#oKs"Z?SCkoUE֭(y#tR7'Mz4ѤGMz4ѤGMz4ѤG%=f#FEVz(eX5uy]ۂvV*?N~^m䱛N$@ 熜4icV|#4*m:}*ɵRrAI$tX? X X- ;fxjB+bxVm(Y`KyI1"  !j"nՓ*?Ñ{J#a$`$N$N3j~EL7#ioDI %zauQ@:w:MjQP%x|>ݟ" *KĻDit[,$ڡQ$&b  3:P*c`)8HRg𧴅V;=k'DT*\[ ;/NЖ3݌q~V?X<̶4P [<,0aɍ #ֺx'\"e㟃Px֮>c;8'")J3=4V[d݊Z=B]!uی|/XFce4XFce4XFce4qQ,k 3bjYF Xe ^\Sص-@ kgkc9PZƹ&@k5 f[}]Ѐ4YbI*.nӢ;@F^3N`0\(9byĪ!V%γOH*ƒDACky4ک"v2Q BkDRTÄl u8pLZQ%%D!zюT21 ʡ R!X|G/0R_*TuHzA~I9{ L,0 `Cu-2< e#oI%Eh*X?T|" C/S*5||#Cs)7 i@U)sH?;-0} ra4jrIW/63(Jp|%6B 3bRQ- ie,OϻdA=kaQqszp^?fD,#NT]YU"Ɲ\S8;WƢdQs4JŢh8G+s4",s<9ƵKΜY`f3l)vF v؅It m%:komd- ~=CvE<ܘvhڡivhڡivhp-iu:\tH P‡k {Y3WL`kvmȣ?Dĕ2$m LEUR!b 1TTF%U-7 y"9#tAGbSfbԩ[̄8L T!B)ꠀ6C S,"+v!ڷQȚꔕb, b*0uv`s52_XC9`%FBssH:Db5bx6Z%;" 0=UźP2qqAfDٙtclJsW2!_;sDEä `z.c-Or9lgV(4p(Ϲ7L:jצ i6 0Q2  .;c uFFedEC,'3TN3lMƐAa. n=EBVnj+wZ41^^Mͬ8W_yx[8:9_hl6w*%+΁WoGYK|o>JlS>wUG/ju֧z~lGJޖDD1Ef֪!p[QG+ngohĢF,hĢF,hĢF,.Xt:E GX-HJ˰ck(lm8Gk[+87ؤȋG#7̭Fh tD?{$W+-iߑJ7K`0\4SOTO$E lln(pf Ë)b)"c~DXBh)͸Ta x88|$LD?-lܞ,O@Kgj4ȅGv߈y5aL> r2/+HFVJgSXߑMkG\< 򽮢-qI:]( DrvEfp-/t p`gxs)tpZ8 MQxi9?~ `'{)?Ȑ᳌)laU.(J&!2bs8M]ZQA?3,L1E!M ^H/&?+]0p[v1 c1M$h͂3OSW>T"!1=y ސuE*V'ݦnaF ;se:-s`{>WpЁ؁)%[d/ 0@ 8n.ۭ&eYG}i.2PrsH+sMR6M&A&,ZTL=;3sKJ,M')gd4%\@ІSڅy,ȽO7gQ 8(hbQ (h!Z"*)u/S{e=?+AK;9!*$cPw^31Eh;+'oIꝋ+#n"TVTJ@[1mW/ Ww^ <˅+@ \j88TD"ǽÜ @mJiQƭEc:Wj;9$#9W%@?ΓV[L'/~ [R8݆H|s#WǔFq͗Ƙ,X_(/ZhΟh/v*Y!l7nfő =/ċgѡ9$Cr} .q`G_^X7%z;Z{KTbKBz>x֮>c;8'")J3=4V[d݊=B]!u|'8Fc48Fc48Fc4qQk 2bj9F Xc ^VSص-h@ g2),W`+Qi5P5X h2#!|An,HLˌA&҂$v@JS7529>TQUR>4jkǕa Hč"'qH<(2:s*Cr2E2"Itw!Rv&×"|؂]&(RO.$%fF"eqDH@vfnTBHLD>}{jW2Wm؉}h^?f"6syNUGY6+u:|ّRhPufi !o\q}_tb x:Cz'?hJ:;m"W]rPݬ`(^G{Yo69h,sa\o?oQr1g'&wzeret䠧v^号ɉVy滽 .sq4wΛOѡ.}DyΦ\r4E|9èIk]E@vem8=g <[3eK-7cqf;]xhfhSk۷Zܒ2%s,Oυ 7U3ꚝpFs{zN9yadb^'F5rYtV@44.&(u{A'9qREL#HڛrhIq A"DPi 79L6h# uAA[C`ۊ-b%xۍt/QzcGDZ8UaG(X7d紐435Dj*ARښ)u5RalM9X{ =VR5:B @/ˡT(̗A@B;55, 4s0 ͳwy纉Ab 3 .'u&)>\ xud?p"oXU &׀D0! B҈Fx1@4"!|! NWb6Z+4hFh.~ ho'34R!@\>ydF?Sf˷)5ҕh÷$$vNǬ)vEktܞv[E1|ͺ*?_$N.Ixus{TK*pb$L:٢e_ח(ʹ.[v!l ۦ\];ﭴ>;0UIdl/?.ATk4m!HVd,Ek.tCr m|xt i-muy{m2@zW<޽7#Ydurdraw-0.29.0/examples/mane.dur000066400000000000000000000015301476305210600165270ustar00rootroot00000000000000cmane.durKO@=bM$dU[VYAU BE-ΕT/Y`oOl1iv:zA<꾎\d-P?h0zA ?FH <>K,Kza~MR&w~OIқ(Qr"[|,[[_"Y >;J딵#4_=rvvwE-ZhѢE-ZhѢE-ZhѢE-ZhѢE-ZhѢE-ZhѢEmXgS\kU]۪nv٪-F:MaY٪RTcKݪ>?u1+{ fuC:?+ѢE-ZhѢE-ZhѢE-ZhѢE-ZhѢE-ZhѢE-ZhѢEfjisbI~durdraw-0.29.0/examples/rain.dur000066400000000000000000000014721476305210600165450ustar00rootroot00000000000000L`drain.dur_OP{>EĻr'[^ #fni2:mF%|w۱) gaY;ݳ^7Jݬ:+mrYד:M?դ0ǣqu<_trQü񸜯 `(6 phPΛMjZLaU_ReGaw1?zfh5gdkWkg_`/[ QG=]<,>4sc37y7$ތZw$tv,b <].\p“&ބ.l>NOd,{g---------------------------------------------------ծYWˣoVzqQxl~_=n Ychޱ)\pmwiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiZA+{ ^MYCoBRp… JۥJߤVz=45", "wheel"] requires = ["setuptools>=61.2", "wheel"] build-backend = "setuptools.build_meta" [project] name = "durdraw" description = "Versatile ASCII and ANSI Art terminal text editor" readme = "README.md" #license = "BSD-3-Clause" #license-files = [ # "LICENSE", #] version = "0.29.0" classifiers = [ 'Environment :: Console :: Curses', 'Topic :: Artistic Software', 'Topic :: Text Editors', 'Topic :: Terminals', 'License :: OSI Approved :: ISC License (ISCL)', 'Intended Audience :: End Users/Desktop', 'Natural Language :: English', 'Operating System :: POSIX', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Topic :: Utilities' ] authors = [ {name = "Sam Foster", email = "samfoster@gmail.com"}, {name = "Durdraw Contributors"}, ] requires-python = ">=3.9" dependencies = [ "windows-curses; platform_system == 'Windows'", ] [project.urls] Homepage = "https://durdraw.org" Documentation = "https://github.com/cmang/durdraw/blob/master/README.md" Source = "https://github.com/cmang/durdraw" #Changelog = "https://github.com/cmang/durdraw/releases" [project.optional-dependencies] gif-export=["Pillow>=9.0.0"] [project.scripts] durdraw = "durdraw.main:main" durfetch = "durdraw.durfetch:main" durview = "durdraw.durview:main" [tool.setuptools] packages = ["durdraw"] [tool.setuptools.package-data] durdraw = [ "help/*", "charsets/*", "themes/*", "plugins/*", "neofetch/*", "durf/*", ] durdraw-0.29.0/start-durdraw000077500000000000000000000005101476305210600160030ustar00rootroot00000000000000#!/usr/bin/env python3 # Launches Durdraw from durdraw import main from durdraw.durdraw_options import Options from durdraw.durdraw_movie import * if __name__ == "__main__": #try: main.main() #except Exception as e: # print(f"Exception: {e}") # print("Caught exception. Exiting.") # exit(1) durdraw-0.29.0/test/000077500000000000000000000000001476305210600142355ustar00rootroot00000000000000durdraw-0.29.0/test/__init__.py000066400000000000000000000000001476305210600163340ustar00rootroot00000000000000durdraw-0.29.0/test/durdraw/000077500000000000000000000000001476305210600157055ustar00rootroot00000000000000durdraw-0.29.0/test/durdraw/__init__.py000066400000000000000000000000001476305210600200040ustar00rootroot00000000000000durdraw-0.29.0/test/durdraw/test_log.py000066400000000000000000000143651476305210600201100ustar00rootroot00000000000000import durdraw.log as log import time from datetime import datetime, timedelta import io import json import logging import os import time def init_test_logger(name='test_log', **kwargs): fake_stream = io.StringIO() logger = log._getLogger( name, level='INFO', handlers=[logging.StreamHandler(fake_stream)], **kwargs ) return logger, fake_stream class TestLog: def test_log_complete_format(self): logger, fake_stream = init_test_logger() before = time.time() logger.info('Hello, world!') after = time.time() log_record = json.loads(fake_stream.getvalue()) result = datetime.strptime( log_record['timestamp'], '%Y-%m-%dT%H:%M:%S.%f%z', ) del log_record['timestamp'] expected = {'msg': 'Hello, world!', 'level': 'INFO', 'name': 'durdraw.test_log', 'data': {}} assert log_record == expected assert before <= result.timestamp() <= after def test_log_timestamp_timezone_default_utc(self): logger, fake_stream = init_test_logger() logger.info('Hello, world!') log_record = json.loads(fake_stream.getvalue())['timestamp'] result = datetime.strptime( log_record, '%Y-%m-%dT%H:%M:%S.%f%z', ) assert result.utcoffset() == timedelta(0) def test_log_timestamp_timezone_local(self): # need to check that the code is actually producing local timezone # set the timezone temporarily in case someone is running the tests while in UTC os.environ['TZ'] = 'America/Boise' time.tzset() logger, fake_stream = init_test_logger(local_tz=True) logger.info('Hello, world!') log_record = json.loads(fake_stream.getvalue())['timestamp'] result = datetime.strptime( log_record, '%Y-%m-%dT%H:%M:%S.%f%z', ) assert result.utcoffset() is not None # this should be timedelta(days=-1, seconds=64800) during non-DST assert result.utcoffset() != timedelta(0) def test_log_no_args(self): logger, fake_stream = init_test_logger() logger.info('Hello, world!') result = json.loads(fake_stream.getvalue()) del result['timestamp'] assert result == {'msg': 'Hello, world!', 'level': 'INFO', 'name': 'durdraw.test_log', 'data': {}} def test_log_kwargs(self): logger, fake_stream = init_test_logger() logger.info('Hello, world!', {'key1': 'value1', 'key2': 'value2'}) result = json.loads(fake_stream.getvalue()) del result['timestamp'] assert result == {'msg': 'Hello, world!', 'level': 'INFO', 'name': 'durdraw.test_log', 'data': {'key1': 'value1', 'key2': 'value2'}} def test_log_to_file(self): if os.path.exists('test_log.log'): os.remove('test_log.log') logger = log.getLogger('test_log', level='INFO', filepath='test_log.log', override=True) logger.info('Hello, world!') assert os.path.exists('test_log.log') with open('test_log.log', 'r') as file: result = json.loads(file.read()) del result['timestamp'] os.remove('test_log.log') assert result == { 'msg': 'Hello, world!', 'level': 'INFO', 'name': 'durdraw.test_log', 'data': {}, } def test_log_no_emit(self): if os.path.exists('test_log.log'): os.remove('test_log.log') logger = log.getLogger('test_log', level='CRITICAL', filepath='test_log.log', override=True) logger.info('Hello, world!') # the file should not exist, as the level is set to CRITICAL assert not os.path.exists('test_log.log') # the file should be created, as the level is set to CRITICAL logger.critical('Hello, world!') assert os.path.exists('test_log.log') with open('test_log.log', 'r') as file: result = json.loads(file.read()) del result['timestamp'] os.remove('test_log.log') assert result == { 'msg': 'Hello, world!', 'level': 'CRITICAL', 'name': 'durdraw.test_log', 'data': {}, } def test_multiple_loggers(self): '''Test that child loggers with different names don't produce duplicate logs''' if os.path.exists('test_log.log'): os.remove('test_log.log') logger1 = log.getLogger('test_log', level='INFO', filepath='test_log.log', override=True) logger2 = log.getLogger('test_log2', level='INFO', filepath='test_log.log', override=False) logger1.info('Hello, world!') logger2.info('Hello, world!') with open('test_log.log', 'r') as file: results = list(map(json.loads, file)) for result in results: del result['timestamp'] os.remove('test_log.log') assert results == [ { 'msg': 'Hello, world!', 'level': 'INFO', 'name': 'durdraw.test_log', 'data': {}, }, { 'msg': 'Hello, world!', 'level': 'INFO', 'name': 'durdraw.test_log2', 'data': {}, }, ] def test_reuse_loggers(self): '''Test that child loggers with the same name don't produce duplicate logs''' if os.path.exists('test_log.log'): os.remove('test_log.log') logger1 = log.getLogger('test_log', level='INFO', filepath='test_log.log', override=True) logger2 = log.getLogger('test_log', level='INFO', filepath='test_log.log', override=False) logger1.info('Hello, world!') logger2.info('Hello, world!') with open('test_log.log', 'r') as file: results = list(map(json.loads, file)) for result in results: del result['timestamp'] os.remove('test_log.log') assert results == [ { 'msg': 'Hello, world!', 'level': 'INFO', 'name': 'durdraw.test_log', 'data': {}, }, { 'msg': 'Hello, world!', 'level': 'INFO', 'name': 'durdraw.test_log', 'data': {}, }, ] durdraw-0.29.0/themes/000077500000000000000000000000001476305210600145435ustar00rootroot00000000000000durdraw-0.29.0/themes/default.dtheme.ini000066400000000000000000000015261476305210600201410ustar00rootroot00000000000000; Default Theme ; ; name = theme name ; mainColor = what most stuff is drawn as ; clickColor = clickable items (buttons) ; clickHighlightColor = the color the button changes to for a moment when clicked ; borderColor = the color of the border around a drawing ; notificationColor = the color for notification messages ; promptColor = the color for user prompt messages ; ; color codes numbers can be found in Durdraw's 256-color selector. [Theme-256] name: 'Default 256' mainColor: 7 clickColor: 2 clickHighlightColor: 10 borderColor: 7 notificationColor: 3 promptColor: 3 menuItemColor: 7 menuTitleColor: 98 menuBorderColor: 7 [Theme-16] name: 'Default 16' mainColor': 8 clickColor': 3 clickHighlightColor': 11 borderColor': 8 notificationColor': 8, # grey promptColor': 8, # grey menuItemColor': 2, menuTitleColor': 3, menuBorderColor': 4, durdraw-0.29.0/themes/mutedchill-16.dtheme.ini000066400000000000000000000012101476305210600210610ustar00rootroot00000000000000; colors for 16-color mode: ; 1 black ; 2 blue ; 3 green ; 4 cyan ; 5 red ; 6 magenta ; 7 yellow ; 8 white ; ; name = theme name ; mainColor = what most stuff is drawn as ; clickColor = clickable items (buttons) ; clickHighlightColor = the color the button changes to for a moment when clicked ; borderColor = the color of the border around a drawing ; notificationColor = the color for notification messages ; promptColor = the color for user prompt messages [Theme-16] name: 'Muted Chill' mainColor: 8 clickColor: 3 borderColor: 6 clickHighlightColor: 5 notificationColor: 4 promptColor: 4 menuItemColor: 8 menuTitleColor: 2 menuBorderColor: 4 durdraw-0.29.0/themes/mutedform-256.dtheme.ini000066400000000000000000000013721476305210600210300ustar00rootroot00000000000000; name: theme name ; mainColor: the color of most text ; clickColor: the color of buttons (clickable items) ; clickHighlightColor: the color the button changes to for a moment when clicked ; borderColor: the color of the border around a drawing ; notificationColor: the color of notification messages ; promptColor: the color of user prompt messages ; menuItemColor: the color of menu items ; menuTitleColor: the color of menu titles ; menuBorderColor: the color of the border around menus ; ; color codes numbers can be found in Durdraw's 256-color selector. [Theme-256] name: 'Muted Form' mainColor: 104 clickColor: 37 borderColor: 236 clickHighlightColor: 15 notificationColor: 87 promptColor: 189 menuItemColor: 189 menuTitleColor: 159 menuBorderColor: 24 durdraw-0.29.0/themes/poopy-256.dtheme.ini000066400000000000000000000010571476305210600201740ustar00rootroot00000000000000; name = theme name ; mainColor = what most stuff is drawn as ; clickColor = clickable items (buttons) ; clickHighlightColor = the color the button changes to for a moment when clicked ; borderColor = the color of the border around a drawing ; notificationColor = the color for notification messages ; promptColor = the color for user prompt messages ; ; color codes numbers can be found in Durdraw's 256-color selector. [Theme-256] name: 'Poopy' mainColor: 130 clickColor: 95 borderColor: 58 clickHighlightColor: 15 notificationColor: 100 promptColor: 94 durdraw-0.29.0/themes/purpledrank-16.dtheme.ini000066400000000000000000000011241476305210600212620ustar00rootroot00000000000000; colors for 16-color mode: ; 1 black ; 2 blue ; 3 green ; 4 cyan ; 5 red ; 6 magenta ; 7 yellow ; 8 white ; ; name = theme name ; mainColor = what most stuff is drawn as ; clickColor = clickable items (buttons) ; clickHighlightColor = the color the button changes to for a moment when clicked ; borderColor = the color of the border around a drawing ; notificationColor = the color for notification messages ; promptColor = the color for user prompt messages [Theme-16] name: 'Purple Drank' mainColor: 6 clickColor: 3 borderColor: 6 clickHighlightColor: 5 notificationColor: 4 promptColor: 4