pax_global_header00006660000000000000000000000064146311375360014523gustar00rootroot0000000000000052 comment=d64bfad6d908503959f59b399d96cef0ac5f7bfe durdraw-0.27.1/000077500000000000000000000000001463113753600132625ustar00rootroot00000000000000durdraw-0.27.1/.dockerignore000066400000000000000000000000051463113753600157310ustar00rootroot00000000000000.git durdraw-0.27.1/.github/000077500000000000000000000000001463113753600146225ustar00rootroot00000000000000durdraw-0.27.1/.github/FUNDING.yml000066400000000000000000000014551463113753600164440ustar00rootroot00000000000000# 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.27.1/CONTRIBUTING.md000066400000000000000000000025121463113753600155130ustar00rootroot00000000000000## 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 try the #durdraw IRC channel on irc.libera.chat. * 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.27.1/Dockerfile000066400000000000000000000002611463113753600152530ustar00rootroot00000000000000# 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.27.1/LICENSE000066400000000000000000000027371463113753600143000ustar00rootroot00000000000000BSD 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.27.1/README.md000066400000000000000000000377611463113753600145570ustar00rootroot00000000000000Durdraw ======= __ __ _| |__ __ _____ __| |_____ _____ __ __ __ / _ | | | __| _ | __| _ | | | |\ /_____|_____|__|__|_____|__|___\____|________| | \_____________________________________________\| v 0.27.1 ![durdraw-0 27 0-demo-3](https://github.com/cmang/durdraw/assets/261501/35d8dd7c-bd80-4f35-ba88-c39b338843db) ## 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 ``` ## 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 files with: ``` $ durdraw -p filename.dur $ durdraw -p file1.dur file2.dur file3.dur ... ``` Other command-line options:

usage: start-durdraw [-h] [-p PLAY [PLAY ...]] [-q | -w | -x TIMES] [--256color | --16color] [-b]
                     [-W WIDTH] [-H HEIGHT] [-m] [--nomouse] [-A] [-u UNDOSIZE] [-V] [--debug]
                     [filename]

positional arguments:
  filename              .dur or ascii file to load

optional arguments:
  -h, --help            show this help message and exit
  -p PLAY [PLAY ...], --play PLAY [PLAY ...]
                        Just play .dur file or files, then exit
  -q, --quick           Skip startup screen
  -w, --wait            Pause at startup screen
  -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)
  --nomouse             Disable mouse support
  --cursor CURSOR       Cursor mode (block, underscore, or pipe)
  --notheme             Disable theme support
  --theme THEME         Load a custom theme file
  --cp437               Encode extended characters using Code Page 437 (IBM-PC/MS-DOS) encoding
                        instead of Utf-8. (Needs CP437 capable terminal and font)
  --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.
  -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 use the "Esc" (or "Meta") key to access commands: ``` .. Art Editing ..................... : F1-F10 - insert character : : esc-up - next fg color : : esc-down - prev fg color : : esc-right - next bg color (16c) : : esc-left - prev bg color : : esc-/ - insert line : .. Animation ....................... : esc-' - delete line : : esc-k - next frame : : esc-. - insert column : : esc-j - previous frame : : esc-, - delete column : : esc-p - start/stop payback : : esc-] - next character group : : esc-n - clone frame : : esc-[ - previous character group : : esc-N - append empty frame : : esc-S - change character set : : esc-d - delete frame : : esc-y - eyedrop (pick up color) : : esc-D - set frame delay : : esc-l - color character : : esc-+/esc-- - faster/slower : : esc-c - color picker : : esc-R - set playback/edit range : : shift-arrows - select for copy : : esc-g - go to frame # : : esc-K - mark selection : : esc-M - move frame : : esc-v - paste : :..................................: :..................................: .. UI/Misc ......................... .. File Operations ................. : esc-m - main menu : : esc-C - new/clear canvas : : esc-t - mouse tools : : esc-o - open : : esc-z - undo : : esc-s - save : : esc-r - redo : :..................................: : esc-V - view mode : : esc-i - file/canvas info : .. Canvas Size ..................... : esc-I - character inspector : : esc-" - insert line : : tab - focus canvas or colors : : esc-: - delete line : : ctrl-l - redraw screen : : esc-> - insert column : : esc-h - help : : esc-< - delete column : : esc-q - quit : :..................................: :..................................: Prev Next Frame Frame | | Main Frame Speed Frame Play/Edit Mouse First | Play | Last Menu Number | Delay Range Tools Frame | Pause| Frame | | | | | | | | | | | [Menu] F: 1/8 : 8 D: 0.00 R: 1/8 [Move] |< << |> >> >| ``` ## CONFIGURATION You can create a custom startup file where you can set a theme. 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:
