pax_global_header00006660000000000000000000000064145504476000014517gustar00rootroot0000000000000052 comment=32e455373d4249a6e335d514933a65584680bc44 durdraw-0.25.3/000077500000000000000000000000001455044760000132565ustar00rootroot00000000000000durdraw-0.25.3/LICENSE000066400000000000000000000013551455044760000142670ustar00rootroot00000000000000Copyright 2009-2021 Sam Foster Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. durdraw-0.25.3/README.md000066400000000000000000000324341455044760000145430ustar00rootroot00000000000000Durdraw ======= __ __ _| |__ __ _____ __| |_____ _____ __ __ __ / _ | | | __| _ | __| _ | | | |\ /_____|_____|__|__|_____|__|___\____|________| | \_____________________________________________\| v 0.25.3 ![durdraw-0 24 0-example](https://github.com/cmang/durdraw/assets/261501/27efcf7c-96ae-4930-8360-21276dc72fca) ## 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 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`. This tries to run in 256-color mode by default. To start in 16-color mode for viewing MS-DOS style ANSI art, use `durdraw --16color`. ## RUNNING WITHOUT INSTALLING You may need to install the "PIL" or "pillow" python module first: ``` pip3 install pillow ``` Then you can run Durdraw with: ``` ./start-durdraw ``` To look at some included example animations: ``` ./start-durdraw -p examples/*.dur ``` To edit 16-color PC Scene (MS-DOS/CP437) ANSI art files in a Utf-8 terminal, use the --16colors option: ``` ./start-durdraw --16colors ``` ## GALLERY [![Watch the 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 ..................... .. About ........................... : F1-F10 - insert character : : version: {ver} : : esc-up - next fg color : : color mode: {colormode} : : esc-down - prev fg color : : character encoding: {charmode} : : 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. Note that 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 control the 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. #### 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 is not usually compatible 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. ### CREDITS Developer: Sam Foster Home page: http://durdraw.org Development: https://github.com/cmang/durdraw ANSI and ASCII artists: cmang, H7, LDA ### LEGAL Durdraw is Copyright (c) 2009-2023 Sam Foster . All rights reserved. Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. durdraw-0.25.3/durdraw.1000066400000000000000000000037661455044760000150240ustar00rootroot00000000000000.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. .TH DURDRAW "1" "January 2024" "durdraw 0.24.1" "User Commands" .SH NAME durdraw \- versatile ASCII and ANSI Art text editor for drawing in terminal .SH DESCRIPTION usage: durdraw [\-h] [\-p PLAY [PLAY ...]] [\-\-startup | \fB\-w\fR | \fB\-x\fR TIMES] .IP [\-\-256color | \fB\-\-16color]\fR [\-b] [\-W WIDTH] [\-H HEIGHT] [\-m] [\-\-nomouse] [\-\-cursor CURSOR] [\-\-notheme] [\-\-theme THEME] [\-\-cp437] [\-\-export\-ansi] [\-u UNDOSIZE] [\-V] [filename] .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 durdraw-0.25.3/durdraw.ini000066400000000000000000000002301455044760000154220ustar00rootroot00000000000000; 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.25.3/durdraw/000077500000000000000000000000001455044760000147265ustar00rootroot00000000000000durdraw-0.25.3/durdraw/__init__.py000066400000000000000000000000001455044760000170250ustar00rootroot00000000000000durdraw-0.25.3/durdraw/charsets/000077500000000000000000000000001455044760000165425ustar00rootroot00000000000000durdraw-0.25.3/durdraw/charsets/unicode-groups.xml000066400000000000000000000553021455044760000222340ustar00rootroot00000000000000 durdraw-0.25.3/durdraw/durdraw_ansiparse.py000066400000000000000000000476441455044760000210340ustar00rootroot00000000000000#!/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 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)] - 1 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) 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.25.3/durdraw/durdraw_appstate.py000066400000000000000000000260661455044760000206630ustar00rootroot00000000000000import configparser import curses import gzip import os import pdb import pickle import subprocess import sys 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 = "" self.colorMode = "256" # or 16, or possibly "none" or "true" or "rgb" (24 bit rgb "truecolor") self.maxColors = 256 self.editorRunning = True self.screenCursorMode = "default" # can be block, underscore, pipe 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.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.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.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.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 setCursorModePnt(self): self.cursorMode="Draw" 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 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.25.3/durdraw/durdraw_charsets.py000066400000000000000000000103131455044760000206420ustar00rootroot00000000000000# 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.25.3/durdraw/durdraw_color_curses.py000066400000000000000000001135441455044760000215420ustar00rootroot00000000000000import 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 } 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_256color(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_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(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.25.3/durdraw/durdraw_file.py000066400000000000000000000321101455044760000177440ustar00rootroot00000000000000# 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.25.3/durdraw/durdraw_gui_manager.py000066400000000000000000000013721455044760000213110ustar00rootroot00000000000000# 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.25.3/durdraw/durdraw_gui_manager_curses.py000066400000000000000000000013431455044760000226730ustar00rootroot00000000000000# 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.25.3/durdraw/durdraw_help.py000066400000000000000000000002461455044760000177620ustar00rootroot00000000000000from importlib.resources import files def get_resource_path(module: str, name: str) -> str: """Load a resource file.""" return files(module).joinpath(name) durdraw-0.25.3/durdraw/durdraw_movie.py000066400000000000000000000335451455044760000201610ustar00rootroot00000000000000from copy import deepcopy from durdraw.durdraw_options import Options import json 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): for x in range(0, self.height): self.content[x].reverse() 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 toJSON(self): return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) durdraw-0.25.3/durdraw/durdraw_options.py000066400000000000000000000012761455044760000205310ustar00rootroot00000000000000import 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.25.3/durdraw/durdraw_sauce.py000066400000000000000000000054071455044760000201360ustar00rootroot00000000000000#!/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.25.3/durdraw/durdraw_ui_curses.py000066400000000000000000007101501455044760000210350ustar00rootroot00000000000000# This file needs to have all of its non-curses code moved into separate files/libraries 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 PIL import Image #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 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() self.colorpair = self.ansi.colorPairMap[(self.colorfg, self.colorbg)] # set ncurss color pair 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.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 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 # phase 2 self.ansi.initColorPairs_cga() 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 enableTransBackground(self): curses.use_default_colors() 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, 15, -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 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 self.colorpair = self.ansi.colorPairMap[(self.colorfg, self.colorbg)] 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.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() 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() # 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() # Window is too narrow, but tall enough to show more stuff on the bottom. elif realmaxY - 10 > self.mov.sizeY: if self.appState.colorMode == "256": self.statusBar.colorPicker.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.colorfg < 16: 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": 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() def nextBgColor(self): """ switch to the next bg color, cycle around if at beginning """ if self.appState.colorMode == "256": pass elif self.appState.colorMode == "16": if 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": pass else: if self.colorbg > 1: 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 = 8 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(1500) 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: 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: 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]): 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] 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 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}" self.notify(inspectorString, pause=True) def clickedInfoButton(self): realmaxY,realmaxX = self.realstdscr.getmaxyx() # test size if realmaxX < self.mov.sizeX + self.appState.sideBar_minimum_width: # I'm not wide enough self.showFileInformation(notify=True) else: self.toggleShowFileInformation() 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.sideBar_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 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_repeat(self): """ |>|> Clone all the frames in the range so they repeat once """ pass def transform_reverse(self): pass 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() 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}") 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)) #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 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.appState.sideBarShowing = False self.statusBar.hide() self.cursorOff() self.setWindowTitle(self.appState.curOpenFileName) self.playing = True self.metaKey = 0 if self.appState.playOnlyMode: self.appState.drawBorders = False if not self.appState.playOnlyMode: # mode, show extra stuff. self.drawStatusBar() playedTimes = 1 new_time = time.time() # see how many milliseconds we have to sleep for # then divide by 1000.0 since time.sleep() uses seconds sleep_time = (1000.0 / self.opts.framerate) / 1000.0 self.mov.gotoFrame(self.appState.playbackRange[0]) mouseX, mouseY = 0, 0 while self.playing: # catch keyboard input - to change framerate or stop playing animation # get keyboard input, returns -1 if none available self.move(self.xy[0], self.xy[1]) self.refresh() if self.appState.viewModeShowInfo: self.showFileInformation() if not self.appState.playOnlyMode: self.drawStatusBar() self.move(self.xy[0], self.xy[1] - 1) # reposition cursor c = self.stdscr.getch() if c == 27: self.metaKey = 1 self.commandMode = True c = self.stdscr.getch() # normal esc if self.metaKey == 1 and not self.appState.playOnlyMode and c != curses.ERR: # esc 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 self.prevBgColor() c = None elif c in [102, curses.KEY_RIGHT]: # alt-right - next bg color self.nextBgColor() c = None elif c in [curses.KEY_DOWN, "\x1b\x1b\x5b\x42"]: # alt-down - prev fg color self.prevFgColor() c = None elif c == curses.KEY_UP: # alt-up - next fg color self.nextFgColor() 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() 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) 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) elif c in [ord('3')]: # F3 - insert extended character self.insertChar(self.chMap['f3'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) elif c in [ord('4')]: # F4 - insert extended character self.insertChar(self.chMap['f4'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) elif c in [ord('5')]: # F5 - insert extended character self.insertChar(self.chMap['f5'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) elif c in [ord('6')]: # F6 - insert extended character self.insertChar(self.chMap['f6'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) elif c in [ord('7')]: # F7 - insert extended character self.insertChar(self.chMap['f7'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) elif c in [ord('8')]: # F8 - insert extended character self.insertChar(self.chMap['f8'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) elif c in [ord('9')]: # F9 - insert extended character self.insertChar(self.chMap['f9'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) elif c in [ord('0')]: # F10 - insert extended character self.insertChar(self.chMap['f10'], fg=self.colorfg, bg=self.colorbg, frange=self.appState.playbackRange) 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 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.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 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 else: self.pressingButton = False else: if self.pressingButton: self.pressingButton = False print('\033[?1003l') # disable mouse reporting 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() 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 <= 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 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": self.statusBar.drawCharPickerButton.show() 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 pass 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 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() if not self.playing and self.appState.colorMode == "256": if self.window_big_enough_for_colors(): self.appState.sideBarShowing = True self.statusBar.colorPicker.show() else: self.statusBar.colorPicker.hide() if self.playing: self.statusBar.colorPicker.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 self.statusBar.colorPicker.handler.move(self.appState.sideBarColumn, 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) self.statusBar.drawCharPickerButton.update_real_xy(x = statusBarLineNum) 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 # Draw elements that aren't in the GUI framework self.addstr(statusBarLineNum, frameBar_offset, frameBar, curses.color_pair(mainColor)) self.addstr(statusBarLineNum, fpsBar_offset, fpsBar, curses.color_pair(mainColor)) self.addstr(statusBarLineNum, delayBar_offset, delayBar, curses.color_pair(mainColor)) self.addstr(statusBarLineNum, rangeBar_offset, rangeBar, curses.color_pair(mainColor)) if 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)) # 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)) for c in range(1,17): 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)) for c in range(1,9): 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 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, 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 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: 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("F") 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) 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 - 10 < self.mov.sizeY: # I'm not tall enough returnValue = False # and gosh darnit, pepple like me. if realmaxY - 10 < 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() self.hardRefresh() def clickedRedo(self): self.undo.redo() 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 mouseX, mouseY = 0, 0 self.pressingButton = False 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: if c == 111: # alt-o - 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 self.prevBgColor() c = None #elif c in [102, curses.KEY_RIGHT]: # alt-right - next bg color elif c in [curses.KEY_RIGHT]: # alt-right - next bg color self.nextBgColor() c = None elif c in [curses.KEY_DOWN, "\x1b\x1b\x5b\x42"]: # alt-down - prev fg color self.prevFgColor() c = None elif c == curses.KEY_UP: # alt-up - next fg color self.nextFgColor() 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('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() # 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 == 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.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) elif c in [ord('2')]: # esc-2 copy of F2 - insert extended character self.insertChar(self.chMap['f2'], fg=self.colorfg, bg=self.colorbg) elif c in [ord('3')]: # F3 - insert extended character self.insertChar(self.chMap['f3'], fg=self.colorfg, bg=self.colorbg) elif c in [ord('4')]: # F4 - insert extended character self.insertChar(self.chMap['f4'], fg=self.colorfg, bg=self.colorbg) elif c in [ord('5')]: # F5 - insert extended character self.insertChar(self.chMap['f5'], fg=self.colorfg, bg=self.colorbg) elif c in [ord('6')]: # F6 - insert extended character self.insertChar(self.chMap['f6'], fg=self.colorfg, bg=self.colorbg) elif c in [ord('7')]: # F7 - insert extended character self.insertChar(self.chMap['f7'], fg=self.colorfg, bg=self.colorbg) elif c in [ord('8')]: # F8 - insert extended character self.insertChar(self.chMap['f8'], fg=self.colorfg, bg=self.colorbg) elif c in [ord('9')]: # F9 - insert extended character self.insertChar(self.chMap['f9'], fg=self.colorfg, bg=self.colorbg) elif c in [ord('0')]: # F10 - insert extended character self.insertChar(self.chMap['f10'], fg=self.colorfg, bg=self.colorbg) else: 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 c = None if c == 24: self.safeQuit() # ctrl-x elif c == 15: # ctrl-o - open 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.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() 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 elif c == curses.KEY_MOUSE: # We are not playing try: _, mouseX, mouseY, _, mouseState = curses.getmouse() except: pass if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX \ and mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum: # 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 if not self.pushingToClip: cmode = self.appState.cursorMode if cmode == "Draw" or cmode == "Color" or cmode == "Erase": self.undo.push() self.pushingToClip = True elif mouseState & curses.BUTTON1_RELEASED: if self.pressingButton: self.pressingButton = False print('\033[?1003l') # disable mouse reporting curses.mousemask(1) curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) if self.pushingToClip: self.pushingToClip = False 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 self.xy[1] = mouseX + 1 # set cursor position self.xy[0] = mouseY + self.appState.topLine elif self.appState.cursorMode == "Draw": # Change the color under the cursor # also set cursor position 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() elif self.appState.cursorMode == "Color": # Change the color under the cursor # 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 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": # Change the color under the cursor self.xy[1] = mouseX + 1 # set cursor position self.xy[0] = mouseY + self.appState.topLine self.startSelecting(mouse=True) elif self.pressingButton: self.pressingButton = False print('\033[?1003l') # disable mouse reporting 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 elif mouseState & curses.BUTTON1_PRESSED or mouseState & curses.BUTTON4_PRESSED or mouseState & curses.BUTTON5_PRESSED: #print('\033[?1003h') #self.notify("Farfenugen") # 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) #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 realmaxY,realmaxX = self.realstdscr.getmaxyx() self.gui.got_click("Click", mouseX, mouseY) if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX: # we're in the canvas cmode = self.appState.cursorMode if cmode == "Draw" or cmode == "Color" or cmode == "Erase": self.undo.push() # 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) # 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.. # 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) 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 self.appState.debug: self.notify("bottom bar. " + str([mouseX, mouseY])) else: if self.appState.debug: self.notify(str([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.drawStatusBar() if self.appState.viewModeShowInfo: self.showFileInformation() self.refresh() def selectColorPicker(self): if self.appState.colorMode == "256": self.appState.colorPickerSelected = True #self.statusBar.colorPicker.handler.showColorPicker() self.statusBar.colorPicker.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 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): bottomLine = self.realmaxY - 3 + self.appState.topLine # move the cursor down if self.xy[0] < self.mov.sizeY: self.xy = [self.xy[0] + 1, 1] 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 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 def move_cursor_topleft(self): self.appState.topLine = 0 self.xy[0] = 0 self.xy[1] = 1 self.refresh() def move_cursor_left(self): # pressed LEFT key if self.xy[1] > 1: self.xy[1] = self.xy[1] - 1 def move_cursor_right(self): # pressed RIGHT key if self.xy[1] < self.mov.sizeX: self.xy[1] = self.xy[1] + 1 def move_cursor_up(self): # pressed UP key if self.xy[0] > 0: self.xy[0] = self.xy[0] - 1 if self.xy[0] - self.appState.topLine + 1 == 0 and self.appState.topLine > 0: # if we're at the top of the screen self.appState.topLine -= 1 # scrolll up a line 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 def move_cursor_home(self): self.xy[1] = 1 def move_cursor_end(self): self.xy[1] = self.mov.sizeX def move_cursor_to_line_and_column(self, line, col): self.appState.topLine = line self.xy[0] = line self.xy[1] = col 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 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 openMainMenu(self): self.openMenu("File") def openAnimMenu(self): self.openMenu("Anim") def openMouseToolsMenu(self): self.openMenu("Mouse Tools") def openMenu(self, current_menu: str): menu_open = True 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 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() def openFromMenu(self): 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() else: self.appState.sideBarEnabled = True if self.appState.colorMode == "256": self.statusBar.colorPicker.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: elf.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 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 == 27: # esc key if search_string != "": search_string = "" else: self.stdscr.clear() prompting = False if self.playing: elf.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 = "" 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.debug: 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') 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.debug: self.notify(f"File header: {fileHeader.hex()}") if self.appState.debug: self.notify(f"Checking for gzip file...") if fileHeader == b'\x1f\x8b': # gzip magic numbers if self.appState.debug: self.notify(f"gzip found") # file == gzip compressed f.close() try: f = gzip.open(filename, 'rb') f.seek(0) if self.appState.debug: 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.debug: 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.debug: 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.debug: 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.debug: self.notify(f"{self.opts}") if self.appState.debug: 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 return True try: # Maybe it's a really old Pickle file... if self.appState.debug: self.notify(f"Unpickling..") pickle_fail = False f.seek(0) unpickler = durfile.DurUnpickler(f) if self.appState.debug: self.notify(f"self.opts = unpickler.load()") self.opts = unpickler.load() if self.appState.debug: self.notify(f"self.mov = unpickler.load()") self.mov = unpickler.load() if self.appState.debug: self.notify(f"self.appState.curOpenFileName = os.path.basename(filename)") self.appState.curOpenFileName = os.path.basename(filename) if self.appState.debug: 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.debug: 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.debug: self.notify(f"self.opts = pickle.load(f ") self.opts = pickle.load(f) if self.appState.debug: 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.debug: 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, 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 [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 == 'ansi': # ansi = escape codes for colors+ascii prompting = True while prompting: # Ask if they want CP437 or Utf-8 encoding self.clearStatusLine() self.promptPrint(f"ANSI file encoding? [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 in ['png', 'gif']: 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": 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 filename = os.path.expanduser(filename) # 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) 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 == '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): """ Saves to ascii file, strips trailing blank lines """ try: f = open(filename, 'w') 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') f.write(outBuffer + '\n\n') f.close() return True 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): 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 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 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): # 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 # Draw each character for linenum in range(topLine, lastLineToDraw): line = mov.currentFrame.content[linenum] for colnum in range(mov.sizeX): 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 if charColor[0] > 8 and charColor[0] <= 16 and self.appState.colorMode == "16": # bright color self.addstr(screenLineNum, colnum, str(line[colnum]), curses.color_pair(cursesColorPair) | curses.A_BOLD) else: self.addstr(screenLineNum, colnum, str(line[colnum]), curses.color_pair(cursesColorPair)) # draw border on right edge of line if self.appState.drawBorders: 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 : 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'])) else: pass for x in range(screenLineNum, mov.sizeY): self.addstr(x, 0, " " * mov.sizeX) curses.panel.update_panels() 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 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() 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 - 1: 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() 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.refresh() 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.refresh() 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" 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]: 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, [D]elete, [F]ill, Co[l]or, 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 ['m', 'M']: # move # prompting = False #elif chr(prompt_ch) in ['x', 'X']: # flip horizontally # self.undo.push() # self.mov.currentFrame.flip_horizontal() # 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 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 pasteFromClipboard(self, startPoint=None, clipBuffer=None, frange=None): if not clipBuffer: clipBuffer = self.clipBoard if not clipBuffer: # clipboard is empty, and no buffer provided return False self.undo.push() if not startPoint: startPoint = self.xy lineNum = 0 colNum = 0 width = len(clipBuffer.content) - 1 height = len(clipBuffer.content[0]) - 1 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: 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 copySegmentToClipboard(self, startPoint, height, width): """ startPoint is [line, column] """ clipBoard = self.copySegmentToBuffer(startPoint, height, width) self.clipBoard = clipBoard 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 + 1, width + 1) 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 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.25.3/durdraw/durdraw_ui_widgets.py000066400000000000000000000621071455044760000212010ustar00rootroot00000000000000# 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.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() if self.hidden == False: 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.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): self.hidden = True self.window = window self.colorMap = {} self.x = x self.y = y self.caller = caller self.handler = ColorPickerHandler(self, window) 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("F", 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("Show/Hide Sidebar", caller.toggleSideBar, "s") settingsMenu.set_x(self.x - 1) settingsMenu.set_y(settingsMenuColumn) self.settingsMenu = settingsMenu # 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, "t") 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") 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.setCursorModePnt, "d") toolMenu.add_item("Color", self.setCursorModeCol, "c") toolMenu.add_item("Erase", self.setCursorModeErase, "e") toolMenu.add_item("Eyedrop", self.setCursorModeEyedrop, "y") 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 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 # 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 = colorPicker #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) if self.caller.appState.colorMode == "256": self.colorPickerButton.show() else: self.colorPickerButton.hide() #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.toolButton.set_label(self.caller.appState.cursorMode) self.drawCharPickerButton.hide() def setCursorModeSelect(self): self.caller.appState.setCursorModeSelect() self.toolButton.set_label(self.caller.appState.cursorMode) self.drawCharPickerButton.hide() def setCursorModePnt(self): self.caller.appState.setCursorModePnt() self.toolButton.set_label(self.caller.appState.cursorMode) self.drawCharPickerButton.show() def setCursorModeCol(self): self.caller.appState.setCursorModeCol() self.toolButton.set_label(self.caller.appState.cursorMode) self.drawCharPickerButton.hide() def setCursorModeErase(self): self.caller.appState.setCursorModeErase() self.toolButton.set_label(self.caller.appState.cursorMode) self.drawCharPickerButton.hide() def setCursorModeEyedrop(self): self.caller.appState.setCursorModeEyedrop() self.toolButton.set_label(self.caller.appState.cursorMode) 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.25.3/durdraw/durdraw_ui_widgets_curses.py000066400000000000000000001063671455044760000225740ustar00rootroot00000000000000# 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): 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() 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: curses_addstr(self.curses_win, current_option + 1, 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" #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 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 == 27: # esc, cancel prompting = False else: self.caller.appState.drawChar = chr(c) prompting = False #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): self.colorPicker = colorPicker self.parentWindow = window self.x = colorPicker.x self.y = colorPicker.y self.parentWindow = colorPicker.caller.stdscr self.appState = colorPicker.caller.appState # figure out picker size #total = curses.COLORS #total = curses.COLORS realmaxY,realmaxX = self.parentWindow.getmaxyx() self.realmaxY = realmaxY self.realmaxX = realmaxX #self.height = int(total / realmaxX) + 2 # enough lines to fill content+ #self.height = 4 self.height = 8 #height = int(total / realmaxY) # enough lines to fill content+ #self.width = realmaxX - 10 #self.width = 78 #self.width = 38 self.width = 38 #gridLine = [[]] * self.width #self.colorGrid = [gridLine] * self.height #self.colorGrid = [[0] * self.width] * self.height 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 #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 color_pair = curses.color_pair(fg) if col >= self.width - 2: col = 0 line += 1 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 if fg == self.colorPicker.caller.colorfg: if fg == 1: # black plain_color_pair = curses.color_pair(9) if self.appState.colorPickerSelected: curses_addstr(self.window, line, col, 'X', plain_color_pair | curses.A_UNDERLINE | curses.A_BLINK) else: curses_addstr(self.window, line, col, 'X', plain_color_pair) else: if self.appState.colorPickerSelected: curses_addstr(self.window, line, col, 'X', color_pair | curses.A_UNDERLINE | curses.A_BLINK) else: curses_addstr(self.window, line, col, 'X', color_pair) else: curses_addstr(self.window, line, col, self.fillChar, 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 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 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=}") 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 = curses.COLORS - 1 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: color = 0 #self.colorPicker.caller.colorfg = color self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif c == curses.KEY_UP: if color < 32: color -= 16 elif color > 31 and color < 52: color = 15 else: 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 color < 16: color += 16 else: color += self.width - 2 if color >= curses.COLORS: color = curses.COLORS - 1 self.colorPicker.caller.setFgColor(color) 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 self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() elif c in [13, curses.KEY_ENTER, 9, 353]: # Return, Accept color. 9=tab, 353=shift-tab #if not self.appState.stickyColorPicker: if not self.appState.sideBarShowing: self.hide() prompting = False self.appState.colorPickerSelected = False c = None self.updateFgPicker() #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.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() 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.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) #curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) #print('\033[?1003h') # enable mouse tracking return color def gotClick(self, mouseX, mouseY): if 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 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.25.3/durdraw/durdraw_undo.py000066400000000000000000000062151455044760000200010ustar00rootroot00000000000000from 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.25.3/durdraw/help/000077500000000000000000000000001455044760000156565ustar00rootroot00000000000000durdraw-0.25.3/durdraw/help/durhelp-16-long.dur000066400000000000000000000515501455044760000212240ustar00rootroot00000000000000edurhelp-16-long-newlogo.durko8ր36dG@7gtdf(6{k[IN&3I]l]HPw92ˑCzxxxDx/%~1Aɞ[^, }?V=/TEp`k[lx>6ޡxś5_n聝{l5EiKǍIti2D :?#dxfkdfZEE9d'\؇$pdW#٧DEGҟ;ASg)_f?6 ށԷr~[hܱ{Đe s4$ΐM-n1-=FxLVv in"cme8Ws35sdL `;ҹ&.붂{7GaPEuUϗrsGe|T7eSOT}5U -7&m\l&POП7F'lؙm Ewslϛ; o saCom|K_Qa&nſIO2?+|ea=/3᦮_rm")fs.fKS}z3sZvN* Sl״݁Z[5ol_k>wg[ĤShg+j?E9^nfz>2HZ;`7^҅H$G"w"=I_Ѳ{M("W$$ge=)g{SYѲ_e߮|Zmd{ʓ,+Ӷ;}guXh; VP7?ykȦQ34ue=%%b u$켜:!ҙ!~.WTI;q_k,7h7;~3 sso:zwu`r-rEU/$n3xx={,nT6bƗo"~F җTYnn-tBĹzYz _OZ[Үl-EQ@%^yԼm> gfc (XT BdFR[_gVc.I퐐@v ˴$vg]KD4=S' |E Ӓb[:L;%9 vg;יtH$ Jn@DaN3EL7Uq3C? W8O8)#!mI )HAҳȡD=DeXLAOۗ EWЂ~~;?c"$= zTSV?A R!$z=xfw( ~$/k!O蓡~߻y hc$+b%KI">lL}  wOsi9̖W_ 5XMH?Oxc74}Gr7yWĮfϟc?Nxڧ7l)^STC!Mu4=CSMLj7}j/U2./'-;U;3ԥaݺ iC֡ݘZ:,Qo/.<'K&iw?]{_>\mmt$j73ҭ-F)ZS%ݑX\WءW9x5qSٻRy'h[g2|bwřxemx2[ lM̄0Gyt#%xx8=9no1p|pH9A7Hޙ[yCWٛ˾%~]/mO/e17.pI'ݨ!YE~UlF/qؐ> NQ7AZ#ey//{Nʩ&B)7F/TF9m'cM s,K汣$UR3! Ck x _ g哪zQUO[نjPG @ɊOiw**F5\{_#FjS*z!qf`gSgªEWk|QN ]=%1_Oqe=Ľ.PW@q$a?m&NϸB .{xWþ!攲% dS2Chύqw< ):25*sI[)5aԎsG-6&kpM"{qE _|!]~K!Լ*=LW>G/o{vmay+WHQt0:0'YƯ֣[}plZ y=^n-jV\ 69n9.kԞeyՊFh5Sޘ@*UȒ$BnH?qا ֨Wߪb;Sk(-ȑvT/Cׂ qˎ\5,8.!7|V`UYGlsl~Sa$e DE@^jĖ07ܱ`=^e(J>>kK-dMt3upe?J WtlK^yfDOP[.^FI(ǀY=ɕzpAZYsjB2,X>+&rgZŨr r.pbBu${E\?ΐǿ)ѥ6 ĐkZԉr K,dYFODiZu\>'.rAX։6.1t~U'Ȅkf^wB0pɂ ގ/9<|] !Iq" 66ZSo0~7S wQ2HFDeV*/T7<+".gH#usvM0*?q2 aW;tITH?^pCU<cKAK4n@ p ,EG `MP4cW<[sW;^mXB-^?lk?+kwD+yFK@tKUDg$7U"Xgqj`2Z17[,2Բux3l.Iը- DO.Qnׯ!Fl .N-N/ZVˎhZMEP Za鮯L8@hTH5r`WjLHY$K''|Ί*(v2Do#۫Ptgu_m=8+<#WO>F~GYȵ+I[@6m{,V)._ȭ#aԑр\ . Idt-= y~ܠ5$sO< f+lVͽ՝ 6KyQ wWTTI@7XMRXJ:_%~1H:K_R C7f ~g/|=h9SnIF1\{RRv[' `AR-j+ QInJetXһ$WﯦoO:X3vCB9t.0o,Ӟ#إCw9/\O4\5Ld෢?LKRn2+2T,,۝\g5"T0\+]G:E[\:nXW0ST 6~ǙFGl.^s<ȎKj'%bBLh# K#a/b=3?]n_.]C 9t/HJz )ROOcnXeD M'HЇ Mr(*<钌;^ׯ=O6rL}:0ytߋl:/%PԲ15l('#?υo0[+_ |%@v|c5!<D=u}GY#]}Jٻ?8Aiߤ{ NES Mh:BS4 M 4#z T|>tT9llZR&&u>/ Y[vcj#o?\_ dlbG>+t/iK%iw?]{_>\mmt$j73ҭ-F)ZS%ݑX\WءW9x5qSٻRy'h[g2|bwřxemx2[ lM̄0Gyt#%xx8=9no1p|pH9A7Hޙ[yCWٛ~x;#~]/m Вr \}B ήjH] ah+=-T:ԪVv*~ &RRb0# 0,~\ot!Tff#τUxe:&+@q_zcaX!RsΪc%ƂhRo`p04R s9\^)!9i &(JY̆8WL/딹$֛ӿ9e:9š,'fz"+EU8]@7ʳ8'(Lb/G7 6/a_oԂ.+.h];f VWP7܄U'.-ͥxUj8LBVB,A1>K{nUō@.,X(C,YlG"os`t0W™/lAmf6zs)e~xBwe _ 5osi7 3." ^QTPϚSqܵIzK5 =́lnܠF.JFoUt ծ{Gfzك<$+oӨ(N@ WqEOzaQ Ž2O(q&a?@ܤ&NO2.{xWӾ攲%dsvLsC^mI,:= $Rk7è ')jfxT Ee1gqq\ClJ9LRtR3{`3b0*W>G/o{vay+YHQv0:0'Y֣֯[Npb>`CAp{Ÿ[έ8q3mnwCmsr]֚=jQWUګn#%I ~2S­n}PLӣP GQ1Ͽ ] ^3fD;,;s֐ ĖSENZUUĊf=uKeMi|F6mYyEXnVZ-87ܱ`=^g(J>BkK-dMt3upe?JVWtlKyfOP[._FI(ǀY=ɕzpAZY9V7K-=孌 Y1n P]<^׏3!| ltiM)1u\KY@<Q>lu]#W+ɭ\֭uKr][92Z8]ƨ `肷*K:i{fB+~REnb !.g'; ͝xT*Gů+Qٸp Ǎp7ʆȆ>vkA8$i5, mO֢ZXmG=ї͏U#9;A͎܃Z&Q*k"b'MGHi -T!U b^ifeGPncT:zG4[ h) P|8B~D<} sŭm/&hxGOYo\|i:Kd2)޷f@P4xaۘ1-uS^Z-낼]dd@Cqi Xj0Fj0J۱ñ3YHg+*%ٻK%TXAʸ=*y[&N6EST_TՉRAVZ,ی$<\Xn#杙E(rAv4 R&4N+-HW3ώBr"LrRڑڅ]ݟ?O OoRRC4)hzoN^e>]^\O:Zvwg6-Kúu ҆C1s ͑/u2Y6^_\xO޴ݥ4廟/.uG tтuÛ֖nٔtԩtK,.+tg PKͫY)])ּpݭ3 }>{kq-T`&fBtkc?<<؜Y{Y8rb>g8$ݜZz-+Me?{6~hIkz^k`gW^C5$.0q* jU+;hZZ)1D FH  ?.R 7:IF*3~P"mE.|%K[A|d$qMنԪ`PaU'T*'Ί` tD!qf`gSgªEWk Wc'h>D;pGx{¯v ۰\[ U/~x+WX50;.W3+ 1̍3"~ -In>+Eh"eִbnXd253>ܮ_C:\ZJ_,v]_pC#5 ЪjZ6s%՘)IyJ R)cV:x9ɘlʹ 9♷D]a0L\`Q#LjF5W^iɊedaպj;IĬm~ jv,6W*=PY 01XSǴ=oʥ8BJ[h 2zH0^F/8 2/uԳ@}Jdڸx:dHV/N;5FYJ$㗙O 7+w ƌQoCjY200-8$LPwx&$@KSR0V߆`Vݎ*,F:K%^Q)]5,!Ɗ VP-<%, -5 u)N̔:bf$NJ@p1,"F Q.9/U)B'0e'ײB~# Ef% AoHB58pGh@Fצq4]ilF"bmyv4oe;Ҏp.,)݇>I+ n0yH,"PYWTǚ0;SIY0Փh)c+[wRd%#DfYS %ַR(5ƙni m_r-SELL{>ܒj17cS5k^!a$o>y -6իK<-@dęp+0c9g2wlD1d ɣ3$DSzLKw,71$R(X/-2ռC[X[r|.;Be:2~t˺ ͑aT@]?)''ܑ?ՍsTi4U_MUBˍIFi3a Ƕ6'Q [:vfv["}wY+ˎ7 v9&x@Æ>-ߗ"YsR43D ^-ܰ7 TIG }oݰ7eƗ>8rS2YMX$ 7"}Wl~[OpfN"@ZzÔI Pvt㚶;p\k9-Kxlt lecT@5'sK@R-^oX&״T ss#>?_<6KRGCdB +Zv tXD5dବ0@l/v{:+ZەOlByҝ}eڶx\=k-9|g! z'9om_ٴ3j撦l[DҺD~!SG>Dr1\:3$կ rxW==0IX17wWw&X+,"YD ]RAR6s'ݼb6Iec) f|&k 鰏/}I5 A`ܘK'Di`KtHO%b_d\r핧KIrf6mI,4o@Fo$o++uQa5K__?=?` y dЁ+~L{LbzDDs^?upW0]ߊ0-)&JٺCȴS_lwsI׈GRptt@oq4[a^O}S5gZ=9׻`{uC";/vȟ` 1d(0.=;ZڎKCT濈̔t} ϻ`Xt-3&MӾ )遬Hu>%<1__a-4 u@Bӏo&~7AʉK2Hz^FOo>1 6O}/V/BSװp?Fli|50Q;Մw1x{C~},-gpw)]Ojf9v8.~}Jʖ58M54MOTG345twRU?.|ѲS帳=iI]֭@L6dmڍUhp}ɲ³xҽ.)tmp;]\tNHt5ΦkNfwG[bq9^ٷ;[`Zj^A z {gfn ]awgo,w4+@KZ-p_ 5;!vY4,S_ PZA5КHJ!R0Œ(DXhqZ IR0R m/t+!u,v_r  $xd&#kR6VXdˀjS\ն[4 gU l(S8k *.qNVUYYY Ɯ:q0k/b- e%J :0971[ Sv&3d9 ȭbǴ\䍌ު(P\6Rqfce:sNU\3㊞w2>{e fQI~v3n+"Ud9l FT!Dzz P&scx]9!ob`':5 R~gk7è '(jãR.(<3D8| $fkxaI2W61,I@l^xT#=;V|ư (`;fw,W-`On']1Z y=^n-jV\ 69n9.kԞeyՊFh5Sޘ@*UȒ$BnH?qة ֨Wߪb;Sk(-ȑvT/Cׂ qˎ\5,8.!7|V`UYGlsl~Sa$e DE@^jĖ07ܱ`=^e(J>>kK-dMt3upe?J WtlK^yfDOP[.^FI(ǀY=ɕzpAZYsjB2,X>+&rgZŨr r.pbBu${E\?ΐǿ)ѥ6 ĐkZԉr K,dYFODiZu\>'.rAX։6.1t~U'Ȅkf^wb0pɂ ގ/9<~] !Iq" 66ZSo0~7S wQ2HFDeV*/T7<+".gH#usvM0*?q2 aW;tITH?^pCU<cKAK4n@ p ,EG `MP4cW<[sW;^mXB-^?lk?+kwD+yFK@tKUDg$7U"Xgqj`2Z17[,2Բux3l.Iը- DO.Qnׯ!Fl .N-N/ZVˎhZMEP Za鮯L8@hTH5r`WjLHY$}Jdڸx:dHV/N;5FYJ$㗙O 7+w ƌQoCjY200-8$LPwx&$@KSR0V߆`Vݎ*,F:K%^Q)]5,!Ɗ VP-<%, -5 u)N̔:bf$NJ@p1,"F Q.9/U)B'0e'ײB~# Ef% AoHB58pGh@Fצq4]ilF"bmyv4oe;Ҏp.,)݇>I+ n0yH,"PYWTǚ0;SIY0Փh)c+[wRd%#DfYS %ַR(5ƙni m_r-SELL{>ܒj17cS5k^!a$o>y -6իK<-@dęp+0c9g2wlD1d ɣ3$DSzLKw,71$R(X/-2ռC[X[r|.;Be:2~t˺ ͑aT@]?)''ܑ?ՍsTi4U_MUBˍIFi3a Ƕ6'Q [:vfv["}wY+ˎ7 v9&x@Æ>-ߗ"YsR43D ^-ܰ7 TIG }oݰ7eƗ>8rS2YMX$ 7"}Wl~[OpfN"@ZzÔI Pvt㚶;p\k9-Kxlt lecT@5'sK@R-^oX&״T ss#>?_<6KRGCdB +Zv tXD5dବ0@l/v{:+ZەOlByҝ}eڶx\=k-9|g! z'9om_ٴ3j撦l[DҺD~!SG>Dr1\:3$կ rxW==0IX17wWw&X+,"YD ]RAR6s'ݼb6Iec) f|&k 鰏/}I5 A`ܘK'Di`KtHO%b_d\r핧KIrf6mI,4o@Fo$o++uQa5K__?=?` y dЁ+~L{LbzDDs^?upW0]ߊ0-)&JٺCȴS_lwsI׈GRptt@oq4[a^O}S5gZ=9׻`{uC";/vȟ` 1d(0.=;ZڎKCT濈̒ Bܾ]0,s`&i_@S:?//ܰ N:!7D DQUx%w| $F_7 {B wmmu`A'Y+t|_JeckPNF N#a4WJjBy;{E?> Gɻ'v5w;?q>IeKthh:FοA;U{*tyq}>h٩rٞٴ.ML } &_H*4G~@dĎz{}~qYW<^zvKӔ~|.H. E :B'of[[gSҵRKt#-[ӝ-C-5rjfdw8[Oud0IJ3ڮ=dptRٚ aҭFJpr{\csfbe[ˉ\ tsoh=~33󆮰7 vF һ^%yݯ]y ]%b20q*ˀje@k"U+%H3 cEj F'IHe/J H҅Ա0}i5hO$I1ېZcr, _; QbY̠Nr0>A"ЬR7cj6LXuJ`gQ6n D8w;R.5笺1Vr`,f.! 6:` 'z0a!L!K biN;qj*U@ڸzہF`R=ЊAl#puNKi :#nlY[Qf[*ra*'B2zX`]JC{5Z.p tí<|r$R{t`r՘VA-xr5o c&pn~pX@M\;Ϙ oyYВ\WH=SK'Xm%Ģ㳴m,vZUAn ςuH>b(6p9+0aKa>-=]?UmEpK}0'nܨ~t |m@_%%b J͚`7չ0e]{]nlX/[)r*3&`so-lN^\2у|v y#*:q7j+{fzc<$ oVA|•S\ QOq!`7Ic붊e!NϸB .{xWþ!攲% dS3!.?wapț&䣙Ta4V t͢fX#Q{uٯ浇B8L& =߸"o>m.%QʐNj{Op^ "`kpN0*W>G/o{vmay+WHQv0:0'Y֣֯[}plZ y=^n-jV\ 69n9.kԞeyՊFh5Sޘ@*UȒ$BnH?qة ֨U˥v(P(Y_b3kmjHMp`K]?@m&X*`ESk3~S!$e{ DE@^[j0 7Sܱ`=^_(J>2kK-dJt'ume?㞙JV WtlK^vfOP[.[FI(ǀ=ɕ:oAZYwRBS2,wX>+&rgZr q.pJBuq${E\?ΐѥ6 ĐkZԉrKK,aYFODiZu\>'.rAX։6.1t7~&fkf^w/pVȂ ގ/9|m#B\,E0t+%l\m 4q9tW=an\RQ=*~e^('ƭT3^8nsV6E6\_FX9 %O`aLSU~Yer/uV钨ߑ(d;5*7  Wc'h>D;pGx{¯v |۰\[ E/~xWX50;*W3+ 1 3"~ -In>Eh"eVbnXP23>ܮ_C7\ZJ_,v]_pC#5 ЪؑjZ6s%՘)IyJ R)cV:x9ɘlʹ 9"D]a0L\`QKjF5W^iɊedaպj;IĬm~ jv 6׍*=PY 01XSǴ=oʥ8BJ[h 2zH0^F/8 G2/uԳ@$ſ`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.25.3/durdraw/help/durhelp-16-page2.dur000066400000000000000000000062601455044760000212610ustar00rootroot00000000000000@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.25.3/durdraw/help/durhelp-256-long.dur000066400000000000000000000673151455044760000213200ustar00rootroot00000000000000zedurhelp-256-long-new.dursQG6vәnfMI;ߝ%D4s &12H!8:?BGÑeOUvt˄kf=NѣQpffm0LH³ԬswtO\(ڮGF6DUD;ku7IƔ~UõU8` cUn(܇Azy :+4IlmekǗίϮ'8z*u|[P,Ft"ZA3XE;a c< ?{߼RiAtxF rsw^9AzK/ $:ŲH(H~~"|;c1^h  E)!b]"‘-.Bl]<.h&!?ڽ5w#h(Ԝn:MT[\lcABgBI±g 5Fc)TZ{_H'7—7jwr8^*7}(t R7p0I9F/էxI1V VdJQ+2n\tGCm(FgvPRԫDSlΙsO<5@j>NB^ X(܂T DP/mn>Xh=G"܆h)cF/ O qD@Z͑M8pǼ ՜_9l.qQZ?@zO2ni=Pwv_smPd-[?hlX|h3B$_n.wz֜<{1{K gyCkГ^i bNe~&D-:0i7L,3~7&*t+@#~{.\x+ b#$>E7t`ZjۅrE 3 }[MFL!lYޙ4aA77˂pX"Ax0ֆn-@9D$_p  ?ݼ>uZ&ȁ+ޠ7o-z  t uoߗ=7җ#AG 6/;0[rTec7=Wسgm3ccCqVtSe0'уe"1Y8Juwi'|)+ nS a {hxhx2PAFޒ$פn^ ~9'jn{֋NF0}[kkE5.x!踼Fn// 'x7\dhB:С3Ԡe_zcYqTW`K^w4$s) pL,le R\oI']EHrs\6 b,:9s,;RuYc8hr!0g캝uo{/j1V1fYe0=pTpI'И %yE>p 6,X26IVaN bv{JI k(eX ҹ ui/7 7 sLls>^80odn(+š~Ud :,gbI3+TH3S 7/]YZ b'ӵr0g1WΛ ř>s㗝wAZin4t*` Wf_RLgA,ò"g!8>E(Ks]-)9/hOhf qN5w6fy,7$WU1^@rC2ɂ"ePn@24eeZB .sbq6z M؞GeE7 |)mxbPmhpiEZ9jYQC/RQCj˛Q܂G:(ު ,en|UWھ)眫³9Kk>mψ#pUBÿNKnT 5](C2%cXn >){ Hxdp,"f)YVdOq Mm3{)IJ )w3W ׭}C僚8s8[#\ O2NRdҾ,.8sy*G@gr,]!қ73#ot %z7~cWJrEMUѝ]Fo{ tfsWβ$gp~.q,+T~cy s\J L6A_dj8?W;-mS\t_q .; p`FþY 3pO{"ʩߔjl7xKk:f ^\}KL5$i;. 5Z<o/T|8|0QbK!9 ɛSNL o/ H<͝ao n+ uf޹nts>d@\ƚox?U.amm%*Ư3W[Τ}]zi C6^{pQ8Y-n bKmɰ<{uM[AtUnSZ hY, ^~hcqW^`@,RִWFSmhͺ 4ZpUW:6< ŵG4,Όq\uLΊk㚯+Ƿ[_F`yPF"zvcXvlr)gۡUX]KS/fǢIz6k ׵Io5VP\}s g&pfC?CnY:O}4@݈lprLLoW.uVVp|y`zu|[P,Ft"ZA3XE;a c< ?{߼RiAtxF rsw^9AzK/ $:ŲH(H~~"|;c1^h  E)!b]"‘-.Bl]5/(:ݚ; |p㪶v\kf}O5m aA% U/ Og5oH&9+rf֩Ÿ!~6˙9h*=)s2 sP[icE7t`ZjۅrE 3 }[MFL!lYޙ4aA77˂pX"Ax0ֆn-@9D$_p  ?ݼ>uZ&ȁ+ޠ7o-z  t uoߗ=7җ#AG 6/;0[rTec7=Wسgm3ccCqVtSe0'уe"1Y8Juwi'|)+ nS a {hxhx2PAFޒ$פn^ ~9'jn{֋NF0}[kkE5.x!踼Fn// 'D]q!7Qx~mA{E&& :cM mXʑ~> Lu v64k7v!33uh3gn?UܲqnZ#6n$} JM/-2Uw%OOO5a]Q#qcqЅI  GSgd ]cwnM[x4`|hYR|]^⁲f( )9("1_E;S`,v#,LwN"V':J?SnzuY'_sNt̀(XePy|LxKcg*IbK0eFX$T?V7un#f t}\K Y֯1sZ [L( Xfb8s7Wq3YAϝ8,IfD%!6,K Z,Kw)c,3}Y@:P.W"8: 70$hjϐkN.Ȫ3wVAN70\{n| v-ݼccjaNv8*$hLCݒg,ذ(cYXr^|;p,ݯɑ aN bv{JI k(eX ҹpW4 2'Yy#s;DY)۬'mtON,ljqQ&ͬkR!Lm̷Hs^a"`w 13O+ gT3:Y  hhK,LԥDz"g!8>%(Ks]-鉆4fVT ft^ 7YnHbީZ@rC2ɂ"7 G8s6M9\(Vʢ8 8IFI|]eahK * #(P< tKo, ҽFG'T롣ǎE\JZ+ g5UaDwv-Щy;]ig8˒PJ¹D{E{RA4Msm*q*63!^0Msy\f~-'R2e;j>Af1d:r"=iWc#~+~SfEւ|h-^uFo/ߛ=4x:hޞߛsU/m0pvO|3,Լh\|R oDn/D-,$oN!8mH2b4$7$$4wq1i73CLיyf6n0.э.oYqClN kb n @W G wv~Ϥ_n!:"te- л[zEL)EfU?5^-q4'E7mBMTN˳j-eճ4xQzY ^xJZ^ O*5.k U\y[$׊[";3q39+1>wk2l~Cv,Ïa%ڱ%Ci0ߟmVau-OYv&٬)f';^&e׀[Aq91 Q8 gyhdSa>">ñ12]l][Z偵ó Vla6B ?Cщ kϬ_bA>@>Zc;|A>dp"}J+A%]xQ.d0Kx~><(rLzJ# ŊE[۫lbџd{-8vbV/o$\lu^ GbV ]t񠻺9 79B-.|P@馃:DUvk?b-sτlcG;@h4FeXz*N /$Ad7—7Mz2$5m} !Hu N10*UN*yMusƠgpgcն'Kl` ܅5{^>c VP0b*CjDg<)W$G")F dՊL2jE tޗh cC ۞BuPRԫDSlΙsO<5@j>NB^ X(܂T DP/mn>Xh=G"܆h6F/ QO qD@Z͑M8pǼTs~k@A8JS\wGi=ɸϏ@9ۥқ͵ O7Cr4clM1c [DkY̤ Z4“F>_\ߝ_ U,Xs<#﹛;N0-~@*A9CO{Fk9?7q8>A]4r/ä[ސ3lWΠf(ݘ$Z'ҭD Bκpr L&|蛋c|S<Ktuthm~,Zl103 l61AʳBdyg?`҄{?ĊVGkߴ'g/ UbXZߒE2x3tv$iu:蚜#n z޼A--m{׽~_I_ u,:HHlQi‡I>_bZ`^<}H6d ?  WjXMEu_Ø,Gzd&(1&ZץͻJs O]4 hGCyKt_y7ȿσ:(Y/:[# lCZ0B݇KD\9&?^vuDž$?xGyf5 .+t965hctbB+GƗ^zV3{/3&!W2OЬiܜ]1ΜAWq˲ǹiذfSlN-(6: pTݕ|?==SjhwG,Ǎ T A&)hT.MA(tݹm6]RndɾRgo#oHvi{Ӌʚ(x' LWĬS~L g,+0;Q,Z=n(YL]1e|9/nl5`QBM2-i}כ'-i~`)RJpXtֹK5Is-)Id);ȧ[Pi5ly2|/|o b8ݸ_@fy>w0$si{Spذ\/)0k,ޥehz@OB\, X8.h->C:#N9˿^[Yc8hr!0g.pH o{"۵t󆎱1]b;{38N1 -,wKrV `â\e!cyWoñhw&G6:i >4)%1,aa6bJn^/7 7 sks>^80odn(+š~Ud ɉa'˜(ge f(f//|4&fq}\Y8K ΥLɺnPm9 hhK,LԥDz"g!8>%(Ks]-\HSf6w6fy,7$WU1s=>W^v <$ T ,u}q(7 mr!d(p6ϑn_'`'[̯`KpYD,uUƈ ~A SjL.a ٮk8HF\)i+0kOzXqCq.M_)R\a}7&XVpC7FN>&!AabV8(X+G3+j)jHmy38[Y]~[׍/JWtRx;v+ꔷڃ̾ɕ6nVX6BMSqfחuU"Xr9N{'Jẵt6tgrwWyj$#\I9 7bLڗ{Ug.W( s_JV@ӽiP;A |L^8K78]zfwfq5::!B^?v/b?TZD8[ #|oNyJ;Y,8Pε%=.tܓ Yo{ 0oWa Wɵ&L "jm+Tܖ0+n9eX.Qr7|&I88~O[92[-Co r2RGx{+6Azi3$mǷ{e@+Öox'rx|&Jl 4$g!ys iCrI!!9ɕ3aMcŸb;7s_qquqԍntqxӇ bc_XMpc¼W8y: ͸DU{&5 w љ+C/maȆ+brO/ 77Rl=g.i6np*\Vk-m3* U*՚hx W Yw!PV^K@Zdž$V|@ؙ1.Yq{\u1?` ,_*TD8<`~ +ю-ZN5l; k~%X4iVfM6;6)-\ ˡo>Vag?SG#[=$"3,ŦەK_X;z83X,O&,7Thr~Q݅WNK6ëb.DŽ/g4 -__q+yL8ڂc/hFkQEȶXźp$v o;Ec!?ڽ5w#h(Ԝn:MT[\l#2L I8v FcYez B??XO&qS| |yCԬ(Cr8^*7}(t R7p0I9F/էx'm ^q& w&=Vmzr]X5c8?k/27@4yƓr~EY }$(bDɭ@VȔ*Vd@}l_鎆8P86 >ΰ)T%5~MM4朹 =tS[3 $TUe8-He@ E,\})mfcl uY 7_N T;Yۄ ~oL5|ᗾf43x4uwޓ[iOxԝ] \t3T>!G16YpyO 3>߰5A+LjЮE#1QK#/2L 9v jߍ J"J> 'l‡H;7O3]}D>_Gvwf\* ,9Co<{,4qAwf,MgChu})M{}, \k-H[ P.Q-W4 -Á7O7oO>@V9r 7x;?'}}hM>@Q t0̖&|h٘tM..h/o1?mهdsLƘPS]mb/y% ͚͹wL~,{ k6Ŧd~߂RhkSh r L]S~<ŪfF{WprAqtaBFB؝f%MX9+u96j:xwr2{J΄9}5H:WΔ0Kx}>]Ȳ2 SȢ扎%ϔ۵q]לb&]3}1 x%T$*SҘʼ{py+ q2I(UMg:Y@;גŸD|k VÖ' ʧlo·Ʈ ܍U aws:3K97gI V/] o_$% 83䚮10̝Ue> F)sƞ2*2]K7o+sZ%ƽӽ8# .br$gYpx 6,X26xkrdCCRynq= t6{r2ǼL027!Jq(f@>nC{rbAljqQ&ͬkR!Lya[900Sdf,;Y9pNKř>urV -ԣSs<3•闺yXV,ǧe2wrW1e12|G3+ sѼ#6fy,7$WU1ܫ^v <$ $~tpC9lӔ UI!+eQl#E;SݾNM5 OϯR~`+_.OkX*3ʩPYf&2NQ5q döv E@`rM@8 />eec!w ƹt1ˋu8qE&ޞ(`Y{ _sJ^;,ՆيWZi`ά!^`Mw(n#gunoU2^7+m_ߔsIY5]y C3)!̮D Bs * &uk_4lk7ݭęU^j|qByfպ2 8\^%(P6&|Ԡ]M0e# =q@np"kttB@I:z_Xp\QSFtgmܕv, Yp$kK7z\4ʹ'D4`&k1M",Oh-N s9G.-aWr,%\oL,'ܓv5FqpH;r7eZd-(͇ZgCW>mୃ)9W_fS Iڎo73B;V/ˇ-N_2LhHBӆ*\d6+=Osg8Ɗq13twnj*5<İ& yp rwX|qlLj3i_/BW^ W"_nVu[nRwA3{2,^]tm-DT3|N45LL{ڶx fkp~l! _LeoH5Uh'D1HP$ň[Z)UFȸrپ qpl}aSJjқzUh9sz閧f`(QGIP׫йp[E-YSh?ɳ03nphvP+9 7ߘj/}h6hg8[i( W'4Ҟ;gAz󯹶fR[}Bfcl@ 6f|,akhuW4b]Fx/7AkNg=ws FHг-&#&]yXh, Xp0XSrdYeA Z,s <kCkw][h8@R[onPd}:N]s_}AoЛ7hw~tO7|?ˑSv`-9*Mв1+]\Ђ_L] KbAU㙌1籡8xZ_ +kA2ؘ,D%Dqoy?]~nZ_)а=4-5sHFhpsȐ t\^w7痗ۄKڮ(4W,sm )>.m/uz@Y3dsϟ0FY*"_Fc g+YfaJY~DT}WOz!.;Rf j᰺s#_'1ksZRRvO~jdB5_2qqNJ8}\afI"0#, aR`eYLc4ŸvX9@q\!@Ek ccn_ ĸwgķQ%q@cZ,XN O7 dEBƒކc~Qlst@|h#wF3ynq= t6{r2ǼL027!Jq(f@>nCrbAljqQ&ͬkR!Lya[900Sdf,;Y9pNKř>urV -ԥSs<3•闺yXV,ǧe2wrW1e12|G3+ sѼ#6fy,7$WU1ܫ^v <$ $~tpC9lӔ UI!+eQl#E{SݾNM5 OϯR~`+_>OkX*3ʩPYf&2NQ5q döv E@`rM@8 />eec!w ƹt1ˋu8qE&ޞ(`Y{ _sJ^;,ՆيWZi`ά!^`Mw(n#gunoU2^7+m_ߔsIY5]y C3)!̮D Bs * &uk_4lk7ݭęU^j|qByfպ2 8\^%(P6&|Ԡ]M0e# =q@np"kttB@I:z_Xp\QSFtgmܕv, Yp$kK7z\4ʹ'D4`&k1M",Oh-N s9G.-aWr,%\oL,'ܓv5FqpH;r7eZd-(͇ZgCW>mୃ)9W_fS Iڎo73B;V/ˇ-N_2LhHBӆ*\d6+=Osg8Ɗq13twnj*5<İ& yp rwX|qlLj3i_/BW^ W"_nVu[nRwA3{2,^]tm-DT3|N45LL{ڶx fkp~l! _LeoH5Uh'D1HP$ň[Z)UFȸrپ qpl}aSJjқzUh9sz閧f`(QGIP׫йp[E-YSh?ɳ03nphvP+9 7ߘj/}h6hg8[i( W'4Ҟ;gAz󯹶fR[}Bfcl@ 6f|,akhuW4b]Fx/7AkNg=ws FHг-&#&]yXh, Xp0XSrdYeA Z,s <kCkw][h8@R[onPd}:N]s_}AoЛ7hw~tO7|?ˑSv`-9*Mв1+]\Ђ_L] KbAU㙌1籡8xZ_ +kA2ؘ,D%Dqoy?]~nZ_)а=4-5sHFhpsȐ t\^w7痗ۄKڮ(4W,sm )>.m/uz@Y3d sjuʯ")a0L}v‘ee;{'JE%K`ß)k=㺬9'QrۍMf@wb,2Jw%?,et9-O@&Oo]A,31, uf$r3"uo Β%f\_%ڻ1޾, @sM(Ih+eK\q4g5]'c`də;gk | R.=7 dsTdzn1V1vK{{qF|;\ 4łnIqr}AlX,d,9/m8Ȇ0O' ć1;=$52,z@Lm+dym`. en72CPNͪ|݆NTrF4¯;J43p̷Hs^a"`w9pNKř>upY+ hhK,LԥDz"g!8>%(Ks]-\HSi4;f!n$ {ٹ*,7$,h*ѹQ/\ MS3WJYga8HTs|S͂s[-,[ vyz_ERWU!`f)Z rH6lkw,dJڊCSv^Ʈ>qq`KWg{Wdhb w+5S+AabVkwfE S-oGq 9pxЗU]isN b'}Y0Yuh{F9%k>b2e`B8jv{LWvOGFeF09"6q̯2LMe8孶 xr%ŠVPihӔgUcuHXh7N!dRn&Uւy;8sxU#\ O2NO=ϬZWw^ǙU޹9ô߀tgT FP,?d' a޼ٝYy=@{N(CGko"tΖ+jˆ2![S5vp%8 s-sFf9]9hUUrm f 2"SCy%ian"(%/3r[Npdt|83btD{Ү(iGVNLVe[\+^zhʧ uм=%^` I6fwYy 8|{+_[ YHޜBpڐ\e ҐܐFri 4|c'tX1n f3Wm`\e]u]!₇6~0pAko3.Q1~I Bt&+EK[wSͪn9~pkͽ[j.hfO٫n r; gZ@˪giC@ + ba d2UlCk]T"7ֱI(6:D`)vf2 grV\c||]9e*0뒇 6:ѳXJcKhMa6?U\ 4;MթYSNvM| rc0?50prV|z#>ñ12]l][Z偵ó Vla6B ?Cщ kϬ_bA>@>Zc;|A>dp"}J+A%]xQ.d0Kx~><(rLzJ# ŊE[۫lbџd{-8vbV/o$\lu^ GbV ]t񠻺9 79B-.|P@馃:DUvk?b-sτlcG;@h4FeXz*N /$Ad7—7Mz2$5m} !Hu N10*UN*yMusƠgpgcն'Kl` ܅5{^>c VP0b*CjDg<)W$G")F dՊL2jE tޗh cC ۞BuPRԫDSlΙsO<5@j>NB^ X(܂T DP/mn>Xh=G"܆h6F/ QO qD@Z͑M8pǼTs~k@A8JS\wGi=ɸϏ@9ۥқ͵ O7Cr4clM1c [DkY̤ Z4“F>_\ߝ_ U,Xs<#﹛;N0-~@*A9CO{Fk9?7q8>A]4r/ä[ސ3lWΠf(ݘ$Z'ҭD Bκpr L&|蛋c|S<Ktuthm~,Zl103 l61AʳBdyg?`҄{?ĊVGkߴ'g/ UbXZߒE2x3tv$iu:蚜#n z޼A--m{׽~_I_ u,:HHlQi‡I>_bZ`^<}H6d ?  WjXMEu_Ø,Gzd&(1&ZץͻJs O]4 hGCyKt_y7ȿσ:(Y/:[# lCZ0B݇KD\9&?^vuDž$?xGyf5 .+t965hctbB+GƗ^zV3{/3&!W2OЬiܜ]1ΜAWq˲ǹiذfSlN-(6: pTݕ|?==SjhwG,Ǎ T A&)hT.MA(tݹm6]RndɾRgo#oHvi{Ӌʚ(x' LWĬS~L g,+0;Q,Z=n(YL]1e|9/nl5`QBM2-i}כ'-i~`)RJpXtֹK5Is-)Id);ȧ[Pi5ly2|/|o b8ݸ_@fy>w0$si{Spذ\/)0k,ޥehz@OB\, X8.h->C:#N9˿^[Yc8hr!0g.pH o{"۵t󆎱1]b;{38N1 -,wKrV `â\e!cyWoñhw&G6:i >4)%1,aa6bJn^/7 7 sks}],sürmV 6t'"P30ʤ~Q*fE 3Nf['̕t\*놋ϽJ"4H7ZGyf+K3/u) +9 )5D]UjY0JAj4-MY1 UUu#YUa!dASj2u}q(7 mr!;}<+eQl#E;SݾNM5 Oͯ,ׅ- W%8,mψ#pSBÿNKnT 5](C2%cXn >){ H8dp,"f)YVdvOq Mm3{)IJ )w3xW ׭}jZа#tgrWy}k$#\II U.8sy*G@gr,]!қ73#ot %z7~cWJrEMUѝ]Fg{ tfsWβ$gp~.q,+T~cy s\J L6A_dj8?W;-mS\t_q .; p`FþY 3pO{"ʩߔjl7xKk:f ^\}KL5$i;. 5Z<o/T|8|0QbK!9 ɛSNLr^H<͝ao n+ uf޹nts>d@\ƚox?U.amm%*Ư3W[Τ}]zi C6^{pQ8Y-n bKmɰ<{uM[AtUnSZ hY, ^~hcqW^`@,RִWFSmhͺ 4ZpUW:6< ŵG4,Όq\uLΊk㚯+Ƿ[_F`yPF"zvcXvlr)gۡUX]KS/fǢIz6k ׵Io5VP\}s W^A3 !,Olu?̧>~6g8Y9YMׁ÷+:o+[+83|N45LL{ڶx fkp~l! _LeoH5Uh'D1HP$ň[Z)UFȸrپ qpl}aSJjқzUh9sz閧f`(QGIP׫йp[E-YSh?ɳ03nphvP+9 7ߘj/}h6hg8[i( W'4Ҟ;gAz󯹶fR[}Bfcl@ 6f|,akhuW4b]Fx/7AkNg=ws FHг-&#&]yXh, Xp0XSrdYeA Z,s <kCkw][h8@R[onPd}:N]s_}AoЛ7hw~tO7|?ˑSv`-9*Mв1+]\Ђ_L] KbAU㙌1籡8xZ_ +kA2ؘ,D%Dqoy?]~nZ_)а=4-5sHFhpsȐ t\^w7痗ۄKڮ(4W,sm)>.m/uz@YSd szkuү")a0M}v‘em;{+JE%K`ã)s=京ӯ9QrۍMf@wr,2Jw%?,et9-O@&Oo]A,31, uf$r3"o Β5f\_%۔1޿, @sM(Ih+e\u4g5]'c`də;gWl | R.=7 dsTdzn1V1vK{{qF|;\ 4łnIqr}AlX,d,9/m8ͱU9ECRynq= t6{r2ǼL027!Yr|ߧ qbR7 @:ݙ4kYvi C{9FȜ EfJ1Ӷiv 螻M0 zqOܕ3\щ9|l|C:9iЪ=sGψ\جfԛJc9F;SK'9Ҳx)?,:lژȜ%9WAYF!qel50&Ym."YiBr}=YW,8R1NOpմ\h/Vٯr Bԟ~S S4& 88Ol!?(C$GXGLZm53VA(c̘LM';_eN5jU7kl6ؐZH՜jMwX4~˭WG=B4~; {evI)3z.{ܖQ4kfe3Atí㩒iA9m=Ob tC;Roz[= z^I{kn68?E '}1-rΤW{t)(~Q[$hfO^eK9{¡';,㐾 cA"#FGþ ҶvU৚} f˂F{9U8O;Is-4ּ!0=Îir 6]&Q n붻a7!\<w䀯 x6Ooa> oZ-rx;%32#MwȻ%y7g|OJ'ynw'W/Pp9K.ٲ^\KI6]9qo1 1SZj4vƖc1co}7b71lbmA| 7jw8 _07*-fE_ՅfBCޱz5{Zhn /x+]=]Mxn!F V@^:LN T .OeOZZ+nMUƧ G8؎`<3s,\U(Gx=* R`K:vu"^uc;%&&lkWez f~y}P`Gmms.>w"5L{)|G?O?oF durdraw-0.25.3/durdraw/help/durhelp-256-page1.dur000066400000000000000000000061231455044760000213440ustar00rootroot00000000000000ddurhelp-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.25.3/durdraw/help/durhelp-256-page2.dur000066400000000000000000000067301455044760000213510ustar00rootroot00000000000000ddurhelp-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.25.3/durdraw/help/durhelp-256.dur000066400000000000000000000043341455044760000203530ustar00rootroot000000000000001Addurhelp-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.25.3/durdraw/help/durhelp.dur000066400000000000000000000050711455044760000200400ustar00rootroot00000000000000;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.25.3/durdraw/main.py000066400000000000000000000244111455044760000162260ustar00rootroot00000000000000# 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.25.3' DUR_FILE_VER = 7 DEBUG_MODE = False # debug = makes debug_write available, sends verbose notifications durlogo = ''' __ __ _| |__ __ _____ __| |_____ _____ __ __ __ / _ | | | __| _ | __| _ | | | |\\ /_____|_____|__|__|_____|__|___\____|________| | \_____________________________________________\| v %s Press esc-h for help. ''' % DUR_VER 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='+') #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.loadFromFile(movie, 'dur') ui.startPlaying() ui.stdscr.clear() 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.25.3/durdraw/themes/000077500000000000000000000000001455044760000162135ustar00rootroot00000000000000durdraw-0.25.3/durdraw/themes/default.dtheme.ini000066400000000000000000000015261455044760000216110ustar00rootroot00000000000000; 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.25.3/durdraw/themes/mutedchill-16.dtheme.ini000066400000000000000000000012101455044760000225310ustar00rootroot00000000000000; 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.25.3/durdraw/themes/mutedform-256.dtheme.ini000066400000000000000000000013721455044760000225000ustar00rootroot00000000000000; 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.25.3/durdraw/themes/poopy-256.dtheme.ini000066400000000000000000000010571455044760000216440ustar00rootroot00000000000000; 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.25.3/durdraw/themes/purpledrank-16.dtheme.ini000066400000000000000000000011241455044760000227320ustar00rootroot00000000000000; 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.25.3/durformat.md000066400000000000000000000142721455044760000156110ustar00rootroot00000000000000Durdraw 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.25.3/examples/000077500000000000000000000000001455044760000150745ustar00rootroot00000000000000durdraw-0.25.3/examples/cm-doge.asc000066400000000000000000000020531455044760000170770ustar00rootroot00000000000000 ;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.25.3/examples/cm-doge.dur000066400000000000000000000020631455044760000171240ustar00rootroot00000000000000ˮ`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.25.3/examples/cm-eye.dur000066400000000000000000000111201455044760000167620ustar00rootroot000000000000007ldcm-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.25.3/examples/dogeblink.dur000066400000000000000000000041561455044760000175540ustar00rootroot00000000000000`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.25.3/examples/dopehax.dur000066400000000000000000000076251455044760000172520ustar00rootroot00000000000000`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.25.3/examples/indyz-linuxfire.dur000066400000000000000000000041541455044760000207540ustar00rootroot00000000000000gdlinuxfire3.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;H<㇧_~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.)&ÔlzoK,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.25.3/examples/rain.dur000066400000000000000000000014721455044760000165450ustar00rootroot00000000000000L`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