pax_global_header00006660000000000000000000000064147047612250014522gustar00rootroot0000000000000052 comment=0e30fc6cad39ebd429b8b30e43b85da1f01da7aa durdraw-0.28.0/000077500000000000000000000000001470476122500132615ustar00rootroot00000000000000durdraw-0.28.0/.dockerignore000066400000000000000000000000051470476122500157300ustar00rootroot00000000000000.git durdraw-0.28.0/.github/000077500000000000000000000000001470476122500146215ustar00rootroot00000000000000durdraw-0.28.0/.github/FUNDING.yml000066400000000000000000000014551470476122500164430ustar00rootroot00000000000000# 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.28.0/CONTRIBUTING.md000066400000000000000000000025301470476122500155120ustar00rootroot00000000000000## 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. * 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). * 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.28.0/Dockerfile000066400000000000000000000002611470476122500152520ustar00rootroot00000000000000# 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.28.0/LICENSE000066400000000000000000000027371470476122500142770ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2009-2024, 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.28.0/README.md000066400000000000000000000527471470476122500145570ustar00rootroot00000000000000Durdraw ======= __ __ _| |__ __ _____ __| |_____ _____ __ __ __ / _ | | | __| _ | __| _ | | | |\ /_____|_____|__|__|_____|__|___\____|________| | \_____________________________________________\| v 0.28.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. ## REQUIREMENTS * Python 3 (3.10+ recommended) * Linux, macOS, or other Unix-like System ## INSTALLATION You can install from your OS repositories, or follow the instructions below to install from source: [![Packaging status](https://repology.org/badge/vertical-allrepos/durdraw.svg)](https://repology.org/project/durdraw/versions) If you just want to run it without instalilng, scroll down to the next section. 1: Download and extract, or use git to download: ``` git clone https://github.com/cmang/durdraw.git cd durdraw ``` 2: Install or upgrade using pip: ``` pip install --upgrade . ``` Or run the installer: ``` python3 setup.py install ``` 3: Optionally, install some themes and a sample configuration file for your local user into ~/.durdraw/: ``` ./installconf.sh ``` You should now be able to run `durdraw`. Press `esc-h` for help, or try `durdraw --help` for command-line options. ## RUNNING WITHOUT INSTALLING You can run Durdraw with: ``` ./start-durdraw ``` To look at some included example animations: ``` ./start-durdraw -p examples/*.dur ``` ## OPTIONAL INSTALLATION 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 work in 16-color mode for now, and only with CP437 compatible charcters. You also need the PIL python module. For Durfetch support, please install Neofetch and place it in your path. ## GALLERY [![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) ![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) ![eye](https://user-images.githubusercontent.com/261501/210064343-6e68f88d-9e3e-415a-9792-a38684231ba0.gif) ![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) ## COMMAND LINE USAGE You can play a .dur file or series of .dur (or .ANS or .ASC) files with: ``` $ durdraw -p filename.dur $ durdraw -p file1.dur file2.dur file3.dur ... ``` Or view a downloaded ANSI artpack with: ``` $ 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 keyboar 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 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 and some themes into ~/.durdraw/ with the command: ``` ./installconf.sh ``` This will place durdraw.ini into ~/.durdraw/ and the themes into ~/.durdraw/themes/. Here is an example durdraw.ini file, showing the available options:
; 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 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/mutedchill-16.dtheme.ini
theme-256: ~/.durdraw/themes/mutedform-256.dtheme.ini
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. Here is an example 16-color theme:
[Theme-16]
name: 'Purple Drank'
mainColor: 6
clickColor: 3
borderColor: 6
clickHighlightColor: 5
notificationColor: 4
promptColor: 4
and a 256-color theme:
[Theme-256]
name: 'Muted Form'
mainColor: 104
clickColor: 37
borderColor: 236
clickHighlightColor: 15
notificationColor: 87
promptColor: 189
menuItemColor: 189
menuTitleColor: 159
menuBorderColor: 24
The colors and theme options are as follows: colors for 16-color mode: 1 black 2 blue 3 green 4 cyan 5 red 6 magenta 7 yellow 8 white color codes numbers for 256-color mode can be found in Durdraw's 256-color selector. ``` 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 ``` ## 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: ``` {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 paramaters: ``` usage: durfetch [-h] [-r | -l LOAD] [--linux | --bsd] [filename ...] An animated fetcher. A front-end for Durdraw and Neofetch integration. positional arguments: filename .durf 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) ## 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) ## LINKS, MEDIA AND 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 Your support means a lot to Durdraw! As a free and open-source project, your donations fuel my motivation to keep improving this software. Thank you for considering a contribution to help sustain and enhance this project. Contributions help cover essential costs like development time, domain registration, and web hosting. You can contribute 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 Developer: Sam Foster . For a full list of contributors, see the github page below. Home page: http://durdraw.org Development: https://github.com/cmang/durdraw ANSI and ASCII artists: cmang, H7, LDA ## LEGAL Durdraw is Copyright (c) 2009-2024 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.28.0/durdraw.1000066400000000000000000000057071470476122500150240ustar00rootroot00000000000000.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. .\" I edited it. Bad boys, bad boys. -Sam .\" __ __ .\" _| |__ __ _____ __| |_____ _____ __ __ __ .\" / _ | | | __| _ | __| _ | | | |\ .\" /_____|_____|__|__|_____|__|___\____|________| | .\" \_____________________________________________\| .TH DURDRAW "1" "March 2024" "durdraw 0.26.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 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.28.0/durdraw.ini000066400000000000000000000015641470476122500154400ustar00rootroot00000000000000; 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.. 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 ;[Charset] ;charset-file-utf8: durdraw/charsets/unicode-legacy-computing.json ;charset-file-cp437: durdraw/charsets/cp437-ibmpc.json durdraw-0.28.0/durdraw/000077500000000000000000000000001470476122500147315ustar00rootroot00000000000000durdraw-0.28.0/durdraw/__init__.py000066400000000000000000000000001470476122500170300ustar00rootroot00000000000000durdraw-0.28.0/durdraw/charsets/000077500000000000000000000000001470476122500165455ustar00rootroot00000000000000durdraw-0.28.0/durdraw/charsets/unicode-groups.xml000066400000000000000000000553021470476122500222370ustar00rootroot00000000000000 durdraw-0.28.0/durdraw/durdraw_ansiparse.py000066400000000000000000000501171470476122500210240ustar00rootroot00000000000000#!/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 import re import pdb import durdraw.durdraw_movie as durmovie import durdraw.durdraw_color_curses as dur_ansilib import durdraw.durdraw_sauce as dursauce 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 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 there's an escape code, extract data from it if text[i:i + 2] == '\x1B[': # Match ^[ match = re.search('[a-zA-Z]', text[i:]) # match any control code end_index = match.start() + i # 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, maxWidth=80): """ Take an ANSI file blob, load it into a DUR frame object, return frame """ 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 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) 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) 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 there's an escape code, extract data from it if text[i:i + 2] == '\x1B[': # Match ^[[ match = re.search('[a-zA-Z]', text[i:]) # match any control code end_index = match.start() + i # 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: new_frame.content[line_num][col_num] = character 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: 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.28.0/durdraw/durdraw_appstate.py000066400000000000000000000404231470476122500206570ustar00rootroot00000000000000import 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 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() # 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 self.can_inject = False # Allow injecting color codes to override ncurses colors (for BG 256 colors) 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.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 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 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 available_themes = [] # populate with a list of dicts containing name=, path=, type= for filename in self.internal_theme_file_list: pass 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.28.0/durdraw/durdraw_charsets.py000066400000000000000000000103131470476122500206450ustar00rootroot00000000000000# Load character sets from files, initialize them import pdb 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 if __name__ == "__main__": #charMap = load_unicode_block("Chess Symbols") #charMap = load_unicode_block("Symbols for Legacy Computing") #charMap = load_unicode_block("Emoticons") #print(str(charMap)) unicodeBlocksList = get_unicode_blocks_list() print(str(unicodeBlocksList)) durdraw-0.28.0/durdraw/durdraw_color_curses.py000066400000000000000000001301021470476122500215320ustar00rootroot00000000000000import curses import time import pdb 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" } def getColorCode24k(r, g, b): """ r, g and b must be numbers between 0-255 """ code = '\033[38;2;' code += str(r) + ';' + str(g) + ';' + str(b) code += 'm' return code 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] bg = 1 # build the code code = '\x03' #code = code + str(fg) code = code + f'{fg:02d}' code = code + ',' #code = code + str(bg) code = code + f'{bg:02d}' # code = code + '\x03' return code 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] code = '\033[38;5;' # begin escape sequence code = code + str(fg) code = code + ';' code = code + '48;5;' code = code + str(bg) #code = code + str('0') code = code + 'm' return code def getColorCode(self, fg, bg): """ returns a string containing ANSI escape code for given fg/bg """ escape = '\033[' # begin escape sequence escape = escape + self.escapeFgMap[fg] + ';' # set fg color escape = escape + self.escapeBgMap[bg] # set bg color escape = escape + "m" # m = set graphics mode command return escape 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.28.0/durdraw/durdraw_file.py000066400000000000000000000321501470476122500177530ustar00rootroot00000000000000# 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.28.0/durdraw/durdraw_gui_manager.py000066400000000000000000000013721470476122500213140ustar00rootroot00000000000000# 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.28.0/durdraw/durdraw_gui_manager_curses.py000066400000000000000000000013431470476122500226760ustar00rootroot00000000000000# 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.28.0/durdraw/durdraw_help.py000066400000000000000000000002461470476122500177650ustar00rootroot00000000000000from importlib.resources import files def get_resource_path(module: str, name: str) -> str: """Load a resource file.""" return files(module).joinpath(name) durdraw-0.28.0/durdraw/durdraw_movie.py000066400000000000000000000427151470476122500201630ustar00rootroot00000000000000from copy import deepcopy from durdraw.durdraw_options import Options import json import pdb 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) 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] 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: if len(search_str) < len(replace_str): line_str = line_str.replace(search_str.ljust(len(replace_str)), replace_str) else: line_str = line_str.replace(search_str, replace_str.ljust(len(search_str))) #caller.notify(f"found {search_str} in line:") #caller.notify(f"{line_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.28.0/durdraw/durdraw_options.py000066400000000000000000000012761470476122500205340ustar00rootroot00000000000000import 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.28.0/durdraw/durdraw_sauce.py000066400000000000000000000057331470476122500201430ustar00rootroot00000000000000#!/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 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.28.0/durdraw/durdraw_ui_curses.py000066400000000000000000011745231470476122500210510ustar00rootroot00000000000000# 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 time #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 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 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.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') # 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 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 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 enableTrueCGAColors(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.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: self.appState.wrapWidth = 120 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): # 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 colorMode = self.appState.colorMode infoString = '' infoStringList = [] #self.stdscr.nodelay(0) # wait for input when calling getch if fileName: infoStringList.append(f"File: {fileName}") if self.appState.fileShortPath: # extract folder name from full path folderName = self.appState.fileShortPath infoStringList.append(f"Folder: {folderName}") if title: infoStringList.append(f"Title: {title}") if author: infoStringList.append(f"Artist: {author}") if group: infoStringList.append(f"Group: {group}") if date: infoStringList.append(f"Date: {year}/{month}/{day}") 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.append(f"Playing: {self.playing} ") if len(infoStringList) > 0: infoString = ', '.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: # sleep_time = (1000.0 / self.appState.helpMovOpts.framerate) / 1000.0 #elif page == 2: # sleep_time = (1000.0 / self.appState.helpMovOpts_2.framerate) / 1000.0 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 = 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: sleep_time = (1000.0 / self.appState.helpMovOpts.framerate) / 1000.0 elif page == 2: 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 = 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 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.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 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 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 playing 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: 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() sleep_time = (1000.0 / self.opts.framerate) / 1000.0 elif c in [45]: # esc-- (alt minus) - fps down self.decreaseFPS() 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 == 47: # alt-/ - insert line self.addLine(frange=self.appState.playbackRange) elif c == 39: # 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 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.playOnlyMode: # UI for Play-only mode 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 == curses.KEY_LEFT: # left - scroll left self.scroll_viewer_left() elif c == curses.KEY_RIGHT: # right - scroll right self.scroll_viewer_right() if c in [61, 43]: # esc-= and esc-+ - fps up self.increaseFPS() sleep_time = (1000.0 / self.opts.framerate) / 1000.0 elif c in [45]: # esc-- (alt minus) - fps down self.decreaseFPS() 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 [10, 13, curses.KEY_ENTER, 27]: # 27 = esc self.playing = False self.appState.topLine = 0 elif c in [ord('?'), ord('h')]: self.showViewerHelp() elif c in [ord('i'), ord('I')]: # toggle showing info. True/false swap: self.appState.viewModeShowInfo = not self.appState.viewModeShowInfo self.stdscr.clear() 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 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() sleep_time = (1000.0 / self.opts.framerate) / 1000.0 elif mouseX == 16 + offset: # clicked FPS up self.clickHighlight(16 + offset, ">") self.increaseFPS() 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) new_time = time.time() frame_delay = self.mov.currentFrame.delay if frame_delay > 0: realDelayTime = frame_delay else: realDelayTime = sleep_time # sleep_time == (1000.0 / self.opts.framerate) / 1000.0 if new_time >= (last_time + realDelayTime): # Time to update the frame? If so... 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 #self.refresh() else: time.sleep(0.005) # to keep from sucking up cpu 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 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}" 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'] ) #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.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 intense_burning = ['idkfa'] sumbitch = [] sumbitch_len = 25 curses.noecho() while 1: # 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 #load_filename = self.openFilePicker() #if load_filename: # if not False # self.clearCanvas(prompting=False) # self.loadFromFile(load_filename, 'dur') # self.move_cursor_topleft() elif c == 115: # alt-s - save self.save() c = None elif c == 113: # alt-q - quit self.safeQuit() c = None 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 == 47: # alt-/ - insert line self.addLine() elif c == 39: # 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 load_filename = self.openFilePicker() if load_filename: # if not False self.clearCanvas(prompting=False) self.loadFromFile(load_filename, 'dur') self.move_cursor_topleft() 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() 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() self.statusBar.colorPicker.showFgPicker(message=message) #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.showFgPicker() self.appState.colorPickerSelected = False 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) self.selectColorPicker() self.clearStatusLine() 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 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() 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 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 None time.sleep(0.01) self.clearStatusLine() load_filename = self.openFilePicker() if load_filename: # if not False self.clearCanvas(prompting=False) self.loadFromFile(load_filename, 'dur') self.move_cursor_topleft() self.stdscr.clear() self.hardRefresh() 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 = ["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() while prompting: # draw list of files from top of the window to bottomk 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 self.addstr(realmaxY - 1, 0, f"Character set: {block_list[selected_item_number]}") 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 self.setCharacterSet(block_list[selected_item_number]) #self.appState.characterSet = block_list[selected_item_number] self.charMapNumber = 0 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) 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: # clicked set, not block # holy fuck this is deep self.appState.characterSet = block_list[selected_item_number] self.charMapNumber = 0 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 def openFilePicker(self): """ Draw UI for selecting a file to load, return the filename """ # get file list folders = ["../"] default_masks = ['*.dur', '*.durf', '*.asc', '*.ans', '*.txt', '*.diz', '*.nfo', '*.ice', '*.ansi'] masks = default_masks 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() #folders += sorted(glob.glob(f"{current_directory}/*/")) #folders += sorted(glob.glob("*/", root_dir=current_directory)) # python 3.10+ # python 3.9 compatible block instead: 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 = [] 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) # 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 # draw ui selected_item_number = 0 current_line_number = 0 search_string = '' mask_all = False tabbed = False show_modtime = True # show a column for file modified times 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() while prompting: # 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 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 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 selected_item_number == current_line_number and not tabbed: # 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: 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: self.addstr(current_line_number - top_line, 0, file_list[current_line_number], curses.color_pair(self.appState.theme['menuTitleColor'])) else: if show_modtime: 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 mask_all: if tabbed: self.addstr(realmaxY - 4, 0, f"[X]", curses.A_REVERSE) else: self.addstr(realmaxY - 4, 0, f"[X]", curses.color_pair(self.appState.theme['clickColor'])) else: if tabbed: self.addstr(realmaxY - 4, 0, f"[ ]", curses.A_REVERSE) 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'])) if selected_item_number > len(file_list) - 1: selected_item_number = 0 filename = file_list[selected_item_number] full_path = f"{current_directory}/{file_list[selected_item_number]}" if filename not in folders: # 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 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}") #self.stdscr.refresh() # Read keyboard input c = self.stdscr.getch() if tabbed: # Change focus from files to other elements, ie: Show All Files checkbox #c = self.stdscr.getch() if c in [9, curses.KEY_UP, curses.KEY_DOWN]: # 9 = Tab # Change focus away, back to file lister tabbed = False 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 = [] 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 selected_item_number = 0 search_string = "" full_file_list = file_list elif c in [27]: # esc tabbed = False prompting = False c = None self.stdscr.clear() if c == curses.KEY_LEFT: pass elif c == curses.KEY_RIGHT: pass elif c in [9]: # 9 == tab key tabbed = not tabbed 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(file_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(file_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(file_list): 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 selected_item_number = 0 top_line = 0 elif c in [338, curses.KEY_END]: # 338 = end selected_item_number = len(file_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 file_list[selected_item_number] in folders: # change directories if file_list[selected_item_number] == "../": # "cd .." current_directory = os.path.split(current_directory)[0] else: current_directory = f"{current_directory}/{file_list[selected_item_number]}" if current_directory[-1] == "/": current_directory = current_directory[:-1] # get file list folders = ["../"] #folders += sorted(glob.glob("*/", root_dir=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 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 top_line = 0 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[selected_item_number]}" self.appState.workingLoadDirectory = current_directory return full_path elif c == 9: # 9 == tab pass #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: self.stdscr.clear() prompting = False if self.playing: self.stdscr.nodelay(1) 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: if mouseLine < realmaxY - 4: # above the 'status bar,' in the file list 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 selected_item_number = top_line+mouseLine if mouseState == curses.BUTTON1_DOUBLE_CLICKED: if file_list[selected_item_number] in folders: # clicked directory # change directories. holy fuck this is deep if file_list[selected_item_number] == "../": # "cd .." current_directory = os.path.split(current_directory)[0] else: current_directory = f"{current_directory}/{file_list[selected_item_number]}" if current_directory[-1] == "/": current_directory = current_directory[:-1] # get file list folders = ["../"] #folders += glob.glob("*/", root_dir=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 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 selected_item_number = 0 search_string = "" else: # clicked a file, try to load it full_path = f"{current_directory}/{file_list[selected_item_number]}" return full_path 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 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 selected_item_number = 0 search_string = "" full_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 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(file_list) - 1: selected_item_number += 1 if selected_item_number == len(file_list) - top_line: top_line += 1 elif c == None: pass else: # add to search string search_string += chr(c) 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): #selected_item_number = file_list.index(filename) break # stop at the first match 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 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 = string + colorCode + char if ircColors: string = string + '\n' else: #string = string + '\r\n' string = string + '\n' if not error_encoding: 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 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, frame.sizeX)): for lineNum in reversed(range(0, frame.sizeY)): try: if not frame.content[lineNum][colNum] in [' ', '']: return colNum + 1 # we found a non-empty character except Exception as E: pdb.set_trace() #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, frame.sizeX): for lineNum in range(0, frame.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] sleep_time = (1000.0 / self.opts.framerate) / 1000.0 # or 0.1 == good, too self.pngsToGif(filename, pngImages, 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): 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 refresh(self, refreshScreen=True): # rename to redraw()? """Refresh the screen""" topLine = self.appState.topLine if self.appState.playingHelpScreen_2: mov = self.appState.helpMov_2 elif self.appState.playingHelpScreen: mov = self.appState.helpMov else: 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 + self.appState.topLine, self.appState.mouse_line + self.appState.brush.sizeX + self.appState.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 - self.appState.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 + self.appState.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, colnum - self.appState.firstCol, 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, colnum - self.appState.firstCol, 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, colnum - self.appState.firstCol, charContent, curses.color_pair(cursesColorPair)) # draw border on right edge of line if self.appState.drawBorders and screenLineNum + self.appState.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 self.appState.drawBorders and screenLineNum + self.appState.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'])) 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 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] + 1, list(' ' * self.mov.sizeX)) self.mov.frames[frameNum].newColorMap.insert(self.xy[0] + 1, [[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 charColor = mov.currentFrame.newColorMap[linenum][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 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 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 durdraw-0.28.0/durdraw/durdraw_ui_widgets.py000066400000000000000000000730101470476122500211770ustar00rootroot00000000000000# 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 = 24 # 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.enableTrueCGAColors, "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 # 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("Undo", caller.clickedUndo, "u", shortcut="esc-z") mainMenu.add_item("Redo", caller.clickedRedo, "r", shortcut="esc-r") #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("Character Sets", caller.showCharSetPicker, "c", shortcut="esc-S") #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("Find /", caller.searchForStringPrompt, "/", shortcut="esc-F") mainMenu.add_item("Replace Color", caller.replaceColorUnderCursor, "e", shortcut="esc-L") 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.28.0/durdraw/durdraw_ui_widgets_curses.py000066400000000000000000001345521470476122500225740ustar00rootroot00000000000000# 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.menu.gui.got_click("Click", mouseX, mouseY) self.hide() #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.menu.gui.got_click("Click", mouseX, mouseY) self.hide() #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): 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, " ") 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() #oldColor = 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") 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() oldColor = self.colorPicker.caller.colorfg 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() elf.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 if c == 27: # esc, cancel self.colorPicker.caller.colorfg = oldColor self.appState.colorPickerSelected = False c = None 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 #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(oldColor) 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.28.0/durdraw/durdraw_undo.py000066400000000000000000000062151470476122500200040ustar00rootroot00000000000000from copy import copy, deepcopy 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.undoList.append(deepcopy(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.undoList[self.undoIndex] # 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.undoList[self.undoIndex] 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 durdraw-0.28.0/durdraw/durf/000077500000000000000000000000001470476122500156715ustar00rootroot00000000000000durdraw-0.28.0/durdraw/durf/bsd.durf000066400000000000000000000272201470476122500173260ustar00rootroot00000000000000jfbsd.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.28.0/durdraw/durf/cm-eye.durf000066400000000000000000000145001470476122500177320ustar00rootroot00000000000000>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.28.0/durdraw/durf/linux-tux.durf000066400000000000000000000220741470476122500205350ustar00rootroot00000000000000̡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.28.0/durdraw/durf/unixbox.durf000066400000000000000000000124531470476122500202540ustar00rootroot00000000000000ifunixbox.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=v l< lpc1-zXG KuKljx$gߚ㙮s$duߍ=jt/xhy9cmW,BF}ܮ 9Y+"t|Ee(V~N N2 @ϗ f-(@xRttVuFq*.Ҥ08-8$Li_wx&$@K 4[QbF=J~jY "w;v8v&zL5YJJIw P @t+)LjQ<} <%,NԤR'Q$AE1vs +9mIO%A3ÅwfFL"xKN뗪הD[@$V^ GVBmD(CBf$>!Q-%rG[Ѩ2L1iJ-H\1gG(v2wF;r¾uaA ^c0'ݵԷl4-,+ǐfI)oB` SDN9C̶MJ4UG։̲@VVjEqfmIc%3rG-:q76Us9F݇m,txg MY5`it6|,\ϏpYɀ;#bsajΣ:wv<7;) ~gDK Y0֌Lc xh>J&[ZVhfspޙ*3;ҹ..붂0GXaPE3Xⓒ>>,Lϴn񟌏2ךg; I>CHB/iH_j{&YoDgllrEN0۽Ǹ3A h/5˾)\Os5{t}i\GStXٖfWA(-#Nm6@z=2H_VPi; da UxtǼ]zawߓz7B}qeC'\Cwe߇7H }חi2(fk "Gn ר@-Nuk4 Tvm1TkɾY7}u]ҿodnSgRjҿD[dD #~E_қM#,Uݗs \f/tz>K 5 k3iam\gV1%;"]#DyrA9fP7RM^G}IT^jc߻c#aoLJfQAIk&(C0;rWI9O3NugzL:_I^se uͺfݴnltǗ~3n6mQ)⠟696p18M}91qu=xXbC}OF]V⏸c4{K͸Kji6yu*vgrHLv }mo=$(<޿>t9 3g:t967?L6ld~LZ,A~ät9M@8wJW.O.o}ZboIw#.@ߍk[s؃@v'k=9N]Ae".;+0nZF"?iwIv<}빳H?r'*~A}$]FĥoPС &}w͂o|@~؛G?ّ~N|}+Mhw}$9!q=,"2(S߉GJ@?0ߕ&Ogk8]ARY|Lxd#U/|FC_\! IIB/縟-~ߠ7#JɓoԾa(&_2ӰhN=LJ&>D,?38覶򛁡f`~$!]sCތGA.l {ɻGv5w;?aCVs%OFҋy>wL*?_M7M< ^k8 Zҿ_Po]@ŷ5/ܐ4HN?eR?_i[7ڨ }O)$!yBŃ4,Z1_zOZN#H-M_uJ}Ѡ/_ HcH)N=,~.qe趵xA Kσb6/IlugNG5R(ݼ1X̝]388jVF֫l߱kg.Oh/(n)wq @:nT\]H6۠ i}hץAvyZg #P-~QFjN+/n~AmջYw^yuh$$bxW_'$=Aǫ3jl=Aoޝ;˓Noa~ٿ_o歆5˾ i4-IִnV[qhzVjf`D`yKRy7Ƹh+l[cqg¿۹-3Ɲ7oo-Fr{=1Աt_mi^thfjۍ=WQa|?K"|*j.ɿ=ǰ-*J[>)'TK\]bw¿^=smxE˗ 5{3R[.) E7#F#Qv~r@ QǷ)R&HU\VR36Jvt0yDStL@ b֋SBFj(wXSX&9,Jh2Y쁪1dT7澰p@u4Uijj@17sPS@q)UV1OJRa@ILIe:*s$IauI4aB0O@;ShN*x)i,Ih9AXRsvQ?O›:97U:qVsI@c34f,Olf ߞQ!ȔFXW=f=>،"%xSsXv}@v)؜(%*$Iiʢ)Eˊ4l' |a}kԃ$NyZ+jaՔ|:i>و\u0@CP(e2aNZQPΛJmt+NU>m~½bp2+ϣ3\%9UzG8NI ET Xl]!k<DcЦ@RqR Dk!\T@n o V.ll#gM"L9:\s&c@I;MG9Yy^OJYk5`"nت-9h;mഉ]-ZO[ڔa W.ض{і(ۤrۦ\MmU)oH+TҽW<_n8nMNIiqcWo@㬩QS5u"aHvN=h QwPtEQkvGOZSySV2$d%xE 1/8(UD!M4@٤Ø'Wh `c֬ŽNxޖ,J$Ljm[qJcjb=؏/KW,93CˌqF4 Ƥnj",Y5US۞`<%= X ƄKz\ +6m dC$l"Z/>=k}9ÂL cqM)4br/x7"A=`X=]+2 x%u8 C; ,m,őeq׫cLgHe5n'x\&LiQ Aq$-d%mt@{5YY:@,πź8S3&̸4U\+&5`ȊV) v=r[IW1Tl%(9|YCQ 3! 6u &C4Z)4'@@a^Mw$_ltD1NU)9JoI'b*sȔz1Z*0'Tȿ,d8ZrC-dX>i#.apJuPHҍ(ΕKd&Ge':Jzg2Uį>R;8d2D`LZTIj!*σI `{Rvj$r 5b=RUTtY*dmd(.2t(=)$ޫg!$5|C$Hw&n׆l=wJ{>ݶ<\K$g(V~N N2 @ϗ f-(@xRttVuFq*.Ҥ08-8$Li_wx&$@K 4[QbF=J~jY "w;v8v&zL5YJJIw P @t+)LjQ<} <%,NԤR'Q$AE1vs +9mIO%A3ÅwfFL"xKN뗪הD[@$V^ GVBmD(CBf$>!Q-%rG[Ѩ2L1iJ-H\1gG(v2wF;r¾uaA ^c0'ݵԷl4-,+ǐfI)oB` SDN9C̶MJ4UG։̲@VVjEqfmIc%3rG-:q76Us9F݇m,txg MY5`it6|,\ϏpYɀ;#bsajΣ:wv<7;) ~gDK Y0֌Lc xh>J&[ZVhfspޙ*3;ҹ..붂0GXaPE3Xⓒ>>,Lϴn񟌏2ךg; I>CHB/iH_j{&YoDgllrEN0۽Ǹ3A h/5˾)\Os5{t}i\^ژ}[$ʶ Vo7* @iq"m12!@¾JMI_$ cex ; Ի /,58b+>AýL+}TA7+_[f9ukFoq[_>jk)ZwF^VOeHҼ ʖ-ԗ}#sؘ?}U˔%ڂ$s +la ,x0nf2SAtu-<_P(}_L k:ڵ)iv&ʳ #>10j"=[HJH:Rsޥ {;>6 OZ@6pFzߑJ=y~q;Ӹճ0`AM+ck6uc>3@phwL1i.bpKӛ9rxcbz2m ջ9+qu0,]i&qW8Xm UtKNә-p;&^"zHPx~}rvw[g.[g!@LoI %?oי.Ǿ 0,^`HI.xgѩJ|}r݇١/~7r{- x5g=x d{֓\w+-"AJjV;e-2v.mӷ;;@#AxrGe mD\:@/a~,'-}J`{~ K1D.pwHBOS2!"G2߮>\]ix~Xs{$%=G1nXeB gh4$P՛"ϐ4$trI z=/1hv<|q#\K .T<8Kâʙ`iUtZ[z!1?ˊߛ[1d7ϱ ŀ4do_SyN29{Wn[)'TK\]b/x^=smxEKn4ׄgxQ-\OMyI*Dͳ`pnk榮x[r7 z: SA4U [G$Ny&!䆧5dN83MıUbGJư `؉sTce+rhP LMziJD'ª%(f[ Pf%1w'"*QK,CLBNXKA̓,Nl , )^i icÍiXKfA<Ѻ :97U:qVsI@,U34fyj2 [L1@5Ҽ1Ę%W&/rMa٥`sLH&)q/+(6;.BQ)OkE-q+DKcqj%G|뇩0dJG- +/vҊb* t VjX%Ԥ\qqnWe瀓_Ev'=Xy0*/ap;ZqdMB^-[FWȚ2Os5$k$)vAB+,%H>)W"BK=*|l0SjƬWŜPNQixaVdדR{֚gM,\^l56 ttm 6WEi+BB6ln[xtƶ]Mk66%T6njhkJyFZ dr Hqܖ7oRp H2ָM;z"gM-rMZG-)&Ndԓ {LZ^/R*ZhGI r=i1je^m.:L- Y9=b`p^zQL;)BHhc6)pvV8k>Ĭ7gsw;#~زb(d2!m2)mK:Ëc?%KX,93Cˌq4 Ƥu̔aVY5U<|+{҈A^)GXFj"/,~uM`x&2KV?GW*C/ \peu䞭C*xbOVS!ij:B5U+ǂ$U%|%k`Uᨡ\)H=VűxR,GiQM1N1եZ^N(Ve'ni8ZV[?쐞jAR۳U15# HQ1ϯ 3^ OSŖ=o0'&iT,B>8NZ"9;+xɥˮf^'N"o7MÔ9XSHSQ^:%W錺Eu&wJUx\Rѥ|M_#n@'rHRKnZ^F~ys!4e,h檭Z?kjEU{ٟW<ԯ!ڦ9V *3mEpSwEeP 7*VaZĪēbϐq,\շJo:d*TuQ9V3vxuˡV!Xa01RUG=3h*x KWĪ|90[g^J=pMZd jN \S,C+rXҐ.Zr2QcHc,ࣳ}g!ySm)e khyfYMYrufzXfXǤRr .Rb]B,;ćVPwih4@Y-2،I~,g?CƬ450(| 4߅rAVlRAHD _|z50z\ 8ʏRhGň^n UzJz{VLeJ;OqRc ϵ w2YX48#?!3&W 0yϐ$jN@L_6+:IZJvX%Vjt +2`Yuq fJMq' izXZLWLjy52Sz,bib7]# DG= tz=Jtj:Ne6Nmf;gHCnmm|ܮ =[+"t s/ܟ[:|s<;,=_*TH}q[U:ĩ Vκ KPP3}Qᙬs(1.-lE-( f%ؙR1Hg)+*%%XB-6Vdе2h#F5-P8lRJDQm͉jX$ ? bޙY1\I.9_^SmQxXy 'TTY * EɒxTDph8mE 0m2+ N#rŘgUrʤޱ օ6)xUwRRxH,"hh3C^& 1LY9 1 ;4AbӞ"+фWY'2˚%f[YY&jmƙ&Ѧg\Tu&b.br T,gwxwPy!-6eՀM LV8p1??g8ñ[gs&Xcυ99Hrczz x.HDa_/-2< grX3Z׿3Y,.b|V{<*jz+Al9זƺZhY}%ygz@GOHH纸 EbAiR`OJ`:0=Ӻ2>~*\ki[4$UZ G/# D8#}9NlF;Ndyo垱Fȳ9Uslnδn*twƾ G/p=goF{]su{e;hcn+2XU#6ĉưXoX& *5-'}, /K/,uBN{RF242nܲuH_7Uv 2S]e߬|mAh֭!Hũnm_7 ڮ-sjmyY=ٗ] R3K&.+[P_̭cc bLJU-Sh ́Hd~үKzz*ؾtX8qzNL[gs|fB}MtF3-6-j>dGkۙ(϶W.H< FKo]#)Y Kq{2ts:CTZ;*(?i:ew~G*i8~ƩLW€IqK7IkYwج֍-|­ע-j1s\p/}Noፉ9ʬw+|2 .]_!߸sɧA< s8?R|G|J䓑id~<;짏> hx;OBk Z4<+=iNGۢAW?8em==Dg~27$)? Ǎ86jC_pG .IH+P. V>*gWVAEkm .+~ozKӗoǰqt?f4aJ~SO95_ i\m-^ ةˁs$Ah@Eݙ*!+Q|J7oL=sg׌0-|զ'wٺ ĀC='A]%E6h1|t/#;|ui6tA)1=T˾eFцʋۥ_A~~vnV]Wgo 007=U I{Oa~+qO۫wgg؀MNg],7(5<$[ӺYm Km Z9nI} -y@cCHͫKjsoc` sWnZzƝ >l綼w`}WP>mc~鶥ykCml ol7._Gor4,ůϟ.b~_&RrJö+m=B6',R.}pumC[K:Ve߿{EtϵM/ẍ\IEs=5=OZ|%t>5ς¹嚛myp$f$a,LgT2lՒ8Gn֐9]TPS4T**c'&:llŠ&:̭SzYA)0 |;4U) sĖoj (n&4Cɞ~  ؋LΛZG- ʯ3ђ :apJ,=aB0O@;ShN*x)i, Z%5JjIb-D*n䰶STIÝ& lWYҘ]%T6l 3\3cc>OI"ʤ(۸U -1~f.c GKNqnj6%!2bu81i3mޒU2p+g@̅!aJ)L!L `ȢZ_R~:F|=-`*KyBxcՙsyte/R9T!l邱5.VVKѪ9z'`5==M4Y `:_ac]r՚Ψ0\Qgb{W%]4:BF! t q(WY䆩gTzמ:GNS֛2n.nVZ'1XqCzmc pϫ2V7IxZd_ {bEjɪEJ:wF>FRfgf:n͜U WgǎujuL*e)pr)u.S%C|HuVN؍Fۢ!74iws=dJNSL> ʊkRϧpQA]X+dŦ,tMDKŧǘZcuX)]a 2)倆qTX _ kT&Ľ'U1Vah\!E;.2c⡽l=zp L˄) ? 1a2(P#ۙ`mמUBhm +9K"AXgʡ`Ԅw271pUtŤ YW*3eGn2*Fʚͣe9'!kȐUV VDGIZOXL'0@jGX(VQS W =i@-Dy;=bqlOND8aFGཪVvN:K,>M E'V{l#$wƖoh$)dNo1lǨ&$xD!ʀH"6- M Rݣ:ARK. G5y])] R+0ďS ?7e~ծ&&ϝѩ~S+x 5r@t@WޣD^Nᡓn^f3v l_T;pdݳ>)O-ϰ<_7G%y J/!ށ7?'UQeCʠi 4)L?N :S%:8" ҂:VXjzbߪ`Vݎ*StRR]%PacE]+v1bT#O5O S&5I5IP}fݜh JeAxpSIp!杙8$5!'0+pBEЯb[1ʐPD,8@oHT GV4 &s_Zj˯7)WyQ%Jk+7?i ݭLюo]XmsX%>I:`pw--%w";61iRʛÔ%S@C$;)MxՑu")PbeFfYlmzL\g"f"pK*`܍Mܯ{r@Q~!~wh' 2قlSV Xh"d3:_) m|3|9g2=,\󨎄)]=ǰ1ύiDJA$.>"rV/5x;Ә_KdZ3җf/Dh6Q[:9o< \S57v/1L{rHw7n싿p{z op ozmkM-]z5>WW6}_1{I-U5ͮJo,PZGHl k{e"ﭠRw8Xy_'錄'n*C + ~-_%NxXeʾo@p/+J/?ePזDfBQ[&hu`"ګbJ?ǩՓ} 54o"z e:6 &Ϥt_2 AG 07/GX/K7礙AT_P|6<7]q ϗ~Aj&mDg4Bk"άvcJvDFZl{傤s ̠nȽt5ҩǾw)+C:G^bocdGB9l 4 BԻƇL0r{L~'+~W|>^1wIId=F1qVٿC Tz~qf3$ $ ]~@k5z|*&OSE;~ɔNV=:A0)Pv  ]ӿ|_㠛o~#]w)cz3O]3%&c>kp\Y+;| \Xi3y0`00WǎS(%ͧ~׭$]AH>I/Fdz~8;<3 `a|6_߈7$t{KU_xN/Nϟ[t4hI:A-tZC{67+sC#9-K~}F ]o=_s܈k6W>}䱂 Ұhr=Aj;]onH|"4}q +GslFj|1 !;TSLΟƕ-@/=rۋڼ>'A[ԝ9rHtc1wvڂ_~LJYmYꎮS'on^J3Q,*:>Z+ޖMbF”{FiM*Q-S ~Fj(i N5;Lq@XQ큪1k>vbƦ/hz1Xي%CSa~+ɹj@17JlV;nB3駙@ILȤ켩uЪ:-PVcF!$ )H9ंijBk`XRsv$֒Y?ONk;eMNhR vL-Y%ZLeÖ0S!)=c03F%4z*1fɕ {%xSsXv}@v)؜(%*$Iiʢ)Eˊ4l' |a}ay qZQx\ X;Zda*L+QbeʋX 7:V 5 W8ƫ|\`U9dWѣIVGg1L9J sثd3q.gPmlˀŖ\MD:&  m ,G-Jt&Ko HaR6rp$"̔1kU1g8t#^ԞY [ݥ4FilMmzڊ6Ц (m81]mWqMeqMI0঴M5ZëR܄Vh٨;{x@y㖹-oݤ dAdʉqyv7vCN;ΚZ5 PSZG-)&Ndԓ {G-)GM-Z=$M8IWH+fsYױejIJL!pg΢{W^C"ӎ;Ц ۲1;έ gMۿs1YŽNx|C)DI QVq2Ni[bY],x=xŒs1Í=dIgDpJnLLejtUYŃg+M%ret&㮤ΖM*lnʼ&d9^D1L:@^cJ';-/g12P )9&113jMIy^. 0)Ug㕽|JaP ׸ \Y]0g ؓT4hbMdxʱI` _X@%%4?qU8j(W RUq8t^.3ukTS SLui S5U鷛jbV;ZlF25# HQ1ϯ 3^ OkSkŖ=o0%iS,B>8NZ"9;+xɅˮe^'N"o7MÔ9XSHSP.ꂩ _\3L0qԙ؞nS/`Tta.*_췦u( *"r?ʕ"AG-ajy^1Ӕv kkV ff*k\PhX;H*̴Mm6ޅ9B*޸XQ@jO=AƱpW2(oqLx8SDXi1q,Z×/csHViHU8^0Gԣ}ݏ*+,*cws\qjc^^\lIy)5i eK8)pM a+CVjɹDE[ ΪcOQ癙[3ge7=dU.ՙYcBaJY1+º\JKw <ZAAlve(k`3&Mpvz(i0&\*h|Yik +"a2&01XqdJW(?nJ9a#{1W +eZ1I+qg8"~ Q%#!UTĔ0cBrOP QyNrOX(=ۓS#7NX2'xըpR$On CuCI ^8$$mEʿ3Y|䩆q #51 b,Fjg D"Bz2 RH(lajp:HpS-HarNT =GQa5{vWpW9tԊ# cT|Mǧ_hks{tCz v\(0(ѩ%ףӣsx; 8]Cן!U=qq6g3jݳ>)O-ϰ<_7G%y J/!ށ7?'UQeCʠi 4)L?N :S%:8" ҂:VXjzbߪ`Vݎ*StRR]%PacE]+v1bT#O5O S&5I5IP}fݜh JeAxpSIp!杙8$5!'0+pBEЯb[1ʐPD,8@oHT GV4 &s_Zj˯7)WyQ%Jk+7?i ݭLюo]XmsX%>I:`pw--%w";61iRʛÔ%S@C$;)MxՑu")PbeFfYlmzL\g"f"pK*`܍Mܯ{r@Q~!~wh' 2قlSV Xh"d3:_) m|3|9g2=,\󨎄)]=ǰ1ύiDJA$.>"rV/5x;Ә_KdZ3җf/Dh6Q[:9o< \S57v/1L{rHw7n싿p{z op ozmkM-]z5>WW6}_1{I-U5ͮJo,PZGHl k{e"ﭠRw8Xy_'錄'n*C + ~-_%NxXeʾo@p/+J/?ePזDfBQ[&hu`"ګbJ?ǩՓ} 54o"z e:6 &Ϥt_2 AG 07/GX/K7礙AT_P|6<7]q ϗ~Aj&mDg4Bk"άvcJvDFZl{傤s ̠nȽt5ҩǾw)+C:G^bocdGB9l 4 BԻƇL0r{,Kە~aK+M>Kpn#8F~ _qῡ *z=z3C.Ã_q?[ 5z=AoGE~'ߎ}VQLXdJa  zM(}n;XDw?gqMm7Cq.HBd由 w'\خw1]jf5v8.~ ƕK>.x4<o0?+ctd?S?rVO |$M#Og?}U@ZGx;OBk Z4<+=iNGۢAW?8em==Dg~27$)? Ǎ86jC_pG .IH+Pb M+f3+^ V 颵~sCcdI7׷cX8oc3Ti 0%ީer/4 ݶ/hy^@P9 `~ ݢ (F 7kw?>\jz;Vwtl Eb .n{BtHǍfA>:헑4N:W ̔~]uܘ^`De2 #hCQ `/Ƞz??z7֫3XX] '0axuF'ջ3zgyizl@Ls&OW'dzѿޮ[ k}iZiݬ67>[z+bc61tҼHۇ667{ޣ7F[Q DWU\Ow1?/)9{a[T}l!SN  l>-mŲ_{&pi $Z𹞚'D->yKuT:*\KɱvB|µ\sSW-9NݛČ=L)*dQWUa먖)?r\5r8vJ,@U V5;1acV4an slT JiۡB0?Mɕ\Xd%L|PS@qw7JL$N^dRev:jehU~T( +Sb1ByZމ͔}ECpR4KQ5Mc!M5b,9PR;Mk,'ZWawS'JR'j4her,@-QaK)zȔFXW=fC9Y> lNIpIФ4e"eEqEbz>^Ȱm~p2+ϣ3\%9UzG8NIreb YSp&"`6BҎ#ՖRh%:_\7JrSx[rGec98o fQ͘᪘3QJi:?/̊zRjZɀkUဎv4&vh=mEhChSچ 24f^qhc<@[cۮ& 5G[lmram6njhkJyFZ dr Hjpa2孛,L9q5.nӎƮqiQS iy [ME5%yzҺA@?a/jJgPtEQkvGOZSySV2$d%xctx΋RuHdqOB @ݘ)8B+5mze֬ŽNx1KYa12IDeuj!㔶%r{wxŒs1Í=dIgDpJnLZl5*R?8GoeOW1[2YMÅo^I^Cq놬+hm2EZΤDa ֡GqG/=c14AL1i=\TkLP|tLXe)O/~:>S *.7^@=[5GU ŞGuDk",`VHKBf*I-QCRzcš uX]bbKQ% QN W8 \'pTX ׷z!=ՂPgWXLZ0X#P'aS<*xb8e V4Jѯ[%U$B04Ujx׬%W)/y4\0UR@ʺNOyS#LyB9:߫{]ӚΨ,QgzjǻLzUxDR%{Mߗ#n@?z"WY䆩gTzמ:GNS{2nnVZ#1XqŃzmc pǫ2V7IxZd_h zbEjɪEJ6޷E=FR&gf:n͜UWgǎujuL*e)pr)u.R%=|HuVN؍Fբ!54is=dJNPL> ʊkBϧpQA]X+d&tMD ŧǘZc/uX)]a 2)倆qTX _kT&Ľ'U1VahD!E;,2c⡽l=zp L˄) ? 1a (P#ۙ`mמUBh] +9K"AXgʡ`Ԅw171p5tŤ YW*3eGn2*Fʚͣe9!kȐUV VDGIZOXL'0@jGX(VQS W =i@-Dy;=bqlOND8aFGཪVvN:K,>M 'V{l#$wƖoh$)dNo1lǨ&$xD!ʀH"6- M Rݣ:ARK. G5y](] R+0ďS ?7e~ծ&&ϝѩ~S+x 5r@t@WޣD^Nᡓn^f3v l_T;p͐Xix٧ۖgXwd |ʯ #IfV̼@ߪΨ!Neдr]d)d@CqiAf+J,5X=hGoU_0+AndwXF:K)^Q).j"qE;1o)`T$$h3nNTsd%Dz Ix<$hf҈IxOuiRҐhP8 UWeH(LǃT7$DQh+U@i9/]iWp+<4]V&hGNط.9Hkw$0ƻEbAӝpE4)Maʒ)gV ޡ ӝY&:Y(1J2Qh3άM6i6=ۿzR33uu|%Cg0ƦjԽ`9C (?r͓lA),m@f6>r>3pzDC.QyTGBՔcΘ4tqG" zi9ƚ@i/X8`qoӰQQÐ[ dˑ4֕B c.;Ce2V?zBr@:eVP`(+ HrK|RЇ-SYZLb!gh>z9I%2-Ktpd3pq"C4|(-7BM\ȩfo|pwuS9M7_8=x78i7t={6õϖ=+AStXٖfWA(-#Nm6@z=2H_VPi; da UxtǼ]zawߓz7B}qeC'\Cwe߇7H }חi2(fk "Gn ר@-Nuk4 Tvm1TkɾY7}u]ҿodnSgRjҿD[dD #~E_қM#,Uݗs \f/tz>K 5 k3iam\gV1%;"]#DyrA9fP7RM^G}IT^jc߻c#aoLJfQAIk&(C0;rWI9O3NugzL:_I^se uͺfݴnltǗ~3n6mQ)⠟6C5E N|sz5G_oLl\]!VfP_Qpz=gE#6>R3ZM^b5a:S~+wCa_[ ϯ2];qןn,\>\߾0;ޒFn]b׶,rOzrnE\%vWa6HI ܪ7E&~ܥxsg~ OT.t?H,K'ߠC%LvCzl/u7ُ~1#!q 6VHI s]CzX9Dd=Ptە~aK+M>Kpn#8F~ _Hῡ *z=z3C.Ã_q?[ 5z=AoGE~'ߎ}VQLXdJa  zM(}n;XDw?gqMm7Cq.HBd由 w'\خw1]jf5v8.~ ƕK>.x4<o0?+ctd?S?rVO |$M#Og?}U@/n_iϦ)s:m뙜trvwv۞9Olv2+"+!viMbEѽ]WMl< ׿RHU:^xǟ0\nAg]@ Kb]|vݯ%)@a XGb0rEN_[lC1ǏBRtALTUǝ9Fd=B]w#2.k+/n hӋ7':^y H QOH{=x}4,qyl@fa&]_.OOz};vf׸C!7$x;[\W},2.k'06N7S8w;ޠ;w'\s7Q O}u}{s'H.!]#](._8Qwww>6^|йs8vJ_:Kg˝'? >-q~MOOW[Pzx"N޷)#ɻi lޅ{^ -}gM<=sW4?6<3Ik=5N_Eݺ$J3A" SkS\ZZ:87 z: gUIda\5ԈYA㲆ǜ3wj$xD=PJ:fHoUҡb Leh%ցo'-Jɣl2ݶM-dڶ3RQ7`9Gꡮv%=I65׸])qvCC )$ɬZwWcV?Q=O:gNc;CMLYT{sR=ï!L鑗2&j6օLM.޸k-Ej%PX-kKt-U)*J/w ]fjR`:$^*Gdzz܁H:l2[Nt|;Є`eGX!H}QKR!T^"DW/o>QEfD*xD!8Q(@ 'G}vdh$̑|toFчd D _sa=0:#T9j#9D}jxz`:(Bz1X UϳЁ8IP~Jd'I3I`,*,ICSlc!8ΐ߇GYrx'XAEbD^O QHX2pkB@W޽DGKGGg; 8ڮ!o}UzW^8rd͝{B̩?_VȂ|2<9@|"'ܜZěkG3~<*~"d]ƈ.(1GOή"UG۪d?Gv]6V\izl/V sssGjT|J3P%]PesU]+6qDiFj^NjQ(:&;qJAza$q?69 b֑Uae.űUlKFWjגd{TW^qEZ~ +42Me?-|CZxTʝE H1mC+kWYm \4xʕcztV<9]Vig.9-ꬊw0V4úE|i]pe9fɛÒ%!j_avYH+d@j]Kl+4DeqjkI%354e#atvnl~+ 3r݇f!!c9mɪk[ܑ@fYlpdr~~ps"H ܹ}PsψW5yWQlgscy. QRpzeêQkJ7ʿyx|_tcOC돏d CYoMH=̌͑kϧNUhfspީ*5A_p3]\mE6aXaD)S>x7E5Znh/eHcr|rdύ"'p N?/tÅ"#}BN;2"?n޹ 3.V8w5% 0ҹtoȹǺm)1i8WM^0Br-ws'HwQ7ҧԛ{&ZW ~#-iwo2qt$}]E_2CrBܿ[ ígJJ}H뾜'raY\{\mQk\'h$m?vdPU.uDY=֝#w27F|]˶|LANE鉪UJAg ?oeXJOc޽7 pt3sbtzx6 ]Yys2k3[beufK3ҋmLT$g|\a]EnGB(#=ֱNwasΑYŻWO-n[*\8A|ҿJ=qIRgn$>#}l;6ŕϗI&̥:{%nE׿c &:/K/5-4($'Mwjkêl7q/vwFzvw`co+GB70+M7ԍ7_Lw/߸ԿWH@:Lg${`&^_ěx{Xt4?;`JO=_gbo+&A\ǻb_gKR?ŝ8؆>cʏ) ynBŃqlr {`8y [gi\'n*E1،ـtuIvǞ,'Iαqb,@/=Mó<p>!A> /bw s(C*ήfn.NoaV_.~zwqM#G7e oQP&ÿm.,8q倜fyx\FH|׍qG%jƟϱ;sz6'ZGe\.0V^/&goNtU:vđ1V*ã?a{#/Nc{i|gY؀(L\ޟvv`qBz#'2nHvY4he&&]6O al^Ƚ1nq9v AwO:K;1nhN<:}mOp\]pCG^Q\pL[q}ls{凹pb힕t<;O~"|[*6:֯(E,¹o!SF w . ]gh,Z+x{, h~^'n -{fj{jz$xuIgDr/&Hmupto +0u&ᵥA4)O:%R?FjQZ[U##RT!BU $^]E+ +ƘQYTF[bAvpRΘ5g%W׿>{)gbmZ ڥw aW/BMɤ M,:xCljDf aiDӐV؝݌i@1X4&)_ 00sa!9I SpQ5F[jvL.ff]u)C!M2c y!=d@ƲE52/zΔq6*LS1fzZ1kۣ'TӚ+CӐخ5g Pk-XsH3z3p qBQaKGŢ^TQZ'&CʮLu )O@88jH4{'H:Eڈ@mHP'hu5{&2CѮ F%^ h 庮cOY-PMjY[.fm;(BHhc51pV8k>#Ĭ7g9˻>;AYwlYY12dmZm 13gT=xb']d噊5{Ȋu1\F9 ݰZfH^HOު^6bwcV1/]v2K eu)Y 1z9@^cV';0j }T3VώC61[`R=."M"u /j;Lk{,a”f?&C2uL| )!z˒j6BDY=YMB._Õ@\qW@%0 bC%-Vg]ER" 5JUjZzҒ חlgAB*؈Bq@@OQ=N8_ Pi`D_W5KeH{/0oNR;4$ӆA^7MNxboTI1o4$d'Xک^]rԚ>Q7`9Gӧ]IFME5~YJGHC썓'VB&j]ZZDӺ<) 8i/4u3%kgRR Va,K h $ 2G^L nIX*3O6{㞢rت)b VI26A&0!<&C{_mqգb!9H mn0>RC <ڧ >}o}*n+^Mb>0/"#\Hl=pY-0th՜ख-:zʗf^M6jCyϰ[ O>Cy֓6:5L0}l.bLnHf]-1ֳocvWL?aޡ o@=`l^CT,7{vQT10(#=6tYIs-ꐬ{q&#N6l-jV;avBlL{,;ћ<|V(W\Ę\Ee"suafwz)c;R& ȣ\g$f6`d^ Sf\ܒU%(wQm~kȌ*,*8bAS7D!Y`Pծ v^y-CGn^f3h 6׿ͮ'Ws_^zm{2=|[D!sTO / dA>TV >ތ?znNM??Ki.FceU'gWZ#mU#i; ƮLriiV+J4X=PGOL+۹ݹ#i`vsQ>o.j*rE84#KR5/ S@'E|nޝ8 =0w n 1*2*%+kIC= K`*+/EḊ"kBmŕaŦߟbU!Y-<* Y!.<1O=:Deme.os+zG3l{Zߜ`uVŻs|]+}Na"r|yMpaɒ K0X,OwSXQu %PrVf85Qj陚BG20k:\;y7r9FC~}ސ됱朶dՀ-V 3,68eUp9??W8ù[fq$܈>bϹgD{]üG(3繱< ?()`Fca(5xX_92lzD8nb'ϿB ?o >K'BrAui\ KgHm? u_aaKc_:ja\c]r~6V 4ISeq!gSû]FpOEڨeS\dͽVy`v}YV8>u/u!| ^9!Vp߭oGV_E~%>u_m90F,=ݶ(G5.u4`2* {lΑ;iUz_C#]ҿe\> 'D* 3 M|󷲉zk%ٿ'tޛszJ9gt{:=<<[9iPf~Ktƙ-[o2:3%6&*y3>.0W"ңXWW̑s'0F}XuݫԧQNh-M.R Bq]%㸤q37ԃIqHw6]gK$OR½1\㗥ĚN;{sx55aU;t`#\Q0#xvW}|̕\ƛ/Eu&VtǗo\߫Y$ 3=vMr/fx NI B'?nI˱np=>W.G1K ät9]p)>ީJtqoޝZ_=FBb_N0 O<Ux``'ccr̉w9q=RSjU\U&~?UF&4<\9~{2dYۈtr+#Э+&} Mb|o xoom[lMCx|Q~F|q VLՌs<=$'8kkFv?c*=D$65ަaP|LXHIz*!Ӝ_(oɿVϤ_ɉqtH/'N agM%4^4^323Se~d[taQWx$ JW#s:w̓n0p7 dR88|f͟ƏIgQb>|΋xb >VI d7H L8y:2IJqjeeǯ |42ߟx|"A?l/]gWMl< ׿RHU:^xǟ0\nAg]@ Kb]|vݯ%)@a XGb0rEN_[lC1ǏBRtALTUǝ9Fd=B]w#2.k+/n hӋ7':^y H QOH{=x}4,qyl@fa&]_.OOz};vf׸C!7$x;[\W},2.k'06N7S8w;ޠ;w'\s7Q O}u}{s'H.!]#](._8Qwww>6^|йs8vJ_:Kg˝'? >-q~MOOW[Pzx"N޷)#ɻi lޅ{^ -}gM<=sW4?7=3i=5N_Eݺ$J3A" ST]ZZ:87 z: UIda\5Ԩz-^RI-wx@)DFDu~cM,i*#[- |;8)t qZg_6-TWһŰdRw&zIDR6B"Zn3^nZ4Y"ia+wnƴ,BÔjF\JH$iʩ~ݨ-vk;LJJj3S3Hڐ&jbbnֳ&4AѮ F%^}3Q77z=d C5FX3Keo:"2!m""fZicc1͙vn'}O3QV~V Qf6Dn2Fm[bUO) xk.bG6!2bEsaMYGLE|dnUV1ț1XYiWӮLiW(f@V7ƨ:ɘgZ.DEԐL]G!la"zC]YD2E]w.X€)~Dxc̗=]QCa Ɩ%Vkأ9z{ V]+L#*⾉5J#(O^$.*ŨJ*[wS1QǕ,MыN!եF--@Pq}kKk78`%,0/).=˂03T$À=Sa{$qU)eRj0# `&vhI |n~r4\0)g꩓chI$NSzk!5}ns:ۉO5_$uk\8G!!''O4Mf՚R\jsy9Ovphf.vϪ55X~=m 6dJ6P.Tfڟlr=EE'\kT-RUUSXW@R F34r_Ր Q)'"/zL*2S&jTIuH# `DI.XJx-<<򵄗<ɦLX7&ZԪ 봓$F Ie_Φ5PܐE~3JL/> H%kh$ 뱆!jPQ jPgETSA0׋aznIb#PB$ =IIgWi$dIҟb !q>4߰<̒EPs%>j/#r7}oB]# D:%:Z|=:=:ݼfv lcM]O{''xݓ7wwf;_D" ן3zb|Y _ 䠲fssjo\Jv1#.;+\?9ҊTin\Ia0ve KK۴ZQrꁲ>*~"`ZIkS)|C$w W @Ut+(YyIX:E.蘜vc)u驆Vݟ^t3LYGVi V-_]KQXS&_y) UTYko+, (6% jQ`(w* Ŵ 毬]ee7p)WyY%*k+|wy[Y;Z9`ۻ*}StZ,vE]wÕssO%oc KdXr}e|˞"ju-4uƩI'ҧWL:Ҡ1LLJY̻t/h1w;Xwp\u4%mqGf)>r6#)pFDC=#\]=G9ύiDIAL&>{0Ew F9`)=ߘ*`>~}> ?>5 e5!]036G=:Um̥yz@g(~tqi=؄9dbi9` OI``"tOORj1u!Oɑi<7f'roq#/X;!} Zx8|[ ]:9Ȉr rOx2UX:Gzt\o[iא#'_KqUKw#s?03۶a_xH_-7Mz(K 9˥2ϝ#}-F-H"SoEsh]˂ݽ}w~ɬs\ sn}82-+)!rhˡ4gqqE9үqk}A՟nTgԭe`[wI̮u/N:19'V)s)HTp_o⛿Mc]+=~{4Sα?,t fIˬ2[3lazי,HwH/1QCqtR dXǞ;A߅1s:G_g^->rEolrᔺKN*%K}ywyL:Dw:جW>_:'~0}^), ִРpH7 ܛë6aĽݡ;ݍy e4R7|)3;=R^"0쁵{ox1/Udp*n/O*]^b=1~ǿ!v[O*]}tIpt90Xr&e˱O1,IN%wWw}~dZ0r{x7w)`1k=fN\,ω쑚zW:&F(26A䮢d52 0ޓ!F䥓_n]0dhnS~k?ĵ?o~{n?lbkC拂3oϏkdf! =Y]] 0{T "巩A6 |Hc''|MEJSY|(@G~K7&jLNCx99xubߞ4띟8k*x=u4l/;s8l= ;ߢFҿrO#рQ:83gtI'U&C7)۵7n4~L ~8[wܞfKnw^1yu"wƸƹ1ݹ?,kĸexrx8MݛKw=5Fru9 qbz}Fq‰25\o=SÉ{Vҹ_:X4>_nol?|rZڂң vL'lN-Hc`.teUh; =ʵzD, U b9(P{DS2u Vcp,\ݎԖڦj$ kKU(Fz+XGuC7@faŬU5fy(c4:]שzmS4wFGQ;ΡvQzmY j#jvۤkM cmضxͱJЬ%Elv_w6n@@J`Gv/ GlI?a~1qw6zG IfivH iPPc=kB `!Y%h{ЬF_]:բ Մ"A>kKY>|ok۪#"; ۲1;. gMۿ~l1͙v^'}OOeGPLefC6nj!cԶ%菜Qa{wc,OT5CVˌ12rWa6fv(C*B@xVo Sŝ&U?usRBY nw&doiaZ;˜ ƎkZCճc 5 AXTb3H`@ K0eُ/L_ǫ:A px$ {4GxbOVS!DPp%nDWܷ1PItDBCPC` |=&Y)~)Ũ*oRUw}fa8eۥYP{ u&Bq@@OQ=N8߬ PI`D_W5KEH{0oNR;4$@^7INxboTI1o4$d'XBpOK{]rԚ>Q7`9Gꡮv%=I65׸])qvCC )$ɬZxWcV?Q=O:gNc;CMLYT{sR=ï!L鑗2&j6օLM.޸k-Ej'PX-kKt-U)*J/w ]fjR`:$^*Gdzz܁H:l2[Nt|;Є`eGX!H}QKR!T^"DW/o>QEfD*xD!8Q(@ 'G}vdh$̑|toFчd D _sa=0:#T9j#9D}jxz`:(Bz1X UϳЁ8IP~Jd'I3I`,*,ICSlc!8ΐ߇GYrx'XAEbD^O QHX2pkB@W޽DGKGGg; 8ڮ!o}UzW^82=zsB̹?_VȂ|2<9@|"'ܜZěkG3~<*~"d]ƈ.(1GOή"UG۪d?Gv]6V\izl/V sssGjT|J3P%]PesU]+6qDiFj^NjQ(:&;qJAza$q?69 b֑Uae.űUlKFWjגd{TW^qEZ~ +42Me?-|CZxTʝE H1mC+kWYm \4xʕcztV<9]Vig.9-ꬊw0V4úE|i]pe9fɛÒ%!j_avYH+d@j]Kl+4DeqjkI%354e#atvnl~+ 3r݇f!!c9mɪk[ܑ@fYlpdr~~ps"H ܹ}PsψW5yWQlgscy. QRpzeêQkJ7ʿyx|_tcOC돏d CYoMH=̌͑kϧNUhfspީ*5A_p3]\mE6aXaD)S>x7E5Znh/eHcr|rdύ"'p N?/tÅ"#}BN;2"?n޹ 3.V8w5% 0ҹtoȹǺm)1i8WM^0Br-ws'HwQ7ҧԛ{&ZW ~#-iwo2qt$}]E_2CrBܿ[ ígJJ}H뾜'raY\{\mQk\'h$m?vdPU.uDY=֝#w27F|]˶|LANE鉪UJAg ?oeXJOc޽7 pt3sbtzx6 ]Yys2k3[beufK3ҋmLT$g|\a]EnGB(#=ֱNwasΑYŻWO-n[*\8A|ҿJ=qIRgn$>#}l;6ŕϗI&̥:{%nE׿c &:/K/5-4($'Mwjkêl7q/vwFzvw`co+GB70+M7ԍ7_Lw/߸ԿWH@:Lg${`&^_ xOookgLrq2YmA$8xWl#]lI ǁGrX)41yu"wƸƹ1ݹ?,kĸexrx8MݛKw=5Fru9 qbz}Fq‰25\o=SÉ{Vҹ_:X4>_nol?|rZڂң vL'lN-Hc`.teUh7EfX4HX+hǚD嬜қmc@C0j [:*\"/o(:Q5Rve;mH|>v2)zkH<)Y  0RsH! QfjelPEPY<3TqM +׏-MHAbP V z20o j#͔5ҋYj̎Pƀit&C-SSE5 \i#̍vC8t۲5FILҋ+OZcۮ 5ZnrfY=l|MܸVhY(Yڽ&WCe֧N cF0I۸[W>88iH4{@1k 6m+P6Y=jA vHIC=>K<уh 庮cOY-PMj,]\ D]VwQ6 }7fv);. fMۿ]vޚuIfC)3y Q۸Qۖ?rFS1FekbG 7)2cUsa5̐ s35)U[m #bdeq UO2ŧ]8duc!Y }1j9@^b }ZC3c 5 AXTbhGH`@ K0eُ/L_ǫ:A px$ {4GxbOVS Pp%nDWܷ1PIt BCPC` |=&Q)~)Ũ*oRUw}fa8eۥYP{ OԨIdP#OG`S|*L8%uOM*e} غ(U Ҟ%ļՃä 2Ɍ5ӏP&؛%(U=qR6lz,d; :ӄ^O `Qgz%/\ICME5~KJGG=9 ?sbEi2V՘OT;kϓ9Ә6@S7S{V!E!Վ`bTik@ Sz̴u2$`7)*:Z jŠƾ`/Io:@d`R=ՆW='V={Xi@O ҕ#:䱿n@ȣ}*++Ǫb$V*22-e"[ CVMj'|MAhE$j\8  ,3d'=i̮YCSO:j͜ՙ%ر!fN% _6uf%bww)-PwE2`Lb%f `Ѡ eǢoNejIlCɾ"@ W͖B-!֨E,^Ck=n1fw10v0OVˆ1D%~glEKc k0"݃lCT=آɺ1^w`b=$a⭪fO/f7X~/ĆY'P  Agx5X9@UQ&2WQfv){2c EobʠuFbffL0e 0u-Y\rrՆLp̘^̢#F4c@ oMZ_hi0Fx~è=/-5ޫ ^|ѥ G\ eI?CRW5{WhdΖnl!aB-hId4/pڈO(N?4!X|*GH0RinT0/H!=yKm[`rT2QJC*QN7#Jrj.P+hI䑯%J#!K$X)31`-*+!UF|;4|C \P :h7w/=te6k`}o[5d durdraw-0.28.0/durdraw/help/durhelp-16-page1.dur000066400000000000000000000043511470476122500212620ustar00rootroot00000000000000~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.28.0/durdraw/help/durhelp-16-page2.dur000066400000000000000000000062601470476122500212640ustar00rootroot00000000000000@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.28.0/durdraw/help/durhelp-256-long.dur000066400000000000000000001125371470476122500213200ustar00rootroot00000000000000ggdurhelp-256-long.durmw8ߟB6gۘ$O{:m=36'igw6Lb6x'w%$!l ,u_wsỹ ~Wwg96 gC|^=蟞wfyo9:6woam8#~@7ݰ) W]} z֟Q´ R/b:b'UYv>̧wwFDz+ +M__W>Vwo' `gDF麽 G@P>XD'A~77XLY7+]X?/ ajnoSǩď $˫C$/_bYŊEk|7b8?zyy!䋐b]E~^ b?Ixa|@̶`@(XR ԃoβ=cuW7|ӍB 3P6 GOIw C^v~Q7 3N՟/H|a|gfn|nt}> YƷ8WV(ıʹ|! 2?86a̴G8]a!=>A0QnLP0)s3*,X;Q9OvR<..{ C,]oӅ_`Sd#(NyPz<_BW,XIA~Zi$#;rEr< fP8?w/,Ĥ?C9ra][PjOQi-LM{hK%H&YYZ*D ZqYn^r>Lȑ3(RֱΓ,k("g&OxnԷIL83ohR*9O~` ȤNEG(B51n?BnNpO <`-|㰦¦1 66'3҂ h}n)K W[ 6xװiB9sd$c,oto[̞1s8fη-**P0iʷ-b# ^إ8T͙0hf=Tc|3A}#.yGtkN);AڔdW(&{  FFyc jK9'hQv/QȪ*քp=G܊ w*Tqr,8 j%lv; lBS|( Y_F VyUDOiD 02)CGXuk'{* UW@+t]M/W+x ^ p|")M8.&Z:nТAAs4% :6.oW[l[F؆8La铀n>|lzLOl҂F'QqkZ_YAjHToK8OU5Kۗ`t@ $L}|e|'S| d1PCG_׻S^HGRZH|8xAx B7Z)pHC`ؿ]b{U'%кVþCLĚ(KZwC]`3n*eXc!hvJ5c+ #O&oSVT=r1C叭@1w]Nz-j0;rBW<K 9j6 Ces!|J91X|~ׁ5E,ܱu%OOO *z"U3yO1=[_~/M÷L+6 {>箽R@R~onNΎ@ X$5񽼃*RRBeK< LUCu-)@eݗ`QE\(P3| hd3#L|CD:%((ϔur~1=vTwv VT0e9"ZՆ"扒*TSAI4i (Ul^ KՅ&L]p)ES.Olgү!IRʧ"et#Z.Rʦg0+.v*c0ӊwrnT:DpIa\T*ԵP=|(DzCjP.AD5Xt TeT*R|mI4 eؑ a P|,Tk`H!4\ H &PH_b* e4 .IF!9cY(jpy3qo D.%RD*GVH=]Qk2'M܁ǼGqН aCOBH|;Ea+#HV|0j##i:|6ɳ6g9DR-$΂MbQkyIX{D,Ri40r#I8{! VXx>9F̱oOfW[$Rm(4 V%Ҁ񖶐Ė&ĶrrXE h[aK!t+jC0,Є!WʉF45ЗkPf+}ǭO8u_4+lzB8aڡrI=VeIzSN41K꣨@ @s{fGHCsX|X^Q'3y ʴes9UcCQnQlUb&;Fӝ!}9<,K0Irx5cg*W+b@qnɁTM[@X6l۴"K\ʎ[T jv3LQE=E$aF(+rei3kxJ]1.rO-vUa',oA jp7Rr,y( 2_SOX1;15cWy9Xd9nmN l]kRr3ua-uZ}&R)'_qеЉN9UG7S'o_KKRc`Jx얲SFoajb'D͌jFuOrK`"H`4F&Ⳍm(mOfkT^Tt؆fṾe/- iT)E/AYK_/TβzqfYj8vl HYC[jo#y]3[{ cg_#8H5@N=Rޚ,o0Ա.: 28RrL)@uNK\1DQ`j}͸#m\38eV֠dkmW oD[j1*6X8SJk[-l:I(t$$vmb$(/x- ՏF:΂KQ#)[ҲU![IO$H}-]Eh#%+.a/c8W>wHԙDAR%*U0S4UT'Uq;@Vv%@E1ƣVJUV FY3XH?Bo5^M?TnԀ;t`QJjr`}\iZh:nŶvh]}j~OL`1u#s?kkc{JTוO'vrtwU:ǷBR"D|# utw֣Xn(, q?Y,o,BΕ.XėQ05Q)TQÃT!~R—g|i,|hubE" _ |ǟX@Eg."?/օNq}~d|@̶o9vz ,)|P7gٞ 1ƺF`(J${&=%'(={Q+\rFDL78#W¿ ] ;'|p& f-Ε6 2ql3-_'潿χ0M`}6303y-pzanH`6L?-Ds03 l ܌ NGΓ Ð kכ8O! %ebٛo* v|6 Iׄ,EY`1d?3Ot0y4=S%/-c>|E8O˃%&wRxPs߬VN(z.Er>&,a`?k 31$Pl\X׽D"rSTZtSӞr|jᔥ-ۄ~kO3ӄs>H:)omG`Q,oto[̞1s8fη-**P0iʷ-bMpFM űKe+pᛷ z3Ga̚{&f>9;G*],3>ԝZSv,1)'Ȯ4Q:MX88AōƎsԖrOТ[_V%UU ({6x1ar A"| .U|i7 5-6Yq3ՔK,"w P@K$ޅ㫠>6 a\e*Rt\5m\߮4`-ٶ q'݀}6٤?&Nh(Ya3*'`Ԑ`٩^>qp;jwR/6ͻ|H(& (OLJPGckzywFap~&1J៑# *Kn8)R<9B%+6 8^NJu-}5CQ걗 =B?Hf/| +U˰>;$>oC~oo4k (WbZG@LBߜ:{ACb'[xcZ%ha_vP儮F!y-XALJAr.azm=:f0CNy k5큳FꁣYGi2{X gK`uOeR'o;wpcYы C.=Gd"\c丰19~88{A52L)V"}0FnF˾Mۀ՞wjȖ!qQ|VqêT1}a'sտi 5͉33֧&z< ڙ`gޠRk ?)u`6w hg=D2 wl_ISn<5uÁ1x>?/tUL,vsdWk.s9xo'>ccc-Ӿp4 Ī齍rϰ2kk\,1$A{zaB1S >XlBK W'άK;N+{4HXjKB5jhdu 0#L|CD:%nG;0q8or:~b#+4Y(r&\XM(M`"{P̳+YF|uJS[*'Z2F=Ԍr _q?HTԝf~"[W2a'BDhi&.qh(}ycm>Q] .9D"i.E0\!E~vC~9~]## r(aȲV{) -l*SڈTH ٽ# "n]IbkuaC [BVM4K[ILu;EW |&HQA-Ar)eӈ,'SBATe z0Ay;\D3$ u-8l X“ՠ\Jj`4_`!}HS'$+y`G.)@ї P'{!=KӼs(dV")T@!yx%A(^$d$EePZ8龽jn6hJm\VH=]Qk2'M܁Dz7gyqTT3zGRF fwX$F>?-$H'%hiF,'ڽ>EǖȂD'&1~Kp/M ᘇjʍP;m8Կ*t/Nқr `\K$1 ! b= ~u;*AwJGOj G z`5л%s[ MM\vBK`=XZ mВ&ηY\5 3"Qsx*C(C跣Fm9Sh~US;J؂V3Ŧ Üca|j !l;B1:yLwq'rGu=rǂ Ƭq܊ ]A5v&Ou*㠩 =)E$秄DMJmNYj1*g?EkU@«=m*{GGn(H⨦7{6;jX-ulD.X]Zesi.F=j?P&ԯ*o5Vg]5(<-۠Q'dtt*LBCGsd$qY; "bMU$pIĒAL-$ϻcXDH"dzH50OKq|X^Q'3y ʴes9UcCQnQlUb&;Fӝ!}9<,K0Irx5cg*W+b@qɁTM[@X6l۴"K\ʎ[T jv3LQE=E$aF(+rei3kxJ]1.rOj-vUa',o% jp7RrW,y( 2_SOX1;15cWy9Xd9nmN l]kRr3ua-uZ}&R)'_qеЉN9UG7S'o_KKRc`Jx얲SFoajb'D͌jFuOrK`"H`4F&Ⳍm(mOfkT^Tt؆fṾe/- iT)E/AYK_/TβzqfYj8vl HYC[jo#y]3[{ cg_#8H5@N=Rޚ,o0Ա.: 28RrL)@uNK\1DQ`j}͸#m\38eV֠dkmW oD[j1*6X8SJk[-l:I(t$$vmb$(/x- ՏF:΂KQ#)[ҲU![IO$H}-]Eh#%+.a/c8W>wHԙDAR%*U0S4UT'Uq;@Vv%@E1ƣVJUV FY3XH?Bo5^M?TnԀ;t`QJjr`}\iZh:nŶvh]}j~OL`䘺9џ5ñ}=xf%ʧ^_^Xz9*[uXYx)":ano;Q,E7Ig ,7Swp J,V˃(H(q*c(Ij?) ׳X4t>~"|ůMccyOx_^x t"XcORx޸>x?|~_S> f[Sݷ;= >Cx(lτc tPo0m\ %=HԽ(wgh9so#pof?_H E|gfn|nt}>i83ʊp8/D_CǦ0>әQk=0C7G0Tt9M W6CynFk' #NօEa{݃Mw27 S>kBSD",0؃2TyYԙ̧v l4©[&fG-N ot3>e>L0E!~kT"K*'kL ynYFi4/ԁ9i/t Ɍ?9~[>pUmB ߵiB9sd$ţz_o7:ȷ-f9qX_Aj(A4_1m8#&^إ8T͙0hf=Tc|3A}#.yGtkN);AڔdW(&{  FFyc jK9'hQv/QȪ*քp=G܊ w*Tqr,8 j%lv; lBS|( Y_F VyUDOiD 02)CGXuk'{* UW@+t]M/W+x ^ p|")M8.&Z:nТAAs4% :6.oW[l[F؆8La铀n>|lzLOl҂F'QqkZ_YAjHToK8OU5Kۗ`t@ $L}|e|'S| d1PCG_׻S^HGRZH|8xAx B7Z)pHC`ؿ]b{U'%кVþCLĚ(KZwC]`3n*eXc!hvJ5c+ #O&oSVT=r1C叭@1w]Nz-j0;rBW<K 9j6 Ces!|#7eOmj;5dָJ>u8HaU־094ꅚę\Sh{k=ńL0`3o펂qA)5:0;s7@Eo<}z_*|? ;qw9FG_s5O^巓 ]ӱti_V8x_bF9U{gX]\ܵ5[  }xeUoD9|Ae [x։빇γJ~(fU6~GPKPB }aJ܇:5]M2HGcQGf:.՚q7C=@5_WBf5(B2k,AwFRQHX )\^iDQR*MOy)];mIQ&xݔ=^~,ywEQi( aCOBH4^ȶI"Q0d4u#\p$䓒ʳ6Sh GDn)ȴHiydfkqdl 6F29 ŊQh ɾ@,<_%ظ-YӽEX&-m!-MR[!FaaB*DVm8l)䠐nv"WEm#7CdT`_)'@_A==3n);@>~p/M ᘋjʍRЪ,н0:Ioɉa; ğ¹F3#9QՒo'RehK^k G y\[~qSK ^ݜۙZ퓮b-Sd؉ky+E^Bpfp ^4jpZ@l I!DuqrQE&Q4aI lA\jbSwaα C v0af U>݅NYN5<&WW;Røutߣú޸£cָ zMn厁;O:mqrzTIɑ5qX|~JHD4*IrSV d(ڣ1жw Z"0En(H⨦7{6;jX-ulD.X]Zeh.F%NTC(jW7ǚuxz.@RlUmШ+Wz:z&!!x9lϸ˝y\1q*Oo$abIC r l|J,LZ @$Y$'Rӥt>P}AsS}iQeZR ߪ((*1dɝT |ΐȾGbcU%T$3ȫb18wP k& ,[6mZ%P.@eGTw*SH5u"d0#4 SǙ5 <.fT9'asm5XS)y `nⓌrƯиZZAݪȐ)Wԓ/%%Wh10%IfeOqͲs+yEkP5򶊫}-U[,)a%-O6X$t X aU]Rpq:Fdzk1_BXPKS_y{gǑ-Mi٪'QV$\L>UtZ"ЕIʰZL}; $L hB*)Lh*Qθ +u;Q^S`+J#U,vwUDyiRG&VO7j:(%5[0r`u|hz-4Tb[; 4ytw_[>5?̧w{0'1u#s?kkc{JTוO'vrtwU:ǷBR"D|# utw֣Xn(, q?Y,o,BΕ.XėQ05Q)TQÃT!~R—g|i,|hubE" _ |ǟX@Eg."?/օNq}~d|@̶o9vz ,)|P7gٞ 1ƺF`(J${&=%'(={Q+\rFDL78#W¿ ] ;'|p& f-Ε6 2ql3-_'潿χ0M`}6303y-pzanH`6L?-Ds03 l ܌ NGΓ Ð kכ8O! %ebٛo* v|6 Iׄ,EY`1d?3Ot0y4=S%/-c>|E8O˃%&wRxPs߬VN(z.Er>&,a`?k 31$Pl\X׽D"rSTZtSӞr|jᔥ-ۄ~kO3ӄs>H:)omG`Q,oto[̞1s8fη-**P0iʷ-bMpFM űKe+pᛷ z3Ga̚{&f>9;G*],3>ԝZSv,1)'Ȯ4Q:MX88AōƎsԖrOТ[_V%UU ({6x1ar A"| .U|i7 5-6Yq3ՔK,"w P@K$ޅ㫠>6 a\e*Rt\5m\߮4`-ٶ q'݀}6٤?&Nh(Ya3*'`Ԑ`٩^>qp;jwR/6ͻ|H(& (OLJPGckzywFap~&1J៑# *Kn8)R<9B%+6 8^NJu-}5CQ걗 =B?Hf/| +U˰>;$>oC~oo4k (WbZG@LBߜ:{ACb'[xcZ%ha_vP儮F!y-XALJAr.azm=:f0CNy k5큳FꁣYGi2{X gK`uOeR'o;wpcYы C.=Gd"\c丰19~88{A52L)V"}0FnF˾Mۀ՞wjȖ!qQ|VqêT1}a'sտi 5͉33֧&z< ڙ`gޠRk ?)u`6w hg=D2 wl_ISn<5uÁ1x>?/tUL,vsdWk.s9xo'>ccc-Ӿp4 Ī齍rϰ2kk\,1fe˪sR1)k,tys˥g>P005̪lʑxN@Y58itT q"Nh] 08rޡ: ~b$15YXVm60JE(,OK^պZo0MCJ;\WOfSE4k_ʠ>锖'~_l37qߵC8檹L6쿮HdBCϑAD"4.McJ@Hn:e!DtPO.BtqVdY׽Ֆ =?QeJÑɐr#a8:bĭ _z0"j\{f,U32ȳabQpvi ۖveP~"ղ(n=VRo3 Maeӏ,4'SAUe(¼Z1kDywbƻDB]Kb!ۧ$oO|}(FRp@!k),DaU)>tj7.d20:!uK]5do0{jjP$J)d0I/2X,j5R 6NUpETjHq3;mIQ&xݔ=^~,{wG8*i=m #, #& sDEHh,U#Z`T^#i&T-HEohzl<G92- ḌDC؈е82#hcb(jKJ d_sHUE ίslܖĬYVib"GcČ$6I%n;rapXtyۡ m-N골 x`]<'"J9шr wcO)E>S_e[ýL7'c.*7CRҪLн 0:Ioʉ&qD ¹F3#9Qբo'Re6h^k G z`[KK~]Z핶˂-ASfؑk+E^C˙pf!ZS_xԨiB.aq;J4q:yLwq'rGu=rǂ ƬqYA5v&Ou*㠩 驌#k2ek&O (&rڜV%UT:t+)Z2^h[PC<B-mP7sB$qTӛ=5GӺJrMDx.hƲ?޹4}yYNTC(jW7h]cv Z`z|@+2aqq]JQOG$$1|O8AMu 2,&TI ב$L,i(DQ܂O=VITk9!Xa$r<+\Djy1gY/u2o0-11Lk^6C[56V%l2*1Xo>W9Hq̣ʒ!wWS=6{y`B,^ >!yMdUekfM+b% 5Ne n7U$CQLf"Wza8F*'䯆bWv"lV- w|z*%/w̒Q")5cyoS3vٙgME蔚;u9)u,g?þQ tc%vVIVJYF9Xh|i-u-uSNn p)ɔ+쒳"-vt6$n)haeF-w%pQ(qRV BPn,`d] չ-l}w-j]!pl~2:%A;->*:(W1w".#xkbN5,7=jzZ؜/%"[Rl֬z[nX2]H ݾq[[Sj:=?!eRUA]\Ji3K#ɔ2/vikq"K<Nm;ѷ}mcSvxFIJl͸MjmhKUm=FƲ  g qXIzm˺I}l5:t,0骃.D8#2I=݄|۵/!JK(^Kzѩ]RvHblUVғ( e+. {*WwKW-tH ʤKeX-UϾuulQeuT J̔MU&UD(g1t PQ(@R|Ux|n`q;w>*O<4[zM@WO+է5l-9`$ӂQg-^ Uǭؖ`M|1O iLȜZoؾi|u //_]`-D,P,{0]($3H˛)ƒ;P8s +A$Lms8A$`~ty5qY,_: Z|]X~W&_11'vbV//o<|:|Y ϋu@')o\Ye?)[K t#u/QűݲԴ瀶\.sτ9"l,/R# vpwa*heϧ ub#½_tH~R @*«PQ.\&@+XQ{OT r&^~i-Tas7hQCzDMѠ9yRD e+-X ~-#lCI@7`>6w=l'M6ia8Zo5 yxV،J- /, 5$Xv*ץ]K }nv:_  I>>2>Ó)>gx2TbZoރv)Q$#wX-$o>_tàIjRgjD,ZM{쵱Qq/ ]>ӟgḻ#٣}\/ɿ؄X:|˴/y+M/jzo*g3.L-v>K YٷyT>EL vfV,tyG's˥h>P015LlďʑxN@Y58ytT q"Nh7] 08r⡒:~b $1%5YXVmF0JE(,OK^ֺZo0MCJ;\WOfSE4k_ʠ>锖'~_m37qߵC8檹N6쿮HdBCϑAD"4.WMcR@Hn:e!DvPO.BtqVdYؽՖ =?QeJÑɐr#a8:b- _z0"j\{f,U32ȳabQp~i+ۖveP~"ղ(nMVRPh +~`9 **Cw-Պm${ PpגXDU&1ЩhA<YD_22Bf  gx)dV")T@!yh&I(~$xg$EePZ8龽jn6hJmSq]ƴһ Ӯk51CqB0؄QdH`03*ZC!w080NڽYNU<&Wg;RøvtߣccָY Vʬ;O:mqr{ŒJdW[*Q6M*~9=fJ:5UE h Ŀm󇺙 h9bU}m"cuqFk5eε˻؏uBTS9VG<K&2gջÔ6ؿ\U$ ȲI<.wDZE^I ב$L,i(DQ܂O$=VITk9!Xa$r<+\Djy13gYAd/d&TaZbbTּl.T÷jl(-J?dr'Ub~C4(}3$"*dIcU%T$3ȫb18wP k& ,[6mZ%P.@eGTw*SH5w"d0#4 SǙ5 <.fT9'asFm5XS)y+cnⓌrƯиZZAݪȐ)Wԓ/%%Wh10%IfeOqͲs+yEkP5򶊫}-U[,)a%-O6X$t X aU]Rpq:Fdzk1_BXPKS_y{gǑ-Mi٪'QV$\L>UtZ"ЕIʰZL}; $L hB*)Lh*Qθ +u;Q^S`+J#U,vwUDyiRG&a/EC6˥xK.ﴸTuhCAVE|QO YȜZoؾi|u //_]`-D,P,{0]($3H˛)ƒ;P8s +A$Lms8A$`~ty5qY,_: Z|]X~W&_11'vbV//o<|:|Y ϋu@')o\Ye?)[K t#u/QűݲԴ瀶\.sτ9"l,/R# vpwa*heϧ ub#½_tH~R @*«PQ.\&@+XQ{OT r&^~i-Tas7hQCzDMѠ9yRD e+-X ~-#lCI@7`>6w=l'M6ia8Zo5 yxV،J- /, 5$Xv*ץ]K }nv:_  I>>2>Ó)>gx2TbZoރv)Q$#wX-$o>_tàIjRgjD,ZM{쵱Qq/ ]>ӟgḻ#٣}\/ɿ؄X:|˴/y+M/jzo*g3.L-v>K Y٪yT>5Lh2zZ-8]haAayriiO; L )?rd(P7F 5r.2- fHڭh L $_oN?fHA a%MrDGMŰ'RdKi}%(’W.az[i:LSPENqŤm<6Uz'nDcD;WE2eO:e+#ĉDM\w-P+j/o,'*+!%;Аsd>-%E4DӘP8NY(/¯xDc]AEYs/E%1sBObOTpuE2@iqL᱈^KՌ4,eزXnh$DjڂdԤ_Ht,ʧbboe"ʦ+i]dP}yKbb>")B$*ܵ$"Q}*`Iz Oׇri* p@!k),Daն)>tj7.droi2ї P'{!=Tu(dV")T@!yh&I(~$xg$EePZ8龽jn6hJmZKNVA#}QF4[+fqC4%aiȪsb˜5;FZ6c # oIȔc ctc c c c0SҸ74,rH7i+6ς;6C|8+D#503x?A - go ^ن{!oB_ l VT.0oU6^pDsXxԖC m9 ! b ~ֈ;*3K[4aKVh@pC,ZZ!ZN>%g 2y8gGȵ^#LBPEH54Cq&7[匚/  S'Y!lA]j-cbSwaα 0af U>BxZA8fݣ:Wu\!H ~jr; LYFMYZk< tUAS!(kCR *I4_ǧD JmNoY*jA*"W{4SZ_叶CQQMol4uZN*6ʉ6]ⱺ05˲xgF3zP$ԯ*Wź: LEYq0hQ*#,׳z:z&!!x9lϸl˝V)Wyxu$& KJc80E`ۓgU5/!rbO6IP,5h ÷ mYhG%1| &>U7CخzlL%*|X ( 490|BzֆpV`c K"P~k6RMn)H|$eE,8MQfO <UN_ Ů*D؜-N[;A ?TJ^%E^aCSk +`5>fJ3:>,5͹-)5Aw"rz RX~}.1̺:Y[$+4:q)u膷c u)zE9Zr LIRVQc-L0[cQ(qRͨV "Pn ,B`^ d]v Ź-lv-|j]Ӌl}2y%A;-=*:(W1w"#%(k+Wj4Y6{[ow2.,K-{~ǎ-)6khk\ZWpAm$okfkOalPkm ݮq[[S} f:M>!eRUA]\Ji2Ȕ.ikq"H<LmϷ}mcSvxGJvl͸]jmhKUm=FƲ g q|XIzwmX6gۄ$O{:m=36'igwLb6x7$~$!iq% t޶[<cuQ&pgc1 HWt)t$$vmb$(/x-ՏAv9΃KQ#)[ֲU![iO"D}-]Eh#Iΰ^\} $LduT J̔M%*ZDJ3n ZTc<5V是O0Rҏm Ltx5itܡ"݂QseCkrlVY'g^zrlrkO@[RdhGxrv^ʧ__\Xz9Utn' `gT8 ׉usYbλ|0ON?o|q3Cxtg pt|~ySssĜ!}JX?I|~?e/^:^?_>O~W_}'vV/.o<}:| Hp 逗~ q׌ @9(=7a_JE~ Bo i})nuC>iycS̕. 2\;+_]8Moas:!'q-p߹qnH_  PmaV҉g(7a&v,^幝(7-_[]=!n0C]n8U8*']|"~{(cI@lf &Weeů4 er{qY W+tXxC~RQHRWG}0N XtG(~>랡z c9X *-らmiDy$+ U\ \+. ѫC\g 9Eq:2}{0cۂe EMׄܡ6)g%~tV}YEy*:Sq_afh#]tBw^pO$}cybb`c Qb"Q/FbbɽZ"8v,~~s߾s5xM_pAJy3TFͬY*pd/gCbѥ2cMݩ5bArJӆwSU8ȨX`5 ~m)D-.Z%jUYIŚ"q': D-U'[Q?u|Z^Zy Xr,eSՔK,"w P3@K$^㫨>#6 7a\E*2ǎ>܃Cz0okI.pS~7v>J=P{x .q; C J2H;@;Xx4;1ʕɉC`DZo.VTr1GH5}Mz#- j8:rBW<3 (9j-cYs!|FEkr_ S;J&I>S_YýD7'cbHK2A$)' Ƙ;`68h&|44<51ZDm}-z[hֱaCo ]pOCzqe!Yiȅn'4 {EE] -i[yb(̘ˀFMIk6{~{H!DuqrQE) aJ][5굚y#sƣ8!pXplc 0CB!䡓m7i]ӻM0b\>Qb-GX05n0t}29N[ ]By{ZXINYzI4;*С[O3ZU$j@zd h?IfFsG/khSѥ3ZQ;Xe֨ZUʤUͱ:ZWg]5(<-4L-AB\-Գѫ0 i  H$qY; "bMUpIĒ r l|J,M^ @$Y$'JӅt>P}YwS}*y\oPT[[X|N↢4(}3$rXG%1| &>U7CzlL%*bX(}090|BzekfM+bv@ߚ}*T3aj _(G& 3BYi9άQiu4I<ūaUSJ%co^O)y(J*_SOX1V;15c'ݨwfHjr`}\Zh:nŶ`S1<9P֓;ߜog['C{l>kk熶ǕW>st?I.( lȑ7(R֑C,k("o&/x̷IL83/hR*+sT }u7CF8pUZpǽS~"k5GawS%,YFQ$|_[iȒ*WzZc(z[?EQdyo. Ct-l `#{G܊ٯB#o4̖ d/ڰ \fac6(4E 9Zj$`՝:n_ELyQP /R!P|pyt>uz=p /`S$9x_|{|"đ&^~,Tqs7jQCzDMѨ9x26lJ7iqQh$8q3*`̐`ѩ^>3;>ϔ_N@A epI{3|<hh x70w3n(CwP-$/ߞ_|mIjRj|G* ny R9b+6 8F^NJu-C3EQ걗 =Bû7Ov͌Y_ Va}v@} £i+6Q,}HN~hus<}sC]9pD⭙nigPY|uA$9aDɹUmy-?X9B.մG^XclGfrk/-٦ ?*R{/[qcYыC.<d" pA:2 "t!a#ciqF1f v2=/!I$Y0pق҇E(>{RK 4;pAŻ `KUw;V˷X;y>l~u~_*A8AT~w}L?ބeʻq3۵`焝ek\T%BxU<*5Ckk_GCowB؛~\hk=Լ`Ӡ{A)5F)v`6w-hg'2 \3\ICv8M˃18/tU|c,vkd?Ggg/{9xo'9cb-Ӿp4 Ī鍋r7eNÙk>6/ijǕMz'IĄc1S5J>XnRK ['ΰ/?0`!}``jW#CYܨ58ȉtT sNh7] 08r桖9H bkjV >Xm*a2PL?l?",y]2hIa.*ꅕv+&mqܯ#(8!P/u'#Bic]d+Z&SZrB(Txe7rXՍDgue2$d| ڧ$qhGr)bŃ|Bu9+"RP[ <'$6DW) G_W&C(FvJpuĈ[>7`e eiﵚQfdgQÖ tF#y' UvgP~"ղn]VROh +~`9 **Cw-Պ}${KQpגXDe&1<)կX(DᦏB*RXns2|nt]4 e R,eXF3NCzfJPȬER5BLTb-$xgMeШZ8龽jn6hTJm݁.$HɧhiF(ڝCEǖȒD'Ц1hނ fl-Lꯨ fn&$ǨcMN4B2j0?~gCX$fBp 5k&p`CUI}{!/ 즜h+c"o8h&|44<51ZDeF+y4ذ.!xy[84Bڎm=I.bk+EQ/C 8 'F%^6jvO[]%m8k}GˍrFD3*3<lA]jbwa J}.0af u>݅NMY7No:>Y(:RøurG܎Ccָ1Mn;O:ms0t9mmGzbm$k@|~$$B6M:ƵgJeت4UEh HHz_C Qnl4wFA*>*6]걺8eSj]fz:Qe L_uk֭N1k-KQyV[~A&N!.^NU4s,lϸ˝y1q*ORoĤabID9Ss 6>oX%&Q@bi  ABƤk:em(־WL©>Tbbtּl.T÷jl(-J,>dr'UbqCQo>;QHq̣ΒڡpWS=6{y`B,v >!yMtUa3ܦ1XUIj;BoS@0E5P}#B4g(c]P$E1[N)oA jp7R Ô߆Tn?>NjۮEemh>n"Z``N)*T}($Swe-}B~r5ңSc4;ev'ͪҲw8vlHYC[jo#y]3[{cg_#8H5@N=%Rޚ۔7EkXwH^RKQu9r)94K vʺ,S%Aĉ"F\0>feOqͲs+yEkP5㊶}-S[,iaږucN:o,0骃.E"\n=Zė%%2W^ypi;q$1E`Z*d+ITӃtݡmt22s볯~G]DiA*!T邙dREHTrYAKbVUV FY3بH?G&:>TnԀ;t`Q$5[0 `ubhz-4Tb[; 4?ozSk` durdraw-0.28.0/durdraw/help/durhelp-256-page1.dur000066400000000000000000000061231470476122500213470ustar00rootroot00000000000000ddurhelp-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.28.0/durdraw/help/durhelp-256-page2.dur000066400000000000000000000067301470476122500213540ustar00rootroot00000000000000ddurhelp-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.28.0/durdraw/help/durhelp-256.dur000066400000000000000000000043341470476122500203560ustar00rootroot000000000000001Addurhelp-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.28.0/durdraw/help/durhelp.dur000066400000000000000000000050711470476122500200430ustar00rootroot00000000000000;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.28.0/durdraw/help/inferno.dur000066400000000000000000001202771470476122500200460ustar00rootroot00000000000000fmenu11.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.28.0/durdraw/main.py000066400000000000000000000262661470476122500162430ustar00rootroot00000000000000# Part of Durdraw - https://github.com/cmang/durdraw # main() entry point import argparse import configparser import curses import os import sys import time import pathlib from durdraw.durdraw_appstate import AppState from durdraw.durdraw_ui_curses import UserInterface as UI_Curses from durdraw.durdraw_options import Options 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.") def main(fetch_args=None): DUR_VER = '0.28.0' 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 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") ui.verySafeQuit() ui.refresh() ui.mainLoop() if __name__ == "__main__": main() durdraw-0.28.0/durdraw/neofetcher.py000066400000000000000000000036701470476122500174330ustar00rootroot00000000000000# Run Neofetch. Extract data from it. Return a dict containing neofetch data. import pathlib import subprocess neo_keys = ['OS', 'Host', 'Kernel', 'Uptime', 'Packages', 'Shell', 'Resolution', 'DE', 'WM', 'WM Theme', 'Terminal', 'Terminal Font', 'CPU', 'GPU', 'Memory'] def isAppAvail(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 fetcher_available_old(): # old because it looks internally the local path instead of global path """ Returne True if Neofetch was able to run successfully. """ try: neofetch_path = pathlib.Path(__file__).parent.joinpath("neofetch/neofetch") subprocess.run([neofetch_path, '--stdout']) return True except: return False def fetcher_available(): return isAppAvail("neofetch") 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 #neofetch_path = pathlib.Path(__file__).parent.joinpath("neofetch/neofetch") neofetch_path = "neofetch" # obviously not a full path. just the executable run from the $PATH neofetch_output = subprocess.check_output([neofetch_path, '--stdout']).decode() neofetch_lines = neofetch_output.split('\n')[2:] # Parse the neofetch output into neofetch_results{} for line in neofetch_lines: if line == '': break try: key = line.split(': ')[0].strip() value = line.split(': ')[1].strip() except: break if key in neo_keys: neofetch_results[key] = value return neofetch_results if __name__ == "__main__": results = run() print(results) durdraw-0.28.0/durdraw/plugins/000077500000000000000000000000001470476122500164125ustar00rootroot00000000000000durdraw-0.28.0/durdraw/plugins/bounce_movie.py000066400000000000000000000016771470476122500214510ustar00rootroot00000000000000# 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.28.0/durdraw/plugins/repeat_movie.py000066400000000000000000000012141470476122500214410ustar00rootroot00000000000000# 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.28.0/durdraw/plugins/reverse_movie.py000066400000000000000000000007671470476122500216500ustar00rootroot00000000000000# 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.28.0/durdraw/themes/000077500000000000000000000000001470476122500162165ustar00rootroot00000000000000durdraw-0.28.0/durdraw/themes/default.dtheme.ini000066400000000000000000000015261470476122500216140ustar00rootroot00000000000000; 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.28.0/durdraw/themes/mutedchill-16.dtheme.ini000066400000000000000000000012101470476122500225340ustar00rootroot00000000000000; 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.28.0/durdraw/themes/mutedform-256.dtheme.ini000066400000000000000000000013721470476122500225030ustar00rootroot00000000000000; 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.28.0/durdraw/themes/poopy-256.dtheme.ini000066400000000000000000000010571470476122500216470ustar00rootroot00000000000000; 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.28.0/durdraw/themes/purpledrank-16.dtheme.ini000066400000000000000000000011241470476122500227350ustar00rootroot00000000000000; 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.28.0/durfetch000077500000000000000000000005231470476122500150130ustar00rootroot00000000000000#!/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.28.0/durformat.md000066400000000000000000000142721470476122500156140ustar00rootroot00000000000000Durdraw 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.28.0/examples/000077500000000000000000000000001470476122500150775ustar00rootroot00000000000000durdraw-0.28.0/examples/cm-doge.asc000066400000000000000000000020531470476122500171020ustar00rootroot00000000000000 ;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.28.0/examples/cm-doge.dur000066400000000000000000000020631470476122500171270ustar00rootroot00000000000000ˮ`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.28.0/examples/cm-eye.dur000066400000000000000000000111201470476122500167650ustar00rootroot000000000000007ldcm-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.28.0/examples/dogeblink.dur000066400000000000000000000041561470476122500175570ustar00rootroot00000000000000`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.28.0/examples/dopehax.dur000066400000000000000000000076251470476122500172550ustar00rootroot00000000000000`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.28.0/examples/indyz-dopehax-256.dur000066400000000000000000000075231470476122500207170ustar00rootroot00000000000000edopehax8.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.28.0/examples/indyz-linuxfire.dur000066400000000000000000000041541470476122500207570ustar00rootroot00000000000000gdlinuxfire3.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.28.0/examples/indyz-xmas.dur000066400000000000000000000160001470476122500177130ustar00rootroot00000000000000 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.28.0/examples/mane.dur000066400000000000000000000015301470476122500165320ustar00rootroot00000000000000cmane.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.28.0/examples/rain.dur000066400000000000000000000014721470476122500165500ustar00rootroot00000000000000L`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