; Durdraw 0.20 Configuration File
[Theme]
theme-16: ~/.durdraw/themes/purpledrank-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 ``` ## OTHER TIPS * To use themes, copy durdraw.ini to ~/.durdraw/ and edit it. Durdraw will also check in the current directory for durdraw.ini. * The mouse can be used for moving the cursor (even over SSH) and clicking buttons, if your terminal supports Xterm mouse reporting. In iTerm2 this is under Profiles, Terminal and Terminal Emulation. ## OPTIONAL INSTALLATION For PNG and animated GIF export, 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. ## FAQ #### 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). 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, 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 the "bright" colors. 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. #### 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/T2ErYc7E 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. This software is distributed under the BSD 3-Clause License. See LICENSE file for details. durdraw-0.27.1/durdraw.1000066400000000000000000000057071463113753600150250ustar00rootroot00000000000000.\" 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.27.1/durdraw.ini000066400000000000000000000002301463113753600154260ustar00rootroot00000000000000; Durdraw 0.20.0 Configuration File [Theme] theme-16: ~/.durdraw/themes/mutedchill-16.dtheme.ini theme-256: ~/.durdraw/themes/mutedform-256.dtheme.ini durdraw-0.27.1/durdraw/000077500000000000000000000000001463113753600147325ustar00rootroot00000000000000durdraw-0.27.1/durdraw/__init__.py000066400000000000000000000000001463113753600170310ustar00rootroot00000000000000durdraw-0.27.1/durdraw/charsets/000077500000000000000000000000001463113753600165465ustar00rootroot00000000000000durdraw-0.27.1/durdraw/charsets/unicode-groups.xml000066400000000000000000000553021463113753600222400ustar00rootroot00000000000000 durdraw-0.27.1/durdraw/durdraw_ansiparse.py000066400000000000000000000500731463113753600210260ustar00rootroot00000000000000#!/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 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=80) 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=80) #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.27.1/durdraw/durdraw_appstate.py000066400000000000000000000311421463113753600206560ustar00rootroot00000000000000import configparser import curses import gzip import os import pdb import pickle import subprocess import sys 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): # User friendly defeaults self.quickStart = False self.showStartupScreen = True 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.maxColors = 256 self.iceColors = False 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.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.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.ansiLove = self.isAppAvail("ansilove") self.PIL = self.checkForPIL() 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 self.showBgColorPicker = False # until BG colors work in 256 color mode. (ncurses 5 color pair limits) # 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 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 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 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/") internal_theme_file_list = glob.glob(f"{internal_theme_path}/*.dtheme.ini") pass 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 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.27.1/durdraw/durdraw_charsets.py000066400000000000000000000103131463113753600206460ustar00rootroot00000000000000# 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.27.1/durdraw/durdraw_color_curses.py000066400000000000000000001301051463113753600215360ustar00rootroot00000000000000import 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.27.1/durdraw/durdraw_file.py000066400000000000000000000321101463113753600177500ustar00rootroot00000000000000# 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.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.27.1/durdraw/durdraw_gui_manager.py000066400000000000000000000013721463113753600213150ustar00rootroot00000000000000# 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.27.1/durdraw/durdraw_gui_manager_curses.py000066400000000000000000000013431463113753600226770ustar00rootroot00000000000000# 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.27.1/durdraw/durdraw_help.py000066400000000000000000000002461463113753600177660ustar00rootroot00000000000000from importlib.resources import files def get_resource_path(module: str, name: str) -> str: """Load a resource file.""" return files(module).joinpath(name) durdraw-0.27.1/durdraw/durdraw_movie.py000066400000000000000000000361471463113753600201660ustar00rootroot00000000000000from 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 self.sizeY = opts.sizeY self.opts = opts self.frames = [] 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 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(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 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.27.1/durdraw/durdraw_options.py000066400000000000000000000012761463113753600205350ustar00rootroot00000000000000import 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.27.1/durdraw/durdraw_sauce.py000066400000000000000000000054071463113753600201420ustar00rootroot00000000000000#!/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 # 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.27.1/durdraw/durdraw_ui_curses.py000066400000000000000000010721661463113753600210520ustar00rootroot00000000000000# 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 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.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 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() else: self.appState.colorMode = "16" self.appState.maxColors = 16 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.noecho() curses.raw() 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.loadHelpFile(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.loadHelpFile(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 initMouse(self): 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): title = f"Durdraw - {title}" sys.stdout.write(f"\x1b]2;{title}\x07") 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: selt.nofiy("There was an error setting the color. 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' """ 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.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() 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, str, 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: if self.appState.charEncoding == 'utf-8': self.stdscr.addstr(y, x, str.encode('utf-8')) else: self.stdscr.addstr(y, x, str) except curses.error: self.testWindowSize() else: try: if self.appState.charEncoding == 'utf-8': self.stdscr.addstr(y, x, str.encode('utf-8'), attr) else: self.stdscr.addstr(y, x, str, attr) except curses.error: self.testWindowSize() def move(self, y, x): realLine = y - self.appState.topLine try: self.stdscr.move(realLine, x) except curses.error: self.testWindowSize() def notify(self, message, pause=False): 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(2500) 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: 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 < 80: try: self.addstr(0, 0, "Terminal is too small for the UI.") self.addstr(1, 0, "Please enlarge to 80 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=1, bg=0, frange=self.appState.playbackRange) else: self.insertChar(ord(' '), fg=1, bg=0) self.xy[1] = self.xy[1] - 1 def deleteKeyPop(self, frange=None): 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([1,0]) # 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([1,0]) # 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.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.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 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 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.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 - 3 < helpMov.sizeY - 1: # 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 + 2 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 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 - 3 < helpMov.sizeY - 1: # 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 startPlaying(self): """ 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. """ self.commandMode = False 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 if self.appState.viewModeShowInfo: self.showFileInformation() 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 not self.appState.playOnlyMode: self.drawStatusBar() self.move(self.xy[0], self.xy[1] - 1) # reposition cursor self.stdscr.refresh() c = self.stdscr.getch() if c == 27: self.metaKey = 1 self.pressingButton = False if self.pushingToClip: self.pushingToClip = False if cursorMode != "Draw" and cursorMode != "Paint": 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() 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 == 48: # alt-/ - insert line self.addLine(frange=self.appState.playbackRange) elif c == 39: # alt-' - erase line self.delLine(frange=self.appState.playbackRange) 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 == 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 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 - 3 < self.mov.sizeY - 1: # 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 + 2 # 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 - previous file pass elif c == curses.KEY_RIGHT: # right - next file pass 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 == curses.KEY_DOWN: if self.appState.topLine + self.realmaxY - 3 < self.mov.sizeY - 1: # wtf? self.appState.topLine += 1 elif c == curses.KEY_UP: 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 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 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 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() elif 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 [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 self.mov.currentFrameNumber == self.appState.playbackRange[1]: self.mov.gotoFrame(self.appState.playbackRange[0]) 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 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 # 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 " % \ 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'] ) 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 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: 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 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) if self.appState.showCharSetButton: self.statusBar.charSetButton.update_real_xy(x = statusBarLineNum + 1) # 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 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)) # 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}" 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.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": 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) # 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.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) #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.opts.sizeX: # cursor is past right edge of the canvas self.xy[1] = self.opts.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 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() 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: # alt-y - Eyedrop 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('l'): # alt-l - color under cursor self.insertColor(fg=self.colorfg, bg=self.colorbg, pushUndo=True) 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() # 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.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() 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() c = None elif c in [ord('3')]: # F3 - insert extended character self.insertChar(self.chMap['f3'], fg=self.colorfg, bg=self.colorbg) self.hardRefresh() c = None elif c in [ord('4')]: # F4 - insert extended character self.insertChar(self.chMap['f4'], fg=self.colorfg, bg=self.colorbg) self.hardRefresh() c = None elif c in [ord('5')]: # F5 - insert extended character self.insertChar(self.chMap['f5'], fg=self.colorfg, bg=self.colorbg) self.hardRefresh() c = None elif c in [ord('6')]: # F6 - insert extended character self.insertChar(self.chMap['f6'], fg=self.colorfg, bg=self.colorbg) self.hardRefresh() c = None elif c in [ord('7')]: # F7 - insert extended character self.insertChar(self.chMap['f7'], fg=self.colorfg, bg=self.colorbg) self.hardRefresh() c = None elif c in [ord('8')]: # F8 - insert extended character self.insertChar(self.chMap['f8'], fg=self.colorfg, bg=self.colorbg) self.hardRefresh() c = None elif c in [ord('9')]: # F9 - insert extended character self.insertChar(self.chMap['f9'], fg=self.colorfg, bg=self.colorbg) self.hardRefresh() c = None elif c in [ord('0')]: # F10 - insert extended character self.insertChar(self.chMap['f10'], fg=self.colorfg, bg=self.colorbg) self.hardRefresh() 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 pdb.set_trace() 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 [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 blank_line = " " * 80 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.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 if mouseState & curses.BUTTON4_PRESSED: # wheel up self.move_cursor_up() elif mouseState & curses.BUTTON5_PRESSED: # wheel down self.move_cursor_down() elif self.appState.cursorMode == "Move": # select mode/move the cursor if self.pressingButton: self.xy[1] = mouseX + 1 # 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 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 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 # set cursor position self.xy[0] = mouseY + self.appState.topLine self.insertColor(fg=self.colorfg, bg=self.colorbg, x=mouseX+1, 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 # 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, y=mouseY + self.appState.topLine, pushUndo=False) elif self.appState.cursorMode == "Eyedrop": # Change the color under the cursor self.eyeDrop(mouseX, mouseY + self.appState.topLine) self.statusBar.setCursorModeMove() self.drawStatusBar() elif self.appState.cursorMode == "Select": # this does not exist lol self.xy[1] = mouseX + 1 # 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 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 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) elif 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": 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.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.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: # normal printable character self.insertChar(c, fg=self.colorfg, bg=self.colorbg) self.appState.renderMouseCursor = False #self.drawStatusBar() if self.appState.viewModeShowInfo: self.showFileInformation() self.refresh(refreshScreen=False) #self.hardRefresh() def selectColorPicker(self): #if self.appState.colorMode == "256": self.appState.colorPickerSelected = True #self.statusBar.colorPicker.handler.showColorPicker() self.statusBar.colorPicker.showFgPicker() #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.showFgPicker() self.appState.colorPickerSelected = False 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): self.statusBar.hide() self.stdscr.clear() old_top_line = self.appState.topLine self.appState.topLine = 0 oldDrawBorders = self.appState.drawBorders # to turn back on when done self.appState.playOnlyMode = True self.startPlaying() # Return to normal when done self.appState.playOnlyMode = False self.appState.topLine = old_top_line 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 self.appState.renderMouseCursor = False def move_cursor_topleft(self): self.appState.topLine = 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 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 self.appState.renderMouseCursor = False 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.renderMouseCursor = False def move_cursor_end(self): self.xy[1] = self.mov.sizeX 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 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 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 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 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 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', '*.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 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 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 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: # if file is selected self.addstr(current_line_number - top_line, 0, 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: self.addstr(current_line_number - top_line, 0, file_list[current_line_number], curses.color_pair(self.appState.theme['promptColor'])) 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'])) 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() 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(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]): # 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 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 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: 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=default_width) self.appState.topLine = 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 not self.appState.playOnlyMode: 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) if fileColorMode == "256" and fileColorMode != self.appState.colorMode: #self.notify(f"Warning: Loading a 256 color file in 16 color mode. Some colors may not be displayed.") if not self.appState.playOnlyMode: 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 not self.appState.playOnlyMode: 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() 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 = "default" 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: self.notify("Error: Some characters were not compatible with this encoding. File not saved.") 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) 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) # 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' try: f.write(string) saved = True except UnicodeEncodeError: self.notify("Error: Some characters were not compatible with this encoding. File not saved.") 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, self.mov.sizeY): for colNum in range(0, frame.sizeX): if not frame.content[lineNum][colNum] in [' ', '']: return lineNum # we found a non-empty character 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, self.mov.sizeY))): if not frame.content[lineNum][colNum] in [' ', '']: return colNum + 1 # we found a non-empty character 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, self.mov.sizeY): if not frame.content[lineNum][colNum] in [' ', '']: return colNum # we found a non-empty character return 1 # blank frame, only save the first line. def savePngFile(self, filename, lastLineNum=None, firstLineNum=None, firstColNum=None, lastColNum=None, font='ansi'): """ Save to ANSI then convert to PNG """ if not self.appState.isAppAvail("ansilove"): # ansilove not found self.notify("Ansilove not found in path. Please find it at https://www.ansilove.org/", pause=True) return False PIL_found = False try: import PIL PIL_found = True except ImportError: PIL_found = False if not PIL_found: self.notify("Error: PNG and GIF export requires the Pillow module for Python.") return False tmpAnsiFileName = filename + '.tmp.ans' # remove this file when done tmpPngFileName = filename + '.tmp.ans.png' # remove this file when done if not self.saveAnsiFile(tmpAnsiFileName, lastLineNum=lastLineNum, firstLineNum=firstLineNum, firstColNum=firstColNum, lastColNum=lastColNum): 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', tmpAnsiFileName, tmpPngFileName], stdout=devnull) else: # amiga font ansiLoveReturn = subprocess.call(['ansilove', 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) 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 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 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.realmaxY - 2 # right above the status line if self.appState.playOnlyMode: lastLineToDraw += 2 if lastLineToDraw > mov.sizeY: lastLineToDraw = mov.sizeY screenLineNum = 0 firstCol = 0 lastCol = min(mov.sizeX, self.appState.realmaxX) # 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.mouse_col + self.appState.brush.sizeY): #brush_line = linenum - self.appState.mouse_line brush_line = linenum - self.appState.mouse_line - self.appState.topLine brush_col = colnum - self.appState.mouse_col 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: 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 if charColor[0] > 8 and charColor[0] <= 16 and self.appState.colorMode == "16": # bright color self.addstr(screenLineNum, colnum, charContent, curses.color_pair(cursesColorPair) | curses.A_BOLD) elif charColor[0] > 7 and charColor[0] <= 15 and self.appState.colorMode == "256": # bright color self.addstr(screenLineNum, colnum, charContent, curses.color_pair(cursesColorPair) | curses.A_BOLD) else: self.addstr(screenLineNum, colnum, 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: self.addstr(screenLineNum, 0, "." * mov.sizeX, 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 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 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, [1,0]) self.mov.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.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() 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, [[1,0]] * self.mov.sizeX) self.mov.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.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""" 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, [1,0]) 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, [1,0]) 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() 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([1,0]) # 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([1,0]) # at the end of each line. self.hardRefresh() def delLine(self, frange=None): """delete current line""" self.undo.push() 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] = [[1,0]] * 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] = [[1,0]] * self.mov.sizeX self.hardRefresh() def addLine(self, frange=None): """Insert new line""" 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], [[1,0]] * 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], [[1,0]] * 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, 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() self.promptPrint("Cut across all frames in playback range (Y/N)? ") askingAboutRange = True 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() self.promptPrint("Delete across all frames in playback range (Y/N)? ") askingAboutRange = True 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() self.promptPrint("Color across all frames in playback range (Y/N)? ") askingAboutRange = True 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() self.promptPrint("Fill across all frames in playback range (Y/N)? ") askingAboutRange = True 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 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 # 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() self.promptPrint("Paste across all frames in playback range (Y/N)? ") askingAboutRange = True 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 = 1 charBg = 0 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.27.1/durdraw/durdraw_ui_widgets.py000066400000000000000000000712131463113753600212030ustar00rootroot00000000000000# 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): """ 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() 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.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("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("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.27.1/durdraw/durdraw_ui_widgets_curses.py000066400000000000000000001315771463113753600226010ustar00rootroot00000000000000# 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. return False 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() 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 else: 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 colorbg == 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: 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: 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: 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: 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: 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): #self.colorPicker.caller.notify(f"showFgPicker") self.showColorPicker(type="fg") 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"): #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]: 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]: 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 == curses.KEY_UP: 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 == curses.KEY_DOWN: if self.colorMode == "256": if color < 16: color += 16 else: color += self.width - 2 if color >= self.totalColors: color = self.totalColors self.colorPicker.caller.setFgColor(color) elif self.colorMode == "16": self.colorPicker.caller.prevBgColor() #color += self.width - 2 self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif c == curses.KEY_HOME: color = 1 self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif c == curses.KEY_END: #color = 255 color = self.totalColors self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif c in [13, curses.KEY_ENTER, 9, 353, 27]: # Return, Accept color. 9=tab, 353=shift-tab, 27 = esc #if not self.appState.stickyColorPicker: if not self.appState.sideBarShowing: self.hide() prompting = False 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 up color += 1 if color >= curses.COLORS: color = 0 #self.colorPicker.caller.colorfg = color self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif mouseState & curses.BUTTON5_PRESSED: # wheel down if color == 0: color = curses.COLORS - 1 else: color -= 1 #self.colorPicker.caller.colorfg = color 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 #clickedCol = mouseX - self.x #clickedLine = mouseY - self.origin #if mouseY < self.origin + self.height: # if self.colorGrid[clickedLine][clickedCol] != 0: # # We're in the grid. Set color # color = self.colorGrid[clickedLine][clickedCol] # self.colorPicker.caller.setFgColor(color) # self.updateFgPicker() 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 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.colorGrid[clickedLine][clickedCol] != 0: 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: 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.27.1/durdraw/durdraw_undo.py000066400000000000000000000062151463113753600200050ustar00rootroot00000000000000from 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.27.1/durdraw/help/000077500000000000000000000000001463113753600156625ustar00rootroot00000000000000durdraw-0.27.1/durdraw/help/durhelp-16-long.dur000066400000000000000000000527641463113753600212400ustar00rootroot00000000000000_fdurhelp-16-long.durms6ְ?wöS")E;ў[/-FOryx}NmsXE|ڦ[|`<ƹ>7%'+۽'ѓ;~C;f8?@;j}boC ԣ\hgvŸ>Ν$2>*,EtJs| (xq[7 ;D0VtA.CӏߌCۂ싒$Cpi /(xbo]O_0)ASSgJA;lg+*'9l%Ť"s]+vpj<', =5 u+ N:f-I) 5CĴ3$.֏'ٺh\IjX /8&#}0e( 6/oSQ:TA@;Enm+-߀WNBr̺]Sf#) {B4em}>'klo% ZElUwT.Prfh9c+{wR$%ЪDbYs%uo%&ݢԤ'&tz}IL31u#TϹ C\>@kA-4AF;mYd#l_;\Oc8H9йS#">g 5M-m-=½3e, =PFcV4 cnEZ/æ!c!ϭ£X[(r}.;B%:2w  TԬ\ރp[E모I?>[Xeߢ (mrlr*/sUρeed. 0N_P"Ї))Y:>O\S ?v/;Q`\sC9p-˾{.Jqk<\bwwѷ,@^_96$U C¹%1\ nʄ!ݳnA\J+ ϐ7A\kjz12Khʲ5n7+>n~ﯙ!r[vw2ϰo!Sq39&JEn5gΐ~R;(W?ż^umW0KvҿfG J½p6^6nDž$>7XToR \3PFѹ囀ZxF_ǜD13e57awYAq҃ȍ&?a/[F((^8:gHu弭&E<($H/HQ, tO$Ziwk7[:"iN?y0pXE}nz.j#X \A-UfBpQ{=MBUtRZuR-e&ݸl鿣To@:3%XVꛨ?!Jv6`^wDSUYO !X"}HgÐ(4Hr'eQ grpHWW.]_ywÚ`Bͽ5XB>\;˄F>>- aKds qI!RlMX>3K?{;ė.x="-g|@ϣv||b~n/ue?FĎsl/G%p!"&Q4p#<!$ 2@(QzdGv%?oCh8??m(c$@= G1+7@ᇐ)ߞXQe|9~Jz ^+>!g\r]Y?JO]gJĔ>w<6_eOs:;(RH lLcpZS=SUé`'`̧`~:nW|jGaOgWoN{Zv7^h6Y ?ԇǷ1c͑vL:^_9; +0}4)xu~~kuksn]e4ă]˾Ymݬต[+v-,.?,kR +Lg6%m--OGEǎw[xWv cT#s̄8Gqm_o0,|c})ϢX'ƃkB0?X&/?]b~&NtPsІK;WMZp gXOwCepnwC>|Qы7ꔽ%Pv6:cgձ79~v{7٦\ahj,Jco%bE.}VnYle5Kʼԩc۳!f=4'qR\$N2}IE\+,KŚR֨&?pы'Gi=2?DhGў:ճ>AMyzneଋQ-6}AZDXl'TILwtZuQ]eNvPT6̷r9~ѯbybe4:1c='|ں ss14gMh7iԤ%i.2fNCY)vIx9$\C"J$sN-:Ҟ M*B SxOjMCv1RXVF}\ +Q hhf(]Yb ޮFVrѕ3k"\Q\[D~t%O @\&ta=̱(|7k(ȚY\|(sN/蠪[繵||]}RdL5G]0W=} '@rWIfS]IXʅЯ=p.i.G`rW|%U/h Ǻܵq=uMa;+rl8IO,q"C&Z䝒V|!=wj&>_瓍H7'&/1iSGCVkׅH\Lz9ݯ@OXA;I^ѭq󼰶M0-|Etsn$ZgF1{jwL41kGgD9d͞D^H%?8no%\4\uf=S:61=5|Y_C)?[)}c|C"g@q'<߹DGN uobEځw6~&r(Պ.(e(qqqh[3}Qr(.M%=Yl5 &%HvpLR)h,tE$ױp6Udk%\P4޶焥{fvECaT_RՉS۬T=y|{3fvf$[헫!IB%ֳb> $&%m*J7HB5(p'h@­Mt履WpJyIRSYk,w$8a]Hygd0~-dAk".2j>JΚ` sw-g uv[DZuH,kԤ[T$4N//"t& .s{>Ԓj16cs59$c07h-%u4hg38K=,@bęp+wKI_` )g:wjDD1u&`M=Gw,7a9@jsЊF9ݚxݭȾ Yx¢v`vO%q4$}1UxTk+csZEϥx'ZDGᮟ%K p8+|t=xg9?C9 kte_t F~5 K􍻻m /!}I k7xHeY .uFNX HgH_ .5oq%}reR}7QGȎs7?C̐~-hAgط)8љfmߢTQޏegH?Gb^jsP/HI ҺٶBHl;_# _^ g/7`\CCf*sHQJHX(A#M@-e i=S߄z~dXw3V!=ת:qS Dp2nAQ7V Zv+MԈW֟e^;0 O ;SUYO !X"}HgÐ(4Hr'eQ6J\8Czw rͻo+uj/10X&4. >8noV["sNK bsn2Fؘ]ti߈F!tx{<i8z'} o擗pKnv{-1<&vdc{y?* 1 4|a Y a@#C<.+AyJ_FQikWG#qWDu>f<__!.'0??/WN(NJ.XVK%x|^GAW&I\?{ATzr:[\V'A<^12/ҟAUF@j` !#M{½5G~ ǷQbUHfSߟiߨNs `>`>0|p˿U;Ro|:?zsӲcBnp &G&>>Ul~ucYh]Ѥ /)xu~~kuksn]e4ă]˾Ymݬต[+v-,.?,kR +Lg6%m--OGEǎw[xWv cT#s̄8Gqm_o0,|c})ϢX'ƃkB0?X&/?]b~&NtPsІK;WMZp gX%wCepnwCQ[UkM3ve[] &6;[:.E_Ҹkї]Vk8 1 +:Č`aq[aqK\ǠVj ")Њ0 3V8beJlwMTnz3M-ިբ j%]xBRB^NIʅ)n& x1ji3#x)g%*!K c/_t|ky:VJp͈8/Tdv@=aIi NT=;4ڕB^u}ݴYAΗbib<>1+*\NL.S{O6a%$j`Y3Wu>㴲~"qI'ū:Ie3C9_, 9ފ% YFɀp3qgos/^YdZAyFDdBKœZy*H :9IR 7K?2V{5kKI7pmv4sXnꏯ<}$᧛Rޥst|wۮ͜:ݎkokt7vRO3B6߅[) \c +g_Y&Wgy_ɬpٞ6Mjb;a L &1!4.dIӞm`[/v[a kFCjK,U놱RV*; vzG:=ua{g%p-R]8M)Z" Y|U$;m̐'sp%}UċZ\)l jxv#e!B3=ԩ jʓ}si\GኢԜW -^i s{.e'R *ۀZ֧f I9Rhρziq#5]k"ZaNTy63^L۞<Fuu-[7"C$EV6,ˡ29R>i7d}H$?Yid֩_\ڵI%xYd r;IYi.] ʨ d? 9A]M,e+Pp>L~yy@ 81K.ꠘ3s9ܫ႐eGye&QzBl ~]-mj.Ӊu֤̚y>f i}Lt&_tWJ<g(~ -g+qh9)OdEEjlY}+/D10gD4D4.nKN1]3 JMzcQHo* ׸Q:CUZ%C P`Q_AU8sk?ɘjTƕWFa.7#z1..O2̹䮒>)뱔 _z:\:r]##i] !J8M^|ukzZ3wyWv9,`pB-YD*L;%"-BT{DdPO6w#qOߘGKǴ'%Omif[U|]"9s1]UBt^=aE$z DŹHO;C47p`1ź.h\d1yHk Ǭ]u4{By%' GWG R_mw8, efK\LqhTOkĂMIpU&tчt!&.]UisOXK&Y~h&3Oy3TTSjn/0g6#ZgDB:01n15ccM cf*ι󢴟9gB0N<(jbem,kDj@N[*$EiA2͔dDsXb3\".~b&FVIL87K]['Tl&AfXjDƐ˱-1ճ,UZ6Vi+؊0ȅV'[uBsGTLW:BKZOJz7]vX}`˾?p6p9BO? \Юfoǿ$;3f} g`gOI3;vY=Ÿ>Ν$2>*,EtJs| (xq[7 ;D0VtA.CӏߌCۂ싒$Cpi /(xbo]O_0)ASSgJA;lg+*'9l%Ť"s]+vpj<', =5 u+ N:f-I) 5CĴ3$.֏'ٺh\IjX /8&#}0e( 6/oSQ:TA@;Enm+-߀WNBr̺]Sf#) {B4em}>'klo% ZElUwT.Prfh9c+{wR$%ЪDbYs%uo%&ݢԤ'&tz}IL31u#TϹ C\>@kA-4AF;mYd#l_;\Oc8H9йS#">g 5M-m-=½3e, =PFcV4 cnEZ/æ!c!ϭ£X[(r}.;B%:2w  TԬ\ރp[E모I?>[Xeߢ (mrlr*/sUρeed. 0N_P"Ї))Y:>O\S ?v/;Q`\sMgx/糧(^0pm_oEny6ؐT5 ^v^N{p]h/\C*tϺ]q3w(`@*Bv䯜fmUD><þLlj6k(up׈o~/;C9Jvh_WzAJ_fͶ]}P,CgsI\( e<{ٸ~tR`5PC꿍JURG/pB iFoj}us >fvznxseMn8'}HJB"7>D;+leT{!;t." wGճ\>t kGkGaߡe8l鈤9:B/4(|@`-~-SH빨b &3#s JTT E7=mVuiJMh%J+WtR̔b[oF,+Qxz|VhAGЯbzZh}&C:WFA@o>/ }',PvƙջKpwoޝf~s6XPso|

hxD75߇CXw_o}]oُ1##Q \I \o8_t ÇJt] P2O[:I7Pnoe=&1p p?)PG!$x9zu ﷧@9Vppƪ^/+ =6tH}3@vғ}z?1!a~Ee"50RϏ o26@/>S\8Gr5w:?pOF-$?p*k`> )3[ߨڑ|{X՛Ӟ+ǍMvCdp87=2A!mXbs3WoB&=Lox(=Nǫ#_^[s(%ZjfǭZ#\on]dqa]#0ǖZX9pe:)n5nn:-v>v;r5}V\kd&9Kwl#Hz󰆆@fOy=)5\c]7A؟?~?6u*(=c⯌؇6ĤXٸ2=o.ւKl<Ų,I,sͼµتe_oW//1q):@mƅm] uhZVqLX!f 혏[:RcIVI1(+VfkBrӛijFmPnP+9*jrJ W. L t31\sh]VKH9- ݴP]Rl{rWnog仯\;RkFyĝZ'=XLBFNVpJҼ9ع׮ rd6NDi \A4Wt`rtQ=o{ +!V=@siK:)^9N*˟̹bVx/U2BO)8k{b$b 3" 3J=$] MSqFZЙQNRZ>(T^ɵ Y?^O.k涣vlW|=t' ?\.Xx~yt׾*<ѝi<LjֻFwXK9ln.,01|/d'[) \c*gWY&WgyWɬpٞ%0-P⚨ÄӀvKhsxXmm,mۇ?,]@uTWx%KcOj: vzG:=ua{g%p)R]8M)AeA ar,"^J\ץbM)ekTœv"P'L:=ug}b\nj3\1SbWj+MtuK54 LY8NnG0>)85 ŸKy[qG!'ˢɣe,w 5/2z4rc6zIE{q:Gژ{ٙ2\k(oX JO`]h~-OTMmUv:tY>GҬa#`O`ٞ. [鑇^Ŝ^8N,C0 xH-_5Ot(;Ɓp7h6&%ѭ"c)k{&V ]Xs, Me?Jg(\V$Wp(ʜ, -:Vyn3_GW=8S͑jp׸(uxUO EI97UwWb=e!rCOKjd$8!9!g=iւb!wm\O`]S;b&8n:;IqS%=KHyU_jڕrlZn$NSuaĴ䩣!͌vϵBg.KJȜK'$Z7igy^X&I>"0ta3j=;&Oi5ãfOH"DȒ~63y- gEa{bquLW}鐋2S=iMTSXW).1J RV҄.3;P.dU*0-sn@ ki"+7uQ8P6y2zޖV8p&jJ9&QvDHh=X1-Fuqs bYe97c^1lT\;Ib5ETMSaָeHmӉz0cV4ߜ5#3UhK"XcƸ=YKXdCb* p+`䖊_ۂ6Xrtc96%"zvZ{*շRBrV*m[bqNhuj"[G(|I}QIu0Vk7lGF:C^h)kuu Mטvx߬L?[)}c|C"g@q'<߹DGN uobEځw6~&r(Պ.(e(qqqh[3}Qr(.M%=Yl5 &%HvpLR)h,tE$ױp6Udk%\P4޶焥{fvECaT_RՉS۬T=y|{3fvf$[헫!IB%ֳb> $&%m*J7HB5(p'h@­Mt履WpJyIRSYk,w$8a]Hygd0~-dAk".2j>JΚ` sw-g uv[DZuH,kԤ[T$4N//"t& .s{>Ԓj16cs59$c07h-%u4hg38K=,@bęp+wKI_` )g:wjDD1u&`M=Gw,7a9@jsЊF9ݚxݭȾ Yx¢v`vO%q4$}1UxTk+csZEϥx'ZDGᮟ%K p8+|t=xg9?C9 kte_t F~5 K􍻻m /!}I k7xHeY .uFNX HgH_ .5oq%}reR}7QGȎs7?C̐~-hAgط)8љfmߢTQޏegH?Gb^jsP/HI ҺٶBHl;_# _^ g/7`\CCf*sHQJHX(A#M@-e i=S߄z~dXw3V!=ת:qS Dp2nAQ7V Zv+MԈW֟e^;0 O ;SUYO !X"}HgÐ(4Hr'eQ6J\8Czw rͻo+uj/10X&4. >8noV["sNK bsn2Fؘ]ti߈F!tx{<i8z'} o擗pKnv{-1<&vdc{y?* 1 4|a Y a@#C<.+AyJ_FQikWG#qWDu>f<__!.'0??/WN(NJ.XVK%x|^GAW&I\?{ATzr:[\V'A<^12/ҟAUF@j` !#M{½5G~ ǷQbUHfSߟiߨNs `>`>0|p˿U;Ro|:?zsӲcBnp &G&>>Ul~ucYh]Ѥ /)xu~~kuksn]e4ă]˾Ymݬต[+v-,.?,kR +Lg6%m--OGEǎw[xWv cT#s̄8Gqm_o0,|c})ϢX'ƃkB0?X&/?]b~&NtPsІK;WMZp gX%wCepnwCQ[UkM3ve[] &6;[:.E_Ҹkї]Vk8 1 +:Č`aq[aqK\ǠVj ")Њ0 3V8beJlwMTnz3M-ިբ j%]xBRB^NIʅ)n& x1ji3#x)g%*!K c/_t|ky:VJp͈8/Tdv@=aIi NT=;4ڕB^u}ݴYAΗbib<>1+*\NL.S{O6a%$j`Y3Wu>㴲~"qI'ū:Ie3C9_, 9ފ% YFɀp3qgos/^YdZAyFDdBKœZy*H :9IR 7K?2V{5kKI7pmv4sXnꏯ<}$᧛Rޥst|wۮ͜:](okp7Nv3B.VWJcT9:7:˻Jf][/T] C`Z5Q 9 ͱ7zY۲ mXX3ړ]ȀuXWx':5vr/2-u*{ 9'dJ.}]U8M Y|U$;mː&"^J\ץbM)ekTœ!"4ӣhOYt!N(Cs=[ N TE 1% w-N'VYN0k\TZ5) _z:\:W##i] !J8M^|ukzZ3wyWv9,_pB-YD*L;%"-BT{D&L} ˿'k8'oL NL#%_cZӒ43*>׮ .*!s_/vh[ !yamě`Z8.bDчI\b2He}Jizx٧ Cuha3l7 T__czvk~޳Sb\hg:vG?$z?7 };KId|TX[&Q4 }gn-wRaZ]):S%I:( ^PRГVߺ`RDk$vLRIWTNrJ|IWaSEVP HmyNX{jlW4F%uY9uJKe[ГǷ?Sjډig1I\OuI~z(4[ ^pLPi=+F3^1aP@lb_bߦt|$)T3wR&$4/OW[z T'(O+5?uyrGRډڅ)h*|O rJV &bت"+S] 0w'rQWڡ `HJ4UGщIJ:JJJMEI5NMrO#bj*Bgb2GC-i36WssmA2 #q!}ւZRiLv6#]$F ' rwp}r&sFDC}QQ*Z48U^)!\aЋ?ϿD6#7SSt| 's§~^A׃ws90Fk_EKwQ ?0` \0K߸fmʱ!jν(^vT& u _g/P\Tx _S; ]\G{ W(w#1|_9q3} ۲D}x} ҏYm6Q-Ju8h_vsjAѾ)en=p,m Xd氓5+=P˰yqe?.0? .<>j2~_40?-3N?d'Zu1,|̄< pNDn|wV>7 K}7BD6!9Cw;(m5)JA!\EA,xg}"֎ LþC7^˾qIstх^hPZ$,s[sQ9MgG hz7o*3oz|7ЪKj W(3!dK}cҙ)jRDxe YV굳 z(=__Ś(p%R?L܇t6 HAc$|_N mY398$+wO.߼;alRx,![eBf%9W)6&,ct %K MhK~ǃ3AQ\|fn>ykZvn߲#cbGF9(@vpڿa? $}t] P2O[:I7Pnoe=&1p Q?)PG!$x9zu ﷧@9Vppƪ^/+ =6tH}3@vғ}z?1!a~Ee"50RϏ o26@/>S\8Gr5w:?pOF-$?p*k`> )3[ߨڑ|{X՛Ӟ+ǍMvCdp87=2A!mXbs3WoB&=Lox(=Nǫ#_^[s(%ZjfǭZ#\on]dqa]#0ǖZX9pe:)n5nn:-v>v;r5}V\kd&9Kwl#Hz󰆆@fOy=)5\c]7A؟?~?6u*(=c⯌؇6ĤXٸ2=o.ւKl<Ų,I,sͼµتe_oW//̘SC KyB׵Pˮ q5aDŽbXz~܎%cЉ@+5OhEsinhjn%n6ڻ&DpK*7?LojBI}MuRx{ U[ǻmWfNiL,uaA;,U놱NVHr/2/u*{ 9'hJ*MaO; ntuCd2=hdOk ;mː&m`rT˵Z\)ljxr#?DhGў:ճ>AMyzn73\AbWj+MtyK~yy@[c%=u@9e(cgpAcay2c(=Q~u F|?REkfM<<~ HK4#>]~e;%d/+j&~,g*,pf2eTgT3gE^5Ot(;Ɓp7גh6&%ѭ"c)k{&V]X+ Me?Jg(ZV$Wp&ʜS+ ,:Vyb3\GW=5S͑jp׸(exUO EI37UwWbz=erCOKmd$8!9 _g=i_䓍Hw'&꒯1iGCVkׅ\Lz9ݯ@OXaA;I^ѭo󼰶M0-|EtsAn$ZgF1{jwL41kGgC9d͞D^B%<8no%\4\uf=S:6/9 Y/[durdraw-0.27.1/durdraw/help/durhelp-16-page1.dur000066400000000000000000000043511463113753600212630ustar00rootroot00000000000000~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.27.1/durdraw/help/durhelp-16-page2.dur000066400000000000000000000062601463113753600212650ustar00rootroot00000000000000@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.27.1/durdraw/help/durhelp-256-long.dur000066400000000000000000000717761463113753600213320ustar00rootroot00000000000000[fdurhelp-256-long.durs }sl׸Ootn&iwޤ%bs4[x1#x1`ôAHGGB|-KQG# ~C̃f,$wlB҉hf{:r~cM=_;Չ69UZlߒaɦ`@CHAƚ?`F1"Wː B]*ʦ(nȽZ?2Ŋ q \$VMYv՞԰x;.(sbN >nL7Om?H&6ɫz vK+'m 2˥ omhq1$G7DQ#y\@PAZ=uS{` (qPld2'*KjpFUͱb.=A7-WцS7Љ5DﵥbNwGoS_s<`.ToG%?HE4A DzyE xs~.8==ըd@[-lM4Iњc?k1Et$D/I-uԒ*٧rbP6ڃ]BU LsͰ&KVtu;y "J!$I1ӹpwNk!|;ɿ3wGϕaV۷~:X6K&#&]6(9i, f Mm ;Chs^;A}pL['[.A^u)"cm(puE %%#m z|uf#iu:#Kz^F7 [aks{d~%Ǒӣ^uހWdRf`QVmO7r?C\8v<YR»Xô{w7d!@SDShw]!u0ЮZ`gX%S=$FsBs5hE;&Ej YKkf)Rh ڬoYq%JE;. ~Nwv!>q0J{ ( qx {Oyeqg `3'V>těӱ3J}K zZ Y1 Y99"kb~g}ktxZΠcX/Yu[wIG3@s\Lo(Wq1Bgvg<&Ov$CLi ? 0^5^Uc!!frU qA Yd%𢓷+![qh^g`;:/,?\™`Fsp36mo JqMɺrL3|ioiOu;:n֡c]hHGFEa $,m JK竣v*6 ˴38W0)cThbrX]}FE/9Nu POW%yƹJ ; LӰ硃cGsGxtgx)v(:[civѶ(]=[Be]1& B.FK#mVC(&!!J1'RĆ-TlBLqvEHf~5UPfq:8Eql/01f9sDWXaӟ3ORGXLKvo7ua¤X<)䵔`^oGTS)Vh);o_%wb CW;șf7.;G]^Y<3("\&.]aAVq'+1{meZzx [%Bz xɫ>م&@v5w;!;kds@] c$l桺gU: OgaoW:#麍nuFɴP.'ݒ ѡR ё -Edc1^@1QyÓ_5DnaHEf+zt#0)$&3$BwɉQuN7t=KYC϶G ط]qP tݚujm )llw;ſ9gyTTӍ kօ(X5^!݌4bUwiSi\?_񑴬hCtD1- ̱([L aN3)ÀI+=B;JgEZR6yl'*nIo.J7Ձ=: #4n)BoG"[NwI0V wH!'z_ 7qB5p!: \*S8.aƾ'UW)Bܭk, ]ÚՑ_.(7C|k5 }Zdl@P=T7@^CZ=6F~4Ŋ qZXLZn:"p|3;!9rۿF#?z%qKsw!/}+< 7KE _Wj]i$䞴<~nUc W1l/бq/Fh:zgkQK.*(<A(-,Bo OeKVg3bCN2U^HщHwԜ%CXR؜T*j![ =N"K$(^Gu?ʀ*|Ҷ{8s)ty/pdmI6-MG|/M$ W䙛Ƕph*/T<[5oI$cuByP/CM3 mSHN>՞԰x;.(sbN >nL7Om?H&6ɫz vK+'m 2˥ omhq1$Gذ E YsqMCiI*-O퉂%)WO,CnHOȜ,IHUsH6NJG ZV޴R\ENXC^[Z({~w?bTG(ן9?9 UQf:MN)tH&W,)5xz|T1mI,5$=Fk|IIБoeD$9nԭRK~shfBL?LʉAhv V.01*d5Ú,-sTZmNX-*zOzOk'qlN9AB쎎#'n?WNZmߖKb`,[ ti۠`#++41 3 {t1mlAy֥c?o)P-@͛PkrD^B/#5zޠ7onO{q"#xMGrN:h{IWz^IiǚIGY>ˁq LdIqiKôp޹Ϳ(?[sKyG<4&G)z=>~7[á7xv ۳j}=4  h=!PҲzM/e~tQX&ێmV[jC e+ "ty Ԉy*R=`ZUޖגT 7n.0o{/ZE;= B.X0ѮZ`g5X%S/wx ޅN1[A;nEj/8kv-"?ܞ@vvР%TTthqJ{ (qx M68֧6p 7/qGxw x i> ꯎ}Ku.Ps͎zxX@V`~o'#c %LXyNmVbηx8y`NșSi乪{}ktxZΠcY׮Wׅu[wIGg4RusV3L0q1Bgvg:&!dS) yA΄x4.:3^5^U%AxN3vj3 .u!;딬^tVbŵ7}ap\Mvy\'?ةuιxξOO8WVIrHp{h9oU;Mc[[R\S gj6TWWS|y@T:tK )ߙ&,k ?IK;(HvP\^.2[Ƥ_L9&R1ΣyuFccu>L8b1@acVGa})ڞNN ^t1̯kq UF3b*]]_ &')O1'5 9b0O靴FKyx *ySsA6 u9ݓ>)zDyW*'5qE(v ;Y共سh,ðuû_B-қ8,5g;N^.6(S. X %c7j#/]Ou6x M~: {j u7 Nmw30RMr>tm͐j/nyp 0)t!:v Cz-2`^okVDiI!1!n{EHw>ދ*DwotX ݕZ}u8nXS5ֽS;~G`'6`'v$s0NIs ';֬ QޙOнB)i6LGO1ҹ~^9#iYцcSN[cQ< ÜfrS 1Vz.`9R-w82ϊ8l v(.O7 T"ݓ.\po.{u|YG,hR Dts左#Y4`{ؑBNAo>x jBu0&PσU8Gq3ۡ!\ Œ}5Ni8=Rᅸ[͛רYjZ5#]Pnr+kTye0ـznꇽwoCG{lP'h:qMj}uDfw4xw:KFh夂Jp[;H0j F?_j}0Bsd wVG~K9B_JW$8yc1^cs_u ֢ܗ\UPyn/Q[Xx:ʖgĆd,YSda#49Y513K499TB {%EfH&&9Q2\7~UmIpDS4_ :ڒ mZt^|Ik}37m*_Tu_ y6kޒ I$]n= _ Tg 1§ lE*|=av\P؃n}Yݾn1$~Lm*ړWT/~\WN=dؗKx4eP L#τsct_%~ #{B?y\b|mn~>n> |#AkѤ%o%nT ?ZCdn+r[~:]`bT/Ȝk5YZ樴ۜN۱[U"X '? Oل΅sZ GOek?j~?ھ-h1Y2t=61AҶA1L dGVW0imcbE#ngnc:قp ZK!kC (ǜ.R-)'4n[㫛7#4$ENA^'Gk5ZAo ;_{'HDG&+y=vw5dZa2@OŤ[op`wxjҫ|X{B&<͋! :{\udg_ (e'|P1/`6!Φ--X))YWi#y@mfpY.m$1=9  tsђFN;`\w^r3[F_L_$cG\‡3}(zq.>®>S=#C^uHϙ_,HgDYLP'.ghzrsV멐eYvơɶ`8˅Q7҈7: fj2$d3DNDذʗM贒)ծ̯;f ì_<]1&j9tb+kg~_%>,*dE;Ƃ_ `wYW~Ax [Ho`8y'0ۄNn'd'c-u~nyah ?Z*Otxu@- ȬyQo5[]!&cDY9W 9-}nb)tWz>kyhHB+ [a[MAA;=7:;I ؉ߜ< S\CΆ5uw tnC aw1F請 Ӵvt 婴tWHZV!|T˖Xe0ܔa@} 褕KXTN̳o-)¼?6 MpHo@xG7%a_ ~Ex~#\EVG' $b;vzЛ8^oyL .`)Q vh0c_`o*|+Ntx!V5jڅ֮aȯq! UF^>~Y-2y6ƿla/у!-G#t~4Ŋ qZXLZn:"p|3;!9rۿF#?z%qKsw!/}+< 7KE _Wj]i$䞴<~nUc W1l/бq/Fh:zgkQK.*(<A(-,Bo OeKVg3bCN2U^HщHwԜ%CXR؜T*j![ =N"K$(^Gu?ʀ*|Ҷ{8s)ty/pdmI6-MG|/M$ W䙛Ƕph*/T<[5oI$cuByP/CM3 mSHN>՞԰x;.(sbN >nL7Om?H&6ɫz vK+'m 2˥ omhq1$Gذ E YsqMCiI*-O퉂%)WO,CnHOȜ,IHUsH6NJG ZV޴R\ENXC^[Z({~w?bTG(ן9?9 UQf:MN)tH&W,)5xz|T1mI,5$=Fk|IIБoeD$9nԭRK~shfBL?LʉAhv V.01*d5Ú,-sTZmNX-*zOzOk'qlN9AB쎎#'n?WNZmߖKb`,[ ti۠`#++41 3 {t1mlAy֥c?o)P-@͛PkrD^B/#5zޠ7onO{q"#xMGrN:h{IWz^IiǚIGY>ˁq LdIqiKôp޹Ϳ(?[sKyG<4&G)z=>~7[á7xv ۳j}=4  hHM*/r$$Se2;wz@?].9bY] Ax>r-Y.P=k}% R#(b4zP=ȷZD OH$těɮX@]ez_GFj-DοgS,rf_,;AHy١y5:<{gp^ 9WDW-3=v}z6%吾~+OAk9ӛ}?!>KoY-?I@Ue@@jW0^5Cx!!4kÈ b]o:-+gڛ>X{B&<͋!-V :{\udg_ (e'\Tp++$9$@|pil |K)VkJ֕cLs}(TwY< cVWaCt(az;qRˊx M`u##7E3@ċux;ux1b4h}FduorsV멐eYvơɶlи˅Q7҈7 j2$d3DDذʗM贒)ծ̯킼f ì_<>^1 T3 \.c+,0O')^!ܓ*o„IɱxSL5Ǽ.Ώ87?S3RBJn9v3n0?H]Nw,yf'Q^IM\;]ÂB;JgmZRLWf5(.c7 T"ݓ.\po.{u|YG,hR Dts左#Y]4`{إBNAo>x jBu5&PU8Gq3ۡ!\ Œ}5Ni8}S[͛רYjgZ5KqPVu+kTy0ـznꇽwoCG{l'h:qMj}uDfw4xw:KFh夂Jp[;H0j F?_j}0Bsd wVG~K9B_JW$8yc1^cs_u ֢ܗ\UPyn/Q[Xx:ʖgĆd,YSda#49Y513K499TB {%EfH&&9Q2\7~UmIpDS4_ :ڒ mZt^|Ik}37m*_Tu_ y6kޒ I$]n= _ Tg 1§ lE*|=av\P؃n}Yݾn1$~Lm*ړWT/~\WN=dؗKx4eP L#τsct_%~ #{B?y\b|mn~>n> |#AkѤ%o%nT ?ZCdn+r[~:]`bT/Ȝk5YZ樴ۜN۱[U"X '? Oل΅sZ GOek?j~?ھ-h1Y2t=61AҶA1L dGVW0imcbE#ngnc:قp ZK!kC (ǜ.R-)'4n[㫛7#4$ENA^'Gk5ZAo ;_{'HDG&+y=vw5q`Cq(g-@7Iy[tӇY30t Ռky>M7_HzB 2ebʼ8\ \Pk ]?Cg4?bT>cv`{[~k_KSл\12([qOB2]o^+yvDShBy#kՕ~W-0,ג 㩻ٗ Z;,J )|EԚD#*T>3h"KdO0 TuׄuѨx6J)x U{[Ѫ7xw Xe"]: daHNw)]x Mc] ҭ#ѻ<4WEmMu~LJyE9򁬠5$\OmV(Bɠo{2#V^S+!+|+5Fߚw3h8/sZօsUPq֩wQGg[R5]L)|KLC~%70[3W^リɜʦ}AO̫yTt.o6E`^}``/4k{yCyֹZ oB3LYr7hn'nfq)b=-7ٗ c9 Q q/mK7gA\[wRxS _GkC ,߀6p7p\0!"\'u˔Ąu4s"9lIUØa$|12Hn8Qט>N+1Σu6dw>35b1@a;_mRyJO7Zn_%zb CW;șf7.$G]^Y<3/3-^\&.]aAVq'+9|{meZzx [%Bz xɫ>م&@vʥw;!;kds@] c$Em桺wU: OgaW:#麍nuFɴP.'ݒ ѡR ё -5dc1^@1QyÓ_5DnaHEf+zt#0)$&3$Bw.N7t=KYC϶G 6]fúZpv {{':6:$Ďdo=)i!tc`gÚu: ;sWH7!0ͻw݆iZ;`:TZ:+g|$-+]>yLp*iKs,2S~LnJ0>tJϥ|,GRYѷMaŅ &8}J[ {neu`N/[ [<ᑈnt"]LUC`;RȉWB=gP \yGz(`ww;4KAI>Gz:wy5KBkװfu?|o#ջ:i|ZUǪøB=R=_.q4B|X!7T ѷY[ˍ_GoF{G'm>RG;ؼtmVN*Q7 k庻sX+g:#;GVp}Whq|G$ni#OyEG^f!Y+ܓVWW^mj=F:77m_Gol-}e[y"(廅Zal ~FlI ɒ55:I>BہUC>dH ^/J_-d+PGQY]divdb%uCnGY[[POږtgN4E3.O-ɰڦHɗ$ ~֗lpMn]VF 31~ 8- պݱ6@r"ߒBx=y3BCR:tMK%~rF_wi=ND}o HnwQSm/:Qo+2)MX30(6ާ}9ЂUy!.,)v}8mi~";gkz)4Svhd6(2%ZFw`8tOu~{4ZOtѸ}4>ABch,"R#o)Ώ{۟aTǒe4lE&Z~z[kM7I")g}sf96r(aɦI³n^BIH7(qk.YhNzЗV`ZCS#[nZ92/gW125cKyD[͹+?bH jf`@*_Jui]eݞ:4cSX|3ٚ/[pۓ^5U6Ժ_>/40s0CWp]z%;nkNdO?/-KS?W˳_8+>ᖁ5ՖZ{r)Vis hk͉Ptm1&"q)顉 vb@Mt O43kx΀*!ʲ >ǡt@Ңܬ"Q&oMMKft!,T3J8!<4n  qȐ՝o*)Rfs)~oV[jC e+ "ty Qy8RQ>`[ٕymQ`-Y.O!R~S@b&s}Ǭˠn= ɔw?vLȝOe yXp,nVWy^j?\K/Tf_ïokʳX+/T;QkS P ?΄]O84:<ݛ*R ˂]ֿF³x{8?9(-Tn)G]\,kCq'\X'D,5ph|wP*)<։封P)-;{[cNxUTw|߿NX}Q+ XOdf. (:b5:bη9"y`Nș\y. ѷ]g K>u|Uz'B̺ذ?:=ǒN%~Ik9ӛ}?)ߒ@5T萇_ ,_֤W*A2穲i@_ma ۟Rzyuyy`l7g.4;P:NX,=!Wy]܉Ftu|g_,(s+F9$Ľ@- qoJMɺrh3|mM7pWWo|ruLdsIV +iDtsڒ2]=Ęr5dJ8dh_r Y]}ƧE/LoMu POW z9M ;Q$+A%>m'/q=Rptu8*kPo|,?C; ܕ{JOE,b:uC6Lk^.3 F()PA6#.,N [|لN+\#k֋0:vp(X(f*L4qeF<;$國=UG 7ua¤X<)f|Ճc^ITS)Vhɏr^~| ) ^ gz`~luyem̔'˦δyZs"w܇yZŝXaX:k/ox M3'df])WTdϣou5W:_kIh7`vA`uk9{)7hwj9NmNH%aPROL7v6Y׬`3{t3RӼ0zWmc(Os|rGҊ "40Ǣx@,o-359c@'-\ rZp*e|kIqQ\ nDz»']>(\4Xи+ oMG(8i%[5.ޱ}%ԃ|~ [*Lp>LqfxwCCj{W^q: q7Qv kV~ Vxר20)ja̳5eCP{և i/8!'h:qMj}uDfw4xw:KFh夂Jp[;H0j F?_j}0Bsd wVG~K9B_JW$8yc1^cs_u ֢ܗ\UPyn/Q[Xx:ʖgĆd,YSda#49Y513K499TB {%EfH&&9Q2\7~UmIpDS4_ :ڒ mZt^|Ik}37m*_Tu_ y6kޒ I$]n= _ Tg 1§ lE*|=av\P؃n}Yݾn1$~Lm*ړWT/~\WN=dؗKx4eP L#τsct_%~ #{B?y\b|mn~>n> |#AkѤ%o%nT ?ZCdn+r[~:]`bT/Ȝk5YZ樴ۜN۱[U"X '? Oل΅sZ GOek?j~?ھ-h1Y2t=61AҶA1L dGVW0imcbE#ngnc:قp ZK!kC (ǜ.R-)'4n[㫛7#4$ENA^'Gk5ZAo ;_{'HDG&+y=vw5̋_n',> ͉d]'zkBAJ}K }Z Y1[<0X̾<м5ּAy)Բ_ci@oI&]lHkI1[ZQ28ω~Sj-p`wxOjk|O fJAhW0^54j b]o:%+gXqMwX,=!Wy]׉EW :{\udg_g & 6I n/m4 XE[4e+5%1Zz>? ,rq3FC#wi katCtOҒꨚk*P 40>ҩ.Gq^c;D9&R1ΣyuFccu>L8b1@amH'/qRt8*mQt,?EJ]gTȲ,S;fd[2Ah\¨ai@-j2$d3DnDذʗM贒)ծ/f ì_<]1 T3\b+,0O')^!ܓ*n„IɱxSL5Ǽ.87?S2RBJ.5v3n0?H]Nw*yf'Q^թIM\;]Â+ln:uo6NtQmt6I ؉ߜ;< S\CΆ5uwtnC aw1F請 Ӵvt 婴tWHZV!|TӖXe0ܔa@} 褕KXTN̳o-)¼?6 MpHo@xG7%a_ ~Ex~#\EVG' $b;vzЛ8^oyL .`)Q vh0c_`o*|+Ntx!V5jڅ֮aȯq! UF^>~Y-2y6ƿl(IH@{և i/8+3u6&ja2!v##yckhhGu|I!7*?ঁc-\wwΑ`+wv壕pB?`pnJ6著-ݝsti#Hp(x܋,!$|]=u{sjCKmW}4\c@E/l?^|V 5,t>-Yψ 9Ty!YF'"Gh=w;Psjbg2 ibaK5csrRl%*(J89,LLrdzn (kKs+Iے̉hu%V۴44$\gnU8Pln׼%H) zA$ 5ΰ+bO!4؊*:UT{R3rϱ:1o$*}3d?cH kT'_'-81<{Ȱ/. irF AF 鞿K'-G~ ¿2*5|$ |¿;G עIխKJܨŸA8~S4DGNWx2T.hKٜ jѩkDoV"*Z`uXhkX2 ɴOHFn(.M kɈ"_\Ѥ-*ߐlNNH9Q< !1W QOpoA8>ܞH# Ɛc6LeZ8/g 4%S[<' \{> Fn!=!sb`T'iz mT!+:-6#N$k)[yӒJqm:w#c {miQr PoS_s<`.ToG%?HE4:ѦU @^X7QJN%Ѳ֤K9F_&S$AGnIRJ-B̡9} 10)'e=%tZĨX_9 kQi%O9^c+ҫD==N2~@n ; w;:>s~Ժ\9v8h}[.ced znm2bХmcȲЯ`҄ư3Ċ6G.$ǴuZ"B;ֆWP9][ROhBV1q{ OkzvN>>ljM6Wz:=jx%]Q; xE& k&et/Z*O>ącw0%yγ- 6COYd{vV4lO/fn-И,FEDqpPn ޹6na~Bk 8EڬE-BIdž-oyJn%DDIpf{ǯӃaZ OWA8KƇx1Iy2I}Gwo:~pjX<_U\Rot/goA7[Tr7o"Ţ[/;۫L_LndTF:mQ&(K߯*i]=j}?˸zȽbuSYy^eֿr6m97tir_Y1r!OՕ΋jHu[7|:y(+oWO|p9gE/Vrv}=,ߋ}C_v>/>ϋuWc|[}bUwW_oO#\b2{X^eMe2yPճIZlP(?8)G5ٔqN[jkQ|Adfjɨ5r`SrĔE4X ߅u8-X JxpykQ|v\zhCV{*j%qv\r4 !;n(}ckyu-O8]³Qr3*N melDN0=\Fq7Tx}+%tr|%No87q7;N"e6 7cX*i /oõ ྘of;Ǘ/@=C G㚣7TA`~ȗfFA'Ao{P-:&bKEs"W6|tnފnF"N8gB3{b-@L:wǢ-sE9hR 0N_SI BsCy+?Yd|$\ZDȻP'Uvpx$<5r8V Lx*Ltbo֛wh!ڻnq-`g>-ms=4[FdBx)>2ÒOpoEf4J6QkơcxMi|Զ9HGT"3okWԄuZqUup$Q).Xv3q(嚶!LjxV&:1ZF.91y キG䊱JFD\}\mhf͏涹t҉!y{81Vc7X.#YB^667nvץ Y $V Z O0!ʬN2m*Ta9Xd<6W Wuaʼ|/8QX?1,U N՘;`q]A~(E]Ll*xi9y%}2 ?Rk01W%M=-y̧Jxa rlvļ][[mL9~a&$ըd~[\w 5bC.A$R%VҠB:C0MƸ42;LʥމQZH{u8a#[iFxe$ #Ew(c2IqS c0 B,BM ۑ!_-F8%;A`qȯy~T+z(T DmЪ ml&zHE~Z`#c L5 6ZygwV00`΢v0p$x=jvHf jIв j!X.HC!\.7C#o`q|Tdi=ڼzz/U"durdraw-0.27.1/durdraw/help/durhelp-256-page1.dur000066400000000000000000000061231463113753600213500ustar00rootroot00000000000000ddurhelp-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.27.1/durdraw/help/durhelp-256-page2.dur000066400000000000000000000067301463113753600213550ustar00rootroot00000000000000ddurhelp-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.27.1/durdraw/help/durhelp-256.dur000066400000000000000000000043341463113753600203570ustar00rootroot000000000000001Addurhelp-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.27.1/durdraw/help/durhelp.dur000066400000000000000000000050711463113753600200440ustar00rootroot00000000000000;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.27.1/durdraw/main.py000066400000000000000000000242761463113753600162430ustar00rootroot00000000000000# 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 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(): DUR_VER = '0.27.1' 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 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("-q", "--quick", help="Skip startup screen", parserStartScreenMutex.add_argument("--startup", help="Show startup screen", action="store_true") parserStartScreenMutex.add_argument("-w", "--wait", help="Pause at startup screen", action="store_true") 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("--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) 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() if args.version: print(DUR_VER) exit(0) if args.times and not args.play: print("-x option requires -p") exit(1) 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) if args.debug: app.setDebug(True) if args.undosize: app.undoHistorySize = int(args.undosize[0]) app.showStartupScreen=True term_size = os.get_terminal_size() #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] else: app.width = 80 # "sane" default screen size, 80x24.. #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] else: app.height = 24 - 1 if args.max: if term_size[0] > 80: app.width = term_size[0] if term_size[1] > 24: app.height = term_size[1] - 2 if args.play: app.showStartupScreen=False elif not args.startup: # quick startup is now the default behavior 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 and theme if app.loadConfigFile(): 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) if app.width == 80 and app.height == 24: print("Canvas size: %i columns, %i lines (Default)" % (app.width, app.height)) else: 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 #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: # 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') 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.27.1/durdraw/plugins/000077500000000000000000000000001463113753600164135ustar00rootroot00000000000000durdraw-0.27.1/durdraw/plugins/bounce_movie.py000066400000000000000000000016771463113753600214520ustar00rootroot00000000000000# 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.27.1/durdraw/plugins/repeat_movie.py000066400000000000000000000012141463113753600214420ustar00rootroot00000000000000# 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.27.1/durdraw/plugins/reverse_movie.py000066400000000000000000000007671463113753600216510ustar00rootroot00000000000000# 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.27.1/durdraw/themes/000077500000000000000000000000001463113753600162175ustar00rootroot00000000000000durdraw-0.27.1/durdraw/themes/default.dtheme.ini000066400000000000000000000015261463113753600216150ustar00rootroot00000000000000; 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.27.1/durdraw/themes/mutedchill-16.dtheme.ini000066400000000000000000000012101463113753600225350ustar00rootroot00000000000000; 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.27.1/durdraw/themes/mutedform-256.dtheme.ini000066400000000000000000000013721463113753600225040ustar00rootroot00000000000000; 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.27.1/durdraw/themes/poopy-256.dtheme.ini000066400000000000000000000010571463113753600216500ustar00rootroot00000000000000; 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.27.1/durdraw/themes/purpledrank-16.dtheme.ini000066400000000000000000000011241463113753600227360ustar00rootroot00000000000000; 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.27.1/durformat.md000066400000000000000000000142721463113753600156150ustar00rootroot00000000000000Durdraw 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.27.1/examples/000077500000000000000000000000001463113753600151005ustar00rootroot00000000000000durdraw-0.27.1/examples/cm-doge.asc000066400000000000000000000020531463113753600171030ustar00rootroot00000000000000 ;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.27.1/examples/cm-doge.dur000066400000000000000000000020631463113753600171300ustar00rootroot00000000000000ˮ`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.27.1/examples/cm-eye.dur000066400000000000000000000111201463113753600167660ustar00rootroot000000000000007ldcm-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.27.1/examples/dogeblink.dur000066400000000000000000000041561463113753600175600ustar00rootroot00000000000000`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.27.1/examples/dopehax.dur000066400000000000000000000076251463113753600172560ustar00rootroot00000000000000`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.27.1/examples/indyz-dopehax-256.dur000066400000000000000000000075231463113753600207200ustar00rootroot00000000000000edopehax8.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.27.1/examples/indyz-linuxfire.dur000066400000000000000000000041541463113753600207600ustar00rootroot00000000000000gdlinuxfire3.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.27.1/examples/indyz-xmas.dur000066400000000000000000000160001463113753600177140ustar00rootroot00000000000000 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?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.27.1/examples/mane.dur000066400000000000000000000015301463113753600165330ustar00rootroot00000000000000cmane.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.27.1/examples/rain.dur000066400000000000000000000014721463113753600165510ustar00rootroot00000000000000L`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