diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 625b47c..ecfe60e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ That's awesome that you want to help! I thank you. The guidelines are pretty loo * If you want to help "clean up" the code to make it more legible or modular, that's great. But, I request that you let me see some of the style changes you are proposing before you dump a giant patch of changes. I don't want major changes in a style I don't enjoy. -* Use Git and Github for development, discussion, pull requests, issues, proposals, etc. You can also contact the developer directly via email (see README.md CREDITS), or try the #durdraw IRC channel on irc.libera.chat. +* Use Git and Github for development, discussion, pull requests, issues, proposals, etc. You can also contact the developer directly via email (see README.md CREDITS), or you can find us on Discord or IRC (see README.md for details). * If you want to contribute ANSI or ASCII Art, that's great! Please post it in the Discussions on Github and give consent if you are OK with it being used (in the program, website, Readme file, youtube videos, etc). diff --git a/README.md b/README.md index 3c7ca24..a9686a5 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ Durdraw _| |__ __ _____ __| |_____ _____ __ __ __ / _ | | | __| _ | __| _ | | | |\ /_____|_____|__|__|_____|__|___\____|________| | - \_____________________________________________\| v 0.27.1 + \_____________________________________________\| v 0.28.0 + +![durdraw-0 28-demo](https://github.com/user-attachments/assets/3bdb0c46-7f21-4514-9b48-ac00ca62e68e) -![durdraw-0 27 0-demo-4](https://github.com/cmang/durdraw/assets/261501/78a3c945-9fd6-4da7-86e2-05e062bf79e3) ## OVERVIEW @@ -75,6 +76,12 @@ To look at some included example animations: ./start-durdraw -p examples/*.dur ``` +## OPTIONAL INSTALLATION + +For PNG and animated GIF export, please install Ansilove (https://ansilove.org/) and make sure it is is in your path. PNG and GIF export only work in 16-color mode for now, and only with CP437 compatible charcters. You also need the PIL python module. + +For Durfetch support, please install Neofetch and place it in your path. + ## GALLERY [![Watch the Tutorial Part 1](https://github.com/cmang/durdraw/assets/261501/ca33c81b-0559-4fc7-a49b-a11768938d3d)](https://youtu.be/vWczO0Vd_54) @@ -92,30 +99,37 @@ To look at some included example animations: ## COMMAND LINE USAGE -You can play a .dur file or series of .dur files with: +You can play a .dur file or series of .dur (or .ANS or .ASC) files with: ``` $ durdraw -p filename.dur $ durdraw -p file1.dur file2.dur file3.dur ... ``` +Or view a downloaded ANSI artpack with: + +``` + $ durdraw -p *.DIZ *.ASC *.ANS +``` + Other command-line options:
 
-usage: start-durdraw [-h] [-p PLAY [PLAY ...]] [-q | -w | -x TIMES] [--256color | --16color] [-b]
-                     [-W WIDTH] [-H HEIGHT] [-m] [--nomouse] [-A] [-u UNDOSIZE] [-V] [--debug]
+usage: durdraw [-h] [-p PLAY [PLAY ...]] [-d DELAYEXIT] [-x TIMES] [--256color | --16color] [-b] [-W WIDTH] [-H HEIGHT] [-m]
+                     [--wrap WRAP] [--nomouse] [--cursor CURSOR] [--notheme] [--theme THEME] [--cp437] [--export-ansi] [-u UNDOSIZE]
+                     [--fetch] [-V]
                      [filename]
 
 positional arguments:
   filename              .dur or ascii file to load
 
-optional arguments:
+options:
   -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
+                        Just play .dur, .ANS or .ASC file or files, then exit
+  -d DELAYEXIT, --delayexit DELAYEXIT
+                        Wait X seconds after playback before exiting (requires -p)
   -x TIMES, --times TIMES
                         Play X number of times (requires -p)
   --256color            Try 256 color mode
@@ -126,16 +140,17 @@ optional arguments:
   -H HEIGHT, --height HEIGHT
                         Set canvas height
   -m, --max             Maximum canvas size for terminal (overrides -W and -H)
+  --wrap WRAP           Number of columns to wrap lines at when loading ASCII and ANSI files (default 80)
   --nomouse             Disable mouse support
   --cursor CURSOR       Cursor mode (block, underscore, or pipe)
-  --notheme             Disable theme support
+  --notheme             Disable theme support (use default theme)
   --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
+  --cp437               Display extended characters on the screen using Code Page 437 (IBM-PC/MS-DOS) encoding instead of Utf-8.
+                        (Requires CP437 capable terminal and font) (beta)
+  --export-ansi         Export loaded art to an .ansi file and exit
   -u UNDOSIZE, --undosize UNDOSIZE
-                        Set the number of undo history states - default is 100. More requires more
-                        RAM, less saves RAM.
+                        Set the number of undo history states - default is 100. More requires more RAM, less saves RAM.
+  --fetch               Replace fetch strings with Neofetch output
   -V, --version         Show version number and exit
 
 
@@ -143,56 +158,100 @@ optional arguments: ## 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: + +You can click highlighted areas of the screen. + +You can use the "Esc" (or "Meta") key to access keyboar shortcuts and commands: ``` - .. Art Editing ..................... - : F1-F10 - insert character : - : esc-up - next fg color : - : esc-down - prev fg color : - : esc-right - next bg color (16c) : - : esc-left - prev bg color : - : esc-/ - insert line : .. Animation ....................... - : esc-' - delete line : : esc-k - next frame : - : esc-. - insert column : : esc-j - previous frame : - : esc-, - delete column : : esc-p - start/stop payback : - : esc-] - next character group : : esc-n - clone frame : - : esc-[ - previous character group : : esc-N - append empty frame : - : esc-S - change character set : : esc-d - delete frame : - : esc-y - eyedrop (pick up color) : : esc-D - set frame delay : - : esc-l - color character : : esc-+/esc-- - faster/slower : - : esc-c - color picker : : esc-R - set playback/edit range : - : shift-arrows - select for copy : : esc-g - go to frame # : - : esc-K - mark selection : : esc-M - move frame : - : esc-v - paste : :..................................: - :..................................: - .. UI/Misc ......................... - .. File Operations ................. : esc-m - main menu : - : esc-C - new/clear canvas : : esc-t - mouse tools : - : esc-o - open : : esc-z - undo : - : esc-s - save : : esc-r - redo : - :..................................: : esc-V - view mode : - : esc-i - file/canvas info : - .. Canvas Size ..................... : esc-I - character inspector : - : esc-" - insert line : : tab - focus canvas or colors : - : esc-: - delete line : : ctrl-l - redraw screen : - : esc-> - insert column : : esc-h - help : - : esc-< - delete column : : esc-q - quit : + ____________. _________ __________ _________ _____ _______ +.-\\___ / |______/ _ /.-\\___ // _ /_/ _ \_.____. \ / +| |/ / | / / /:| |/ / / /Y Y Y | / / +| / /| | / _ _/ || / /: _ _/ : _ | /\/ / +| /:| : : Y |: /:| Y | Y | /:H7 +|____ |_________|___| |_____ |____| | |____|____/\_____| +.-- `-----' ----------- `------': - `-----' -- `------'----' -----------------. +| | +`-----------------------------------------------------------------------------' + + .. Art Editing ..................... .. Animation ....................... + : F1-F10 - insert character : : esc-k - next frame : + : esc-1 to esc-0 - same as F1-F10 : : esc-j - previous frame : + : esc-space - insert draw char : : esc-p - start/stop payback : + : esc-c/tab - color picker : : esc-n - clone frame : + : esc-left - next fg color : : esc-N - append empty frame : + : esc-right - prev fg color : : esc-d - delete frame : + : esc-up - change color up : : esc-D - set frame delay : + : esc-down - change color down : : esc-+/esc-- - faster/slower : + : esc-/ - insert line : : esc-R - set playback/edit range : + : esc-' - delete line : : esc-g - go to frame # : + : esc-. - insert column : : esc-M - move frame : + : esc-, - delete column : : esc-{ - shift frames left : + : esc-] - next character group : : esc-} - shift frames right : + : esc-[ - previous character group : :..................................: + : esc-S - change character set : + : esc-L - replace color : .. UI/Misc ......................... + : esc-y - eyedrop (pick up color) : : esc-m - main menu : + : esc-P - pick up character : : esc-a - animation menu : + : esc-l - color character : : esc-t - mouse tools : + : shift-arrows - select for copy : : esc-z - undo : + : esc-K - mark selection : : esc-r - redo : + : esc-v - paste : : esc-V - view mode : + :..................................: : esc-i - file/canvas info : + : esc-I - character inspector : + .. File Operations ................. : esc-F - search/find string : + : esc-C - new/clear canvas : : ctrl-l - redraw screen : + : esc-o - open : : esc-h - help : + : esc-s - save : : esc-q - quit : :..................................: :..................................: - 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] |< << |> >> >| + .. Canvas Size ..................... + : esc-" - insert line : + : esc-: - delete line : + : esc-> - insert column : + : esc-< - delete column : + :..................................: + + esc-j esc-k + Prev Next Canvas +esc-f esc-g esc-- Frame Frame Size +esc-m Go to esc-+ esc-D esc-R esc-t | esc-p| | + Main Frame Speed Frame Play/Edit Mouse First | Play/| Last | + Menu Number | Delay Range Tools Frame | Pause| Frame | + | | | | | | | | | | | | +[Menu] F: 1/7 : 8 D: 0.00 R: 1/8 [Move] |< << |> >> >| [80x24] + + tab + esc-c esc-S + Pick esc-[ esc-] Charset set F1-F10 esc-[ esc-] + Foreground Character or Unicode Insert Special Prev/Next Cursor + Color Group Block Characters Char Group Position + | | | | \ | +FG:██ (1/21) [Dur..] (12,10) + + ANIMATION: + + Use the Animation Menu [Anim] or keyboard commands to insert (esc-n), + delete (esc-d), move (esc-M) and edit frames. Use esc-k and esc-j to + flip to the next and previous frames. The "Play" button (|> or esc-p) + starts or stops playback. + + When the animation is playing, all changes made effect all frames + within the current playback/edit Range (R: or esc-R). Change playback + speed ( or Frames Per Second) with esc-+ (or esc-=) and esc--. + F: shows the current frame number, and you can go to a specific frame + with esc-g. + + BRUSHES: + + To make a brush, use shift-arrow or esc-K to make a selection, then + press b. To use the brush, click the Mouse Tools menu (esc-t) and select + Paint (P). You can now use the mouse to paint with your custom brush. ``` ## CONFIGURATION -You can create a custom startup file where you can set a theme. - +You can create a custom startup file where you can set a theme and other options. If you did not already do so during installation, you can install a sample configuration and some themes into ~/.durdraw/ with the command: @@ -202,12 +261,31 @@ If you did not already do so during installation, you can install a sample confi This will place durdraw.ini into ~/.durdraw/ and the themes into ~/.durdraw/themes/. -Here is an example durdraw.ini file: +Here is an example durdraw.ini file, showing the available options:
-; Durdraw 0.20 Configuration File
+; Durdraw 0.28.0 Configuration File
+
+[Main]
+
+; color-mode sets the color mode to start in. Available options: 16, 256
+;color-mode: 16
+
+; disable-mouse disablse the mouse.
+;disable-mouse: True
+
+; max-canvas atuomatically sets the canvas size to the terminal window size at startup.
+;max-canvas: True
+
+; Cursor mode requests a cursor type from the terminal. Available options: block, underscore, pipe
+;cursor-mode: underscore
+
+; When scroll-colors is enabled, using the mouse wheel in the canvas changes the
+; foreground color instead of moving the cursor.
+;scroll-colors: True
+
 [Theme]
-theme-16: ~/.durdraw/themes/purpledrank-16.dtheme.ini
+theme-16: ~/.durdraw/themes/mutedchill-16.dtheme.ini
 theme-256: ~/.durdraw/themes/mutedform-256.dtheme.ini
 
@@ -270,36 +348,85 @@ menuTitleColor: the color of menu titles menuBorderColor: the color of the border around menus ``` +## DURFETCH -## OTHER TIPS +Durfetch is a program which acts like a fetcher. It uses Neofetch to obtain system statistics and requires that Neofetch be found in the path. You can put keys in your .DUR files which durfetch will replace with values from Neofetch. You can also use built-in example animations. - * To use themes, copy durdraw.ini to ~/.durdraw/ and edit it. Durdraw - will also check in the current directory for durdraw.ini. +Note that this feature is in beta, and is far from perfect, but it can be fun to play with. If anyone wants to improve durfetch, please feel free. - * 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. +Keys will only be replaced if there is enough room in the art for the replacement value. -## OPTIONAL INSTALLATION +The following values can be used in your art and automatically interpreted by Durfetch: + +``` +{OS} +{Host} +{Kernel} +{Uptime} +{Packages} +{Shell} +{Resolution} +{DE} +{WM} +{WM Theme} +{Terminal} +{Terminal Font} +{CPU} +{GPU} +{Memory} +``` + +The durfetch executable takes the following command-line paramaters: + +``` +usage: durfetch [-h] [-r | -l LOAD] [--linux | --bsd] [filename ...] -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. +An animated fetcher. A front-end for Durdraw and Neofetch integration. + +positional arguments: + filename .durf ASCII and ANSI art file or files to use + +options: + -h, --help show this help message and exit + -r, --rand Pick a random animation to play + -l LOAD, --load LOAD Load an internal animation + --linux Show a Linux animation + --bsd Show a BSD animation + +Available animations for -l: + +bsd +cm-eye +linux-fire +linux-tux +unixbox +``` + +Here are some durfetch examples: + +![tux-fetch-colors](https://github.com/user-attachments/assets/4010d18a-1b79-4594-a9cd-17234584f3c8) + +![unixy3](https://github.com/user-attachments/assets/812514d4-0216-4f41-8384-84563fa664b7) ## FAQ +#### Q: Durdraw crashed! What do I do? +A: Oh no! I am sorry and hope nothing important was lost. But you can help fix it. Please take a screenshot of the crash and post it as a bug report at https://github.com/cmang/durdraw/issues/. Please try to describe what you were trying to do when it happened, and if possible, include the name of your terminal, OS and Python version. I will do my best to try to fix it ASAP. Your terminal will probably start acting weird if Durdraw crashed. You can usually fix it by typing "reset" and pressing enter. + #### Q: Don't TheDraw and some other programs already do ANSI animation? A: Yes, but traditional ANSI animation does not provide any control over timing, instead relying on terminal baud rate to govern the playback speed. This does not work well on modern systems without baud rate emulation. Durdraw gives the artist fine control over frame rate, and delays per frame. Traditional ANSI animation also updates the animation one character at a time, while Durdraw updates the animation a full frame at a time. This makes it less vulnerable to visual corruption from things like errant terminal characters, resized windows, line noise, etc. Finally, unlike TheDraw, which requires MS-DOS, Durdraw runs in modern Unicode terminals. #### Q: Can I run Durdraw in Windows? -A: Short answer: It's not supported, but it seems to work fine in the Windows Subsystem for Linux (WSL). 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. +A: Short answer: It's not supported, but it seems to work fine in the Windows Subsystem for Linux (WSL), and in Docker using the provided Dockerfile. Long answer: Some versions run fine in Windows Command Prompt, Windows Terminal, etc, without WSL, but it's not tested or supported. If you want to help make Durdraw work better in Windows, please help by testing, submitting bug reports and submitting patches. -#### Q: Can I run Durdraw on Amiga, MS-DOS, Classic MacOS, iOS, Android, etc? +#### Q: Can I run Durdraw on Amiga, MS-DOS, Classic MacOS, iOS, Android, Atari ST, etc? A: Probably not easily. Durdraw requires Python 3 and Ncurses. If your platform can support these, it will probably run. However, the file format for Durdraw movies is a plain text JSON format. It should be possible to support this format in different operating systems and in different applications. See durformat.md for more details on the .dur file format. #### Q: Does Durdraw support IBM-PC ANSI art? A: Yes! IBM-PC ANSI art popular in the "ANSI Art Scene" uses Code Page 437 character encoding, which usually needs to be translated to work with modern terminals. When Durdraw encounters these files, it will convert them to Unicode and carry on. When you save ANSI files, it will ask if you want to use CP437 or Utf-8 encoding. #### Q: I only see 8 colors in 16 color mode. Why? -A: Look in your terminal setting for "Use bright colors for bold," or a similarly named option. Durdraw's 16-color mode, like many vintage terminals (including MS-DOS), uses the Bold escape codes to tell the terminal the "bright" colors. This provides compatibility with many older systems. However, some terminals do not support or enable this option by default. Additionally, your terminal decides what colors to assign to the lower 16 colors. +A: Look in your terminal setting for "Use bright colors for bold," or a similarly named option. Durdraw's 16-color mode, like many vintage terminals (including MS-DOS), uses the Bold escape codes to tell the terminal that colors are "bright." This provides compatibility with many older systems. However, some terminals do not support or enable this option by default. Additionally, your terminal decides what colors to assign to the lower 16 colors. In many terminals, Durdraw can override the default 16 color palette. To do this, click on Menu -> Settings and select VGA, Commodore 64 or ZX Spectrum colors. #### Q: Some or all of the F1-F10 keys do not work for me! What can I do? A: You can use ESC-1 through ESC-0 as a replacement for F1-F10. Some terminals will map this to Alt-1 through Alt-0. You can also use the following settings in some terminals to enable the F1-F10 keys: diff --git a/durdraw.ini b/durdraw.ini index 2126aa6..1385933 100644 --- a/durdraw.ini +++ b/durdraw.ini @@ -1,5 +1,28 @@ -; Durdraw 0.20.0 Configuration File +; Durdraw 0.28.0 Configuration File + +[Main] +; color-mode sets the color mode to start in. Available options: 16, 256 +;color-mode: 16 + +; disable-mouse.. disablse the mouse. +;disable-mouse: True + +; max-canvas atuomatically sets the canvas size to the terminal window size at startup. +;max-canvas: True + +; Cursor mode requests a cursor type from the terminal. Available options: block, underscore, pipe +;cursor-mode: underscore + +; When scroll-colors is enabled, using the mouse wheel in the canvas changes the +; foreground color instead of moving the cursor. +;scroll-colors: True [Theme] +; theme-16: ~/.durdraw/themes/purpledrank-16.dtheme.ini theme-16: ~/.durdraw/themes/mutedchill-16.dtheme.ini theme-256: ~/.durdraw/themes/mutedform-256.dtheme.ini + +;[Charset] +;charset-file-utf8: durdraw/charsets/unicode-legacy-computing.json +;charset-file-cp437: durdraw/charsets/cp437-ibmpc.json + diff --git a/durdraw/durdraw_ansiparse.py b/durdraw/durdraw_ansiparse.py index c9863a1..73c60ad 100644 --- a/durdraw/durdraw_ansiparse.py +++ b/durdraw/durdraw_ansiparse.py @@ -185,18 +185,19 @@ def parse_ansi_escape_codes(text, filename = None, appState=None, caller=None, c #if sauce.height == None: # sauce.height = 25 if sauce.width == None: - sauce.width = 80 + #sauce.width = 80 + sauce.width = maxWidth maxWidth = sauce.width width = sauce.width height = sauce.height #caller.notify(f"Sauce pulled: author: {sauce.author}, title: {sauce.title}, width {width}, height {height}") if not sauce.height: - width, height = get_width_and_height_of_ansi_blob(text, width=80) + width, height = get_width_and_height_of_ansi_blob(text) width = sauce.width if not sauce.sauce_found or width > 200 or height > 1200: # let the dodgy function guess - width, height = get_width_and_height_of_ansi_blob(text, width=80) - #width = max(width, maxWidth) - width = max(width, 80) + width, height = get_width_and_height_of_ansi_blob(text) + width = max(width, maxWidth) + #width = max(width, 80) height += 1 if appState.debug: caller.notify(f"Guessed width: {width}, height: {height}") diff --git a/durdraw/durdraw_appstate.py b/durdraw/durdraw_appstate.py index b734ff6..540d466 100644 --- a/durdraw/durdraw_appstate.py +++ b/durdraw/durdraw_appstate.py @@ -6,6 +6,7 @@ import pickle import subprocess import sys +import threading from sys import version_info from durdraw.durdraw_options import Options import durdraw.durdraw_file as durfile @@ -13,15 +14,33 @@ class AppState(): """ run-time app state, separate from movie options (Options()) """ - def __init__(self): # User friendly defeaults + def __init__(self): + + # Check for optional dependencies + #self.ansiLove = self.isAppAvail("ansilove") + #self.neofetch = self.isAppAvail("neofetch") + #self.PIL = self.checkForPIL() + + self.ansiLove = None + self.neofetch = None + self.PIL = None + self.check_dependencies() + + # User friendly defeaults self.quickStart = False - self.showStartupScreen = True + self.mental = False # Mental mode - enable experimental options/features + self.showStartupScreen = False + self.narrowWindow = False self.curOpenFileName = "" python_version = f"{version_info.major}.{version_info.minor}.{version_info.micro}" self.pyVersion = python_version self.colorMode = "256" # or 16, or possibly "none" or "true" or "rgb" (24 bit rgb "truecolor") + self.fileColorMode = None self.maxColors = 256 self.iceColors = False + self.can_inject = False # Allow injecting color codes to override ncurses colors (for BG 256 colors) + self.showBgColorPicker = False # until BG colors work in 256 color mode. (ncurses 5 color pair limits) + self.scrollColors = False # When true, scroll wheel in canvas changes color instead of moving cursor self.editorRunning = True self.screenCursorMode = "default" # can be block, underscore, pipe self.renderMouseCursor = False # show Paint or Draw cursor in canvas @@ -31,6 +50,11 @@ def __init__(self): # User friendly defeaults self.totalBgColors = 8 self.defaultFgColor = 7 self.defaultBgColor = 0 + self.width = 80 # default canvas width/columns + self.height = 23 # default canvas height/lines + self.wrapWidth = 80 # Default width to wrap at when loading ASCII files (.asc/.txt) + self.minWindowWidth = 40 # smaller than this, and Durdraw complains that it can't draw the UI + self.full_ui_width = 80 # smaller than this, and draw the streamlined UI self.stickyColorPicker = True # true to keep color picker on screen self.colorPickerSelected = False # true when the user hits esc-c self.charEncoding = 'utf-8' # or cp437, aka ibm-pc @@ -45,11 +69,13 @@ def __init__(self): # User friendly defeaults #self.characterSet = "Unicode Block" self.unicodeBlock = "Braille Patterns" # placeholder during initialization self.cursorMode = "Move" # Move/Select, Draw and Color + self.fetchMode = False # use neofetch, replace {variables} in dur file + self.fetchData = None # a {} dict containing key:value for neofetch output. + self.inferno = None + self.inferno_opts = None self.playOnlyMode = False # This means viewer mode now, actually.. self.viewModeShowInfo = False # show sauce etc in view mode self.playNumberOfTimes = 0 # 0 = loop forever, default - self.ansiLove = self.isAppAvail("ansilove") - self.PIL = self.checkForPIL() self.undoHistorySize = 100 # How far back our undo history can self.playbackRange = (1,1) self.drawChar = '$' @@ -81,7 +107,6 @@ def __init__(self): # User friendly defeaults 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 @@ -132,6 +157,23 @@ def __init__(self): # User friendly defeaults } self.theme = self.theme_16 + def maximize_canvas(self): + term_size = os.get_terminal_size() + if term_size[0] > 80: + self.width = term_size[0] + if term_size[1] > 24: + self.height = term_size[1] - 2 + + def check_dependencies(self): + dependency_thread = threading.Thread(target=self.thread_check_dependencies) + dependency_thread.start() + + def thread_check_dependencies(self): + # Check for optional dependencies + self.ansiLove = self.isAppAvail("ansilove") + self.neofetch = self.isAppAvail("neofetch") + self.PIL = self.checkForPIL() + def setCursorModeMove(self): self.cursorMode="Move" @@ -162,6 +204,19 @@ def setDurVer(self, version): def setDebug(self, isEnabled: bool): self.debug = isEnabled + def loadThemeList(self): + """ Look for theme files in internal durdraw directory """ + # durhelp256_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp-256-long.dur") + # Get a list of files from the themes paths + internal_theme_path = pathlib.Path(__file__).parent.joinpath("themes/") + self.internal_theme_file_list = glob.glob(f"{internal_theme_path}/*.dtheme.ini") + #user_theme_path = pathlib.Path(__file__).parent.joinpath("themes/") + #self.user_theme_file_list = glob.glob(f"{user_theme_path}/*.dtheme.ini") + # Turn lists into an index of Theme name, Theme type, and Path to + available_themes = [] # populate with a list of dicts containing name=, path=, type= + for filename in self.internal_theme_file_list: + pass + def loadConfigFile(self): # Load configuration filea configFullPath = os.path.expanduser("~/.durdraw/durdraw.ini") @@ -188,15 +243,15 @@ def loadThemeFromConfig(self, themeMode): self.loadThemeFile(themeConfig['theme-16'], themeMode) if 'theme-256' in themeConfig and themeMode == 'Theme-256': self.loadThemeFile(themeConfig['theme-256'], themeMode) - - def loadThemeList(self): - """ Look for theme files in internal durdraw directory """ - # durhelp256_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp-256-long.dur") - # Get a list of files from the themes paths - internal_theme_path = pathlib.Path(__file__).parent.joinpath("themes/") - internal_theme_file_list = glob.glob(f"{internal_theme_path}/*.dtheme.ini") - pass + def getConfigOption(self, section: str, item: str): + # section = something like [Main], item = something like color-mode: + try: # see if section and item exist, otherwise return False + configSection = self.configFile[section] + configItem = configSection[item] + return configItem + except KeyError: + return False def loadThemeFile(self, themeFilePath, themeMode): # If there is a theme set, use it @@ -250,6 +305,38 @@ def isAppAvail(self, name): # looks for program 'name' in path return False return True + def loadDurFileToMov(self, fileName): + """ Takes a file path, returns a movie object """ + fileName = os.path.expanduser(fileName) + #self.helpMov = Movie(self.opts) # initialize a new movie to work with + try: + f = open(fileName, 'rb') + except Exception as e: + return False + if (f.read(2) == b'\x1f\x8b'): # gzip magic number + # file == gzip compressed + f.close() + try: + f = gzip.open(fileName, 'rb') + except Exception as e: + return False + else: + f.seek(0) + try: # Load json help file + #pdb.set_trace() + loadedContainer = durfile.open_json_dur_file(f, self) + opts = loadedContainer['opts'] + mov = loadedContainer['mov'] + return mov, opts + except: + #pass # loading json help file failed for some reason, so... + return False + + + def loadHelpFileThread(self, helpFileName): + help_loading_thread = threading.Thread(target=self.loadHelpFile, args=(helpFileName,)) + help_loading_thread.start() + def loadHelpFile(self, helpFileName, page=1): helpFileName = os.path.expanduser(helpFileName) #self.helpMov = Movie(self.opts) # initialize a new movie to work with diff --git a/durdraw/durdraw_color_curses.py b/durdraw/durdraw_color_curses.py index 0ac1c18..9b63625 100644 --- a/durdraw/durdraw_color_curses.py +++ b/durdraw/durdraw_color_curses.py @@ -317,9 +317,9 @@ def getColorCode256(self, fg, bg): 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 + ';' + code = code + '48;5;' + code = code + str(bg) #code = code + str('0') code = code + 'm' return code diff --git a/durdraw/durdraw_file.py b/durdraw/durdraw_file.py index 66c84ac..78be6d0 100644 --- a/durdraw/durdraw_file.py +++ b/durdraw/durdraw_file.py @@ -279,7 +279,8 @@ def open_json_dur_file(f, appState): currentFrame = 0 lineNum = 0 for frame in loadedMovieData['DurMovie']['frames']: - newMov.insertCloneFrame() + #newMov.insertCloneFrame() + newMov.addEmptyFrame() newMov.nextFrame() for line in frame['contents']: #pdb.set_trace() diff --git a/durdraw/durdraw_movie.py b/durdraw/durdraw_movie.py index 5d8999a..a545a6c 100644 --- a/durdraw/durdraw_movie.py +++ b/durdraw/durdraw_movie.py @@ -132,10 +132,11 @@ class Movie(): def __init__(self, opts): self.frameCount = 0 # total number of frames self.currentFrameNumber = 0 - self.sizeX = opts.sizeX - self.sizeY = opts.sizeY + self.sizeX = opts.sizeX # Number of columns + self.sizeY = opts.sizeY # Number of lines self.opts = opts self.frames = [] + self.layers = {} # Key can be a layer #, or something special. eg: "masks self.addEmptyFrame() self.currentFrameNumber = self.frameCount self.currentFrame = self.frames[self.currentFrameNumber - 1] @@ -208,6 +209,12 @@ def prevFrame(self): self.currentFrameNumber -= 1 self.currentFrame = self.frames[self.currentFrameNumber - 1] + def hasMultipleFrames(self): + if len(self.frames) > 1: + return True + else: + return False + def growCanvasWidth(self, growth): self.sizeX += growth self.opts.sizeX += growth @@ -218,6 +225,41 @@ def shrinkCanvasWidth(self, shrinkage): self.opts.sizeY = self.opts.sizeY - shrinkage #self.width = self.width - shrinkage + def search_and_replace_color_pair(self, old_color, new_color, frange=None): + if frange != None: # apply to all frames in range + for frameNum in range(frange[0] - 1, frange[1]): + #for frame in self.frames: + frame = self.frames[frameNum] + line_num = 0 + col_num = 0 + for line in frame.newColorMap: + for pair in line: + if pair == old_color: + try: + frame.newColorMap[line_num][col_num] = new_color + except: + pdb.set_trace() + #found = True + col_num += 1 + line_num += 1 + col_num = 0 + else: # only apply to current frame + frame = self.currentFrame + line_num = 0 + col_num = 0 + for line in frame.newColorMap: + for pair in line: + if pair == old_color: + try: + frame.newColorMap[line_num][col_num] = new_color + except: + pdb.set_trace() + #found = True + col_num += 1 + line_num += 1 + col_num = 0 + + def search_and_replace_color(self, old_color :int, new_color :int): found = False for frame in self.frames: @@ -386,6 +428,25 @@ def strip_backgrounds(self): pair[1] = 0 return True + def strip_unprintable_characters(self): + """ Remove all non-printable characters from canvas """ + #frame_num = 0 + for frame in self.frames: + line_num = 0 + col_num = 0 + for line in frame.content: + for char in line: + if char.isprintable(): + pass + else: # Not a printable character, so replace it with a ' ' + #line_str = line_str.replace(search_str, replace_str.ljust(len(search_str))) + #line = list(line_str) + frame.content[line_num][col_num] = ' ' + col_num += 1 + col_num = 0 + line_num += 1 + return True + def shift_right(self): """ Shift all frames to the right, wrapping the last frame back to the front """ # a.insert(0,a.pop()) diff --git a/durdraw/durdraw_sauce.py b/durdraw/durdraw_sauce.py index 5a0122d..31d5c33 100644 --- a/durdraw/durdraw_sauce.py +++ b/durdraw/durdraw_sauce.py @@ -30,6 +30,9 @@ def __init__(self): self.fileType_offset = 95 self.tinfo1_offset = 96 self.tinfo2_offset = 98 + # Number of lines in the extra SAUCE comment block. 1 byte. 0 indicates no comment block is present. + self.comnt_lines = 105 + self.comnt_first_line = 133 # comment lines are 64 bytes long # other stuff self.sauce_blob = None diff --git a/durdraw/durdraw_ui_curses.py b/durdraw/durdraw_ui_curses.py index 988c5f9..a3e9cbc 100644 --- a/durdraw/durdraw_ui_curses.py +++ b/durdraw/durdraw_ui_curses.py @@ -9,6 +9,7 @@ import locale import os import os.path +import pathlib import pdb import pickle import shutil @@ -25,6 +26,7 @@ from durdraw.durdraw_ui_widgets import StatusBar import durdraw.durdraw_gui_manager as durgui import durdraw.durdraw_movie as durmovie +import durdraw.neofetcher as neofetcher import durdraw.durdraw_color_curses as dur_ansilib import durdraw.durdraw_ansiparse as dur_ansiparse import durdraw.durdraw_sauce as dursauce @@ -57,6 +59,8 @@ def __init__(self, app): self.appState.realmaxX = realmaxX self.statusBarLineNum = realmaxY - 2 self.stdscr = curses.newwin(realmaxY, realmaxX, 0, 0) # Curses window + if self.appState.charEncoding == 'cp437': + self.stdscr.encoding = 'cp437' self.panel = curses.panel.new_panel(self.stdscr) # Panel for drawing, to sit below menus self.panel.bottom() self.panel.show() @@ -81,9 +85,13 @@ def __init__(self, app): if self.appState.colorMode == "256": if self.ansi.initColorPairs_256color(): self.init_256_colors_misc() + self.appState.totalFgColors = 255 + self.appState.totalBgColors = 1 else: self.appState.colorMode = "16" self.appState.maxColors = 16 + self.appState.totalFgColors = 16 + self.appState.totalBgColors = 8 if self.appState.colorMode == "16": self.init_16_colors_misc() if not app.quickStart and app.showStartupScreen: @@ -108,8 +116,9 @@ def __init__(self, app): self.undo.setHistorySize(self.appState.undoHistorySize) self.xy = [0, 1] # cursor position x/y - was "curs" self.playing = False + #curses.raw() + curses.cbreak() curses.noecho() - curses.raw() curses.nonl() self.stdscr.keypad(1) self.realmaxY,self.realmaxX = self.realstdscr.getmaxyx() @@ -137,8 +146,8 @@ def init_256_colors_misc(self): 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.appState.loadHelpFileThread(self.appState.durhelp256_fullpath) + #self.appState.loadHelpFile(self.appState.durhelp256_page2_fullpath, page=2) self.colorbg = 0 # default bg black self.colorfg = 7 # default fg white self.appState.sideBar_minimum_width = 37 @@ -151,8 +160,8 @@ def init_16_colors_misc(self): 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) + self.appState.loadHelpFileThread(self.appState.durhelp16_fullpath) + #self.appState.loadHelpFile(self.appState.durhelp16_page2_fullpath, page=2) if self.appState.customThemeFile: self.appState.loadThemeFile(self.appState.customThemeFile, 'Theme-256') self.colorfg = 8 # default fg white @@ -170,8 +179,20 @@ def init_16_colors_misc(self): if self.statusBar != None: self.statusBar.colorPickerButton.enabled = False + def toggleMouse(self): + """ enable and disable mouse """ + self.appState.hasMouse = not self.appState.hasMouse # flip true/false + if self.appState.hasMouse: + self.initMouse() + else: + self.disableMouse() + + def disableMouse(self): + print('\033[?1003l') # disable mouse reporting + curses.mousemask(0) def initMouse(self): + print('\033[?1003h') # enable mouse tracking with the XTERM API curses.mousemask(1) # click response without drag support curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) #print('\033[?1003h') # enable mouse tracking with the XTERM API @@ -335,8 +356,12 @@ def rgb_color_to_ncurses_color(self, value): return ncurses_color_value def setWindowTitle(self, title): - title = f"Durdraw - {title}" + if title == "": + title = f"durdraw" + else: + title = f"durdraw - {title}" sys.stdout.write(f"\x1b]2;{title}\x07") + sys.stdout.write(f'\33]0;{title}\a') sys.stdout.flush() def getPlaybackRange(self): @@ -443,7 +468,7 @@ def setFgColor(self, fg): self.colorbg = 0 self.colorpair = self.ansi.colorPairMap[(self.colorfg, self.colorbg)] except KeyError: - selt.nofiy("There was an error setting the color. Please file a bug report explaining how you got to this error.") + self.notify(f"There was an error setting the color. fg: {self.colorfg}, bg: {self.colorbg}. Please file a bug report explaining how you got to this error.") def setBgColor(self, bg): self.colorbg = bg @@ -461,6 +486,7 @@ def switchTo256ColorMode(self): def switchToColorMode(self, newMode: str): """ newMode, eg: '16' or '256' """ + self.appState.inferno = None if newMode == "16": #self.statusBar.colorPicker.hide() self.appState.colorMode = "16" @@ -477,6 +503,8 @@ def switchToColorMode(self, newMode: str): # switch to 16 color sidebar picker self.statusBar.colorPicker.hide() self.statusBar.colorPicker = self.statusBar.colorPicker_16 + self.appState.totalFgColors = 16 + self.appState.totalBgColors = 8 #self.statusBar.colorPicker.show() if newMode == "256": self.appState.colorMode = "256" @@ -485,6 +513,8 @@ def switchToColorMode(self, newMode: str): self.mov.change_palette_16_to_256() self.appState.loadThemeFromConfig("Theme-256") self.statusBar.colorPickerButton.show() + self.appState.totalFgColors = 255 + self.appState.totalBgColors = 1 if self.appState.blackbg: self.enableTransBackground() #self.statusBar.charSetButton.show() @@ -624,34 +654,44 @@ def cursorOn(self): 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]) + def addstr(self, y, x, string, attr=None): # addstr(y, x, str[, attr]) and addstr(str[, attr]) """ Wraps ncurses addstr in a try;except, prevents addstr from crashing cureses if it fails """ if not attr: try: - if self.appState.charEncoding == 'utf-8': - self.stdscr.addstr(y, x, str.encode('utf-8')) - else: - self.stdscr.addstr(y, x, str) + self.stdscr.addstr(y, x, string.encode(self.appState.charEncoding, 'replace')) + #if self.appState.charEncoding == 'utf-8': + # self.stdscr.addstr(y, x, string.encode('utf-8')) + #else: + # self.stdscr.addstr(y, x, string) + except UnicodeEncodeError: + # Replace non-ascii characters with ' ' + string = string.encode('ascii', 'replace').decode('ascii').replace('?', ' ') + self.stdscr.addstr(y, x, string) except curses.error: self.testWindowSize() else: try: - if self.appState.charEncoding == 'utf-8': - self.stdscr.addstr(y, x, str.encode('utf-8'), attr) - else: - self.stdscr.addstr(y, x, str, attr) + self.stdscr.addstr(y, x, string.encode(self.appState.charEncoding, 'replace'), attr) + #if self.appState.charEncoding == 'utf-8': + # self.stdscr.addstr(y, x, strng.encode('utf-8'), attr) + #else: + # self.stdscr.addstr(y, x, string, attr) + except UnicodeEncodeError: + string = string.encode('ascii', 'replace').decode('ascii').replace('?', ' ') + self.stdscr.addstr(y, x, string, attr) except curses.error: self.testWindowSize() def move(self, y, x): realLine = y - self.appState.topLine + realCol = x - self.appState.firstCol try: - self.stdscr.move(realLine, x) + self.stdscr.move(realLine, realCol) except curses.error: self.testWindowSize() - def notify(self, message, pause=False): + def notify(self, message, pause=False, wait_time=2500): self.cursorOff() self.clearStatusLine() self.addstr(self.statusBarLineNum, 0, message, curses.color_pair(self.appState.theme['notificationColor'])) @@ -664,7 +704,7 @@ def notify(self, message, pause=False): else: self.stdscr.getch() if not pause: - curses.napms(2500) + curses.napms(wait_time) curses.flushinp() self.clearStatusLine() self.cursorOn() @@ -677,7 +717,8 @@ def testWindowSize(self): if realmaxY != self.realmaxY or realmaxX != self.realmaxX: self.resizeHandler() self.realmaxY, self.realmaxX = realmaxY, realmaxX - while self.realmaxX < 80 and not self.appState.playOnlyMode: + #while self.realmaxX < 80 and not self.appState.playOnlyMode: + while self.realmaxX < self.appState.minWindowWidth and not self.appState.playOnlyMode: self.smallWindowMode() # go into small window loop.stdscr def smallWindowMode(self): @@ -687,10 +728,10 @@ def smallWindowMode(self): self.stdscr.refresh() self.realmaxY,self.realmaxX = self.realstdscr.getmaxyx() #while self.realmaxX < self.mov.sizeX: - while self.realmaxX < 80: + while self.realmaxX < self.appState.minWindowWidth: 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") + self.addstr(1, 0, f"Please enlarge to {self.appState.minWindowWidth} columns or larger, or press 'q' to quit") except: # if window is too small for the message ^ pass c = self.stdscr.getch() @@ -708,12 +749,13 @@ def backspace(self): self.undo.push() self.xy[1] = self.xy[1] - 1 if self.playing: - self.insertChar(ord(' '), fg=1, bg=0, frange=self.appState.playbackRange) + self.insertChar(ord(' '), fg=self.appState.defaultFgColor, bg=self.appState.defaultBgColor, frange=self.appState.playbackRange) else: - self.insertChar(ord(' '), fg=1, bg=0) + self.insertChar(ord(' '), fg=self.appState.defaultFgColor, bg=self.appState.defaultFgColor) self.xy[1] = self.xy[1] - 1 def deleteKeyPop(self, frange=None): + fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor if self.xy[1] > 0: self.undo.push() if frange: # framge range @@ -721,12 +763,28 @@ def deleteKeyPop(self, frange=None): 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. + self.mov.frames[frameNum].newColorMap[self.xy[0]].append([fg,bg]) # at the end of each line. else: self.mov.currentFrame.content[self.xy[0]].pop(self.xy[1] - 1) self.mov.currentFrame.content[self.xy[0]].append(' ') self.mov.currentFrame.newColorMap[self.xy[0]].pop(self.xy[1] - 1) - self.mov.currentFrame.newColorMap[self.xy[0]].append([1,0]) # at the end of each line. + self.mov.currentFrame.newColorMap[self.xy[0]].append([fg,bg]) # at the end of each line. + + def reverseDelete(self, frange=None): + fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor + if self.xy[1] > 0: + self.undo.push() + if frange: # framge range + for frameNum in range(frange[0] - 1, frange[1]): + self.mov.frames[frameNum].content[self.xy[0]].pop(self.xy[1] - 1) # line & add a blank + self.mov.frames[frameNum].content[self.xy[0]].insert(0, ' ') # at the end of each line. + self.mov.frames[frameNum].newColorMap[self.xy[0]].pop(self.xy[1] - 1) # line & add a blank + self.mov.frames[frameNum].newColorMap[self.xy[0]].insert(0, [fg,bg]) # at the end of each line. + else: + self.mov.currentFrame.content[self.xy[0]].pop(self.xy[1] - 1) + self.mov.currentFrame.content[self.xy[0]].insert(0, ' ') + self.mov.currentFrame.newColorMap[self.xy[0]].pop(self.xy[1] - 1) + self.mov.currentFrame.newColorMap[self.xy[0]].insert(0, [fg,bg]) # at the end of each line. def insertColor(self, fg=1, bg=0, frange=None, x=None, y=None, pushUndo=True): @@ -767,14 +825,16 @@ def insertChar(self, c, fg=1, bg=0, frange=None, x=None, y=None, moveCursor = Fa self.notify(f"Please save your work and restart Durdraw. Sorry for the inconvenience.") break if x < self.mov.sizeX and moveCursor: - self.xy[1] = self.xy[1] + 1 + self.move_cursor_right() + #self.xy[1] = self.xy[1] + 1 else: self.mov.currentFrame.content[y][x - 1] = chr(c) #self.mov.currentFrame.colorMap.update( # {(y,x - 1):(fg,bg)} ) self.mov.currentFrame.newColorMap[y][x - 1] = [fg, bg] if x < self.mov.sizeX and moveCursor: - self.xy[1] = self.xy[1] + 1 + self.move_cursor_right() + #self.xy[1] = self.xy[1] + 1 def pickUpDrawingChar(self, col, line): # Sets the drawing chaaracter to the character under teh cusror. @@ -859,7 +919,7 @@ def showCharInspector(self): inspectorString = f"Fg: {fg}, Bg: {bg}, Char: {character}, {charType} value: {charValue}" try: ibmpc_value = str(ord(character.encode('cp437'))) - inspectorString += inspectorString + f", cp437 value: {ibmpc_value}" + inspectorString = inspectorString + f", cp437 value: {ibmpc_value}" except: pass self.notify(inspectorString, pause=True) @@ -886,6 +946,24 @@ def toggleShowFileInformation(self): self.appState.viewModeShowInfo = not self.appState.viewModeShowInfo self.stdscr.clear() + def toggleDebug(self): + self.appState.debug = not self.appState.debug + + def toggleWideWrapping(self): + if self.appState.wrapWidth < 120: + self.appState.wrapWidth = 120 + else: + self.appState.wrapWidth = 80 + self.notify(f"Line wrapping for loading ANSIs set to: {self.appState.wrapWidth}") + + def toggleInjecting(self): + self.appState.can_inject = not self.appState.can_inject + + def toggleIceColors(self): + self.appState.iceColors = not self.appState.iceColors + self.ansi.initColorPairs_cga() + self.init_16_colors_misc() + def showFileInformation(self, notify = False): # eventually show a pop-up window with editable sauce info fileName = self.appState.curOpenFileName @@ -926,6 +1004,9 @@ def showFileInformation(self, notify = False): infoStringList.append(f"Height: {self.mov.sizeY}") infoStringList.append(f"Color mode: {colorMode}") + if self.appState.fileColorMode: + infoStringList.append(f"File color mode: {self.appState.fileColorMode}") + infoStringList.append(f"Playing: {self.playing} ") if len(infoStringList) > 0: infoString = ', '.join(infoStringList) @@ -939,7 +1020,7 @@ def showFileInformation(self, notify = False): # check and see if the window is wide enough for a nice side sauce wideViewer = False realmaxY,realmaxX = self.realstdscr.getmaxyx() - if realmaxX >= self.mov.sizeX + self.appState.sideInfo_minimum_width: + if realmaxX + self.appState.firstCol >= self.mov.sizeX + self.appState.sideInfo_minimum_width: wideViewer = True fileInfoColumn = self.mov.sizeX + 2 #fileInfoColumn = self.mov.sizeX + 4 @@ -1104,7 +1185,7 @@ def showScrollingHelpScreen(self): 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? + if self.appState.topLine + self.realmaxY < helpMov.sizeY: # wtf? self.appState.topLine += 1 elif c in [339, curses.KEY_PPAGE, ord('u'), ord('b'), ord('<')]: # page up, and vim keys self.appState.topLine = self.appState.topLine - self.realmaxY + 3 @@ -1113,7 +1194,7 @@ def showScrollingHelpScreen(self): 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 + self.appState.topLine = helpMov.sizeY - self.realmaxY elif c in [339, curses.KEY_HOME]: # 339 = home self.appState.topLine = 0 elif c in [338, curses.KEY_END]: # 338 = end @@ -1126,11 +1207,19 @@ def showScrollingHelpScreen(self): if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 + try: + curses.BUTTON5_PRESSED + except: + curses.BUTTON5_PRESSED = 0 + try: + curses.BUTTON4_PRESSED + except: + curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up if self.appState.topLine > 0: self.appState.topLine = self.appState.topLine - 1 elif mouseState & curses.BUTTON5_PRESSED: # wheel down - if self.appState.topLine + self.realmaxY - 3 < helpMov.sizeY - 1: # wtf? + if self.appState.topLine + self.realmaxY < helpMov.sizeY: # wtf? self.appState.topLine += 1 new_time = time.time() @@ -1239,12 +1328,42 @@ def showViewerHelp(self): self.notify(helpString, pause=True) self.cursorOff() - def startPlaying(self): + def apply_neofetch_keys(self): + """ Called by the user at Runtime to search/replace inside the app. """ + neofetch = self.appState.isAppAvail("neofetch") + if neofetch: + self.undo.push() + self.appState.fetchData = neofetcher.run() # populate fetchData with NeoFetch data ... + self.replace_neofetch_keys() # ... so this can work. + self.refresh() + else: + self.notify("Neofetch not found in path. Please install it and try again.") + return False + + def replace_neofetch_keys(self): + """ Find all the Neofetch {keys} in the drawing, and replace them with + Neofetch populated data """ + for key in neofetcher.neo_keys: + dur_key = '{' + key + '}' + self.mov.search_and_replace(self, dur_key, self.appState.fetchData[key]) + + def startPlaying(self, mov=None, opts=None): """ Start playing the animation - start a "game" style loop, make FPS by drawing if current time == greater than a delta plus the time the last frame was drawn. """ + tempMovie = None + tempOpts = None + if mov != None: # playing a movie other than the main or help file + tempMovie = self.mov + self.mov = mov + if opts != None: + tempOpts = self.opts + self.opts = opts + self.commandMode = False + oldTopLine = self.appState.topLine + oldFirstCol = self.appState.firstCol cursorMode = self.appState.cursorMode if not self.statusBar.toolButton.hidden: self.statusBar.toolButton.draw() @@ -1280,24 +1399,42 @@ def startPlaying(self): while self.playing: # catch keyboard input - to change framerate or stop playing animation # get keyboard input, returns -1 if none available - if self.appState.viewModeShowInfo: - self.showFileInformation() self.move(self.xy[0], self.xy[1]) # Here refreshScreen=False because we will self.stdscr.refresh() below, after drawing the status bar (to avoid flicker) self.refresh(refreshScreen=False) + if self.appState.viewModeShowInfo: + self.showFileInformation() if not self.appState.playOnlyMode: self.drawStatusBar() self.move(self.xy[0], self.xy[1] - 1) # reposition cursor - self.stdscr.refresh() c = self.stdscr.getch() + # handle resize + resized = False + realmaxY,realmaxX = self.realstdscr.getmaxyx() + if self.appState.realmaxY != realmaxY: + resized = True + self.appState.realmaxY = realmaxY + if self.appState.realmaxX != realmaxX: + resized = True + self.appState.realmaxX = realmaxX + if resized: + pass + #self.notify("Debug: resized") + #self.appState.topLine = 0 + #self.appState.firstCol = 0 + + #debugstring = f"self.appState.realmaxY: {self.appState.realmaxY}, self.appState.realmaxX: {self.appState.realmaxX}, topLine: {self.appState.topLine}, firstCol: {self.appState.firstCol}" + #self.addstr(self.statusBarLineNum - 1, 0, debugstring) + + self.stdscr.refresh() + if c == 27: self.metaKey = 1 self.pressingButton = False if self.pushingToClip: self.pushingToClip = False - if cursorMode != "Draw" and cursorMode != "Paint": - self.disableMouseReporting() + self.disableMouseReporting() self.commandMode = True c = self.stdscr.getch() # normal esc # Clear out any canvas state as needed for command mode. For example... @@ -1351,14 +1488,17 @@ def startPlaying(self): self.nextCharSet() elif c == 83: # alt-S - pick a character set self.showCharSetPicker() + self.stdscr.nodelay(1) # do not wait for input when calling getch elif c == 46: # alt-. - insert column self.addCol(frange=self.appState.playbackRange) elif c == 44: # alt-, - erase/pop current column self.delCol(frange=self.appState.playbackRange) - elif c == 48: # alt-/ - insert line + elif c == 47: # alt-/ - insert line self.addLine(frange=self.appState.playbackRange) elif c == 39: # alt-' - erase line self.delLine(frange=self.appState.playbackRange) + elif c == 105: # alt-i - File/Canvas Information + self.clickedInfoButton() elif c == 109 or c == 102: # alt-m or alt-f - load menu #self.statusBar.menuButton.on_click() self.commandMode = False @@ -1369,6 +1509,11 @@ def startPlaying(self): # self.statusBar.colorPickerButton.on_click() #self.statusBar.colorPickerButton.on_click() self.selectColorPicker() + elif c == ord(' '): # alt-space - insert drawing character + drawChar = self.appState.drawChar + x_param = self.xy[1] + y_param = self.xy[0] + self.insertChar(ord(drawChar), fg=self.colorfg, bg=self.colorbg, x=x_param, y=y_param, moveCursor=True, pushUndo=True, frange=self.appState.playbackRange) elif c == 122: # alt-z = undo self.clickedUndo() elif c == 114: # alt-r = redo @@ -1466,11 +1611,19 @@ def startPlaying(self): if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 + try: + curses.BUTTON5_PRESSED + except: + curses.BUTTON5_PRESSED = 0 + try: + curses.BUTTON4_PRESSED + except: + curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up if self.appState.topLine > 0: self.appState.topLine = self.appState.topLine - 1 elif mouseState & curses.BUTTON5_PRESSED: # wheel down - if self.appState.topLine + self.realmaxY - 3 < self.mov.sizeY - 1: # wtf? + if self.appState.topLine + self.realmaxY < self.mov.sizeY: # wtf? self.appState.topLine += 1 elif c in [339, curses.KEY_PPAGE, ord('u'), ord('b')]: # page up, and vim keys @@ -1481,19 +1634,18 @@ def startPlaying(self): 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 + self.appState.topLine = self.mov.sizeY - self.realmaxY # prevent a ghost image on any blank lines at the bottom: - self.stdscr.clear() - self.refresh() + #self.stdscr.clear() + #self.refresh() elif c in [339, curses.KEY_HOME]: # 339 = home self.appState.topLine = 0 elif c in [338, curses.KEY_END]: # 338 = end self.appState.topLine = self.mov.sizeY - self.realmaxY + 2 - elif c == curses.KEY_LEFT: # left - previous file - pass - elif c == curses.KEY_RIGHT: # right - next file - pass - + elif c == curses.KEY_LEFT: # left - scroll left + self.scroll_viewer_left() + elif c == curses.KEY_RIGHT: # right - scroll right + self.scroll_viewer_right() if c in [61, 43]: # esc-= and esc-+ - fps up self.increaseFPS() sleep_time = (1000.0 / self.opts.framerate) / 1000.0 @@ -1523,10 +1675,10 @@ def startPlaying(self): #self.showFileInformation() - elif c == curses.KEY_DOWN: - if self.appState.topLine + self.realmaxY - 3 < self.mov.sizeY - 1: # wtf? + elif c in [curses.KEY_DOWN, ord('j')]: + if self.appState.topLine + self.realmaxY < self.mov.sizeY: # wtf? self.appState.topLine += 1 - elif c == curses.KEY_UP: + elif c in [curses.KEY_UP, ord('k')]: if self.appState.topLine > 0: self.appState.topLine = self.appState.topLine - 1 elif c == 12: # ctrl-l - harder refresh @@ -1555,6 +1707,14 @@ def startPlaying(self): if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 + try: + curses.BUTTON5_PRESSED + except: + curses.BUTTON5_PRESSED = 0 + try: + curses.BUTTON4_PRESSED + except: + curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up self.move_cursor_up() elif mouseState & curses.BUTTON5_PRESSED: # wheel down @@ -1598,31 +1758,33 @@ def startPlaying(self): 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 + if not self.appState.narrowWindow: + if mouseX in [tOffset, tOffset + 1]: # clicked pause button + self.clickHighlight(tOffset, "||") + self.stopPlaying() + elif mouseX == 12 + offset: # clicked FPS down + self.clickHighlight(12 + offset, "<") + self.decreaseFPS() + sleep_time = (1000.0 / self.opts.framerate) / 1000.0 + elif mouseX == 16 + offset: # clicked FPS up + self.clickHighlight(16 + offset, ">") + self.increaseFPS() + sleep_time = (1000.0 / self.opts.framerate) / 1000.0 elif mouseY == self.statusBarLineNum+1: # clicked bottom bar - if 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: + if not self.appState.narrowWindow: + if mouseX in range(4,20): + fg = mouseX - 3 + self.setFgColor(fg) + elif mouseX in range(25,33): + bg = mouseX - 24 + self.setBgColor(bg) + elif mouseX == self.chMap_offset + len(self.chMapString): # clicked next character set + self.clickHighlight(self.chMap_offset + len(self.chMapString), ">", bar='bottom') + self.nextCharSet() + elif mouseX == self.chMap_offset - 1: # clicked previous character set + self.clickHighlight(self.chMap_offset - 1, "<", bar='bottom') + self.prevCharSet() + if self.appState.debug: self.notify("bottom bar. " + str([mouseX, mouseY])) elif self.appState.debug: self.notify("clicked. " + str([mouseX, mouseY])) @@ -1654,6 +1816,8 @@ def startPlaying(self): self.statusBar.colorPickerButton.on_click() elif c in [330]: # delete self.deleteKeyPop(frange=self.appState.playbackRange) + elif c in [383]: # shift-delete - delete from opposite direction + self.reverseDelete(frange=self.appState.playbackRange) elif c in [1, curses.KEY_HOME]: # ctrl-a or home self.xy[1] = 1 elif c in [5, curses.KEY_END]: # ctrl-e or end @@ -1710,8 +1874,11 @@ def startPlaying(self): 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]) + if not self.appState.playOnlyMode: + if self.mov.currentFrameNumber == self.appState.playbackRange[1]: + self.mov.gotoFrame(self.appState.playbackRange[0]) + else: + self.mov.nextFrame() else: self.mov.nextFrame() if not self.appState.playOnlyMode: # if we're not in play-only @@ -1728,6 +1895,14 @@ def startPlaying(self): #self.refresh() else: time.sleep(0.005) # to keep from sucking up cpu + + if tempMovie != None: # Need to switch back to main movie + self.mov = tempMovie + if tempOpts != None: + self.opts = tempOpts + + self.appState.topLine = oldTopLine + self.appState.firstCol = oldFirstCol self.stdscr.nodelay(0) # back to wait for input when calling getch self.cursorOn() @@ -1765,6 +1940,7 @@ def initCharSet(self): # we can have nextCharSet and PrevCharSet to switch betwe # geometric shapes #{'f1':0x25dc, 'f2':0x25dd, 'f3':0x25de, 'f4':0x25df, 'f5':0x25e0, 'f6':0x25e1, 'f7':0x25e2, 'f8':0x25e3, 'f9':0x25e4, 'f10':0x25e5}, # little curves and triangles {'f1':0x25e2, 'f2':0x25e3, 'f3':0x25e5, 'f4':0x25e4, 'f5':0x25c4, 'f6':0x25ba, 'f7':0x25b2, 'f8':0x25bc, 'f9':0x25c0, 'f10':0x25b6 }, # little curves and triangles + #{'f1':'🮜', 'f2':'🮝', 'f3':'🮞', 'f4':'🮟', 'f5':0x25c4, 'f6':0x25ba, 'f7':0x25b2, 'f8':0x25bc, 'f9':0x25c0, 'f10':0x25b6 }, # little curves and triangles # terminal graphic characters {'f1':9622, 'f2':9623, 'f3':9624, 'f4':9625, 'f5':9626, 'f6':9627, 'f7':9628, 'f8':9629, 'f9':9630, 'f10':9631 }, # terminal graphic characters @@ -1795,13 +1971,13 @@ def initCharSet(self): # we can have nextCharSet and PrevCharSet to switch betwe 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 + #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] @@ -1851,9 +2027,22 @@ def refreshCharMap(self): # checks self.charMapNumber and does the rest self.chMap = self.fullCharMap[self.charMapNumber] #self.chMapString = "F1%c F2%c F3%c F4%c F5%c F6%c F7%c F8%c F9%c F10%c " % \ + # Build a string, one character a time + self.chMapString = "" + for fkey in range(1,11): + keyIndex = f"f{fkey}" + keyString = f"F{fkey}" + try: # See if we can encode character in the current mode. + #keyChar = chr(self.chMap[keyIndex]).encode(self.appState.charEncoding) + keyChar = chr(self.chMap[keyIndex]).encode(self.appState.charEncoding) + #keyChar = keyChar.encode(self.appState.charEncoding) + except: # If character can't encode, eg: in cp437 mode, make it blank + keyChar = ' ' + self.chMapString += f"{keyString}{keyChar}" self.chMapString = "F1%cF2%cF3%cF4%cF5%cF6%cF7%cF8%cF9%cF10%c" % \ (self.chMap['f1'], self.chMap['f2'], self.chMap['f3'], self.chMap['f4'], self.chMap['f5'], \ self.chMap['f6'], self.chMap['f7'], self.chMap['f8'], self.chMap['f9'], self.chMap['f10'] ) + #self.chMapString = self.chMapStringncoding=self.appState.charEncoding) def clearStatusLine(self): # fyi .. width and height in this context should go into @@ -1870,15 +2059,29 @@ def resizeHandler(self): topLine = self.mov.sizeY - self.realmaxY - 2 if topLine < 0: topLine = 0 - self.appState.topLine = topLine + #self.appState.topLine = topLine if self.xy[0] < self.appState.topLine: # if cursor is off screen self.xy[0] = self.appState.topLine # put it back on def drawStatusBar(self): if self.statusBar.hidden: return False - + if self.realmaxX >= self.appState.full_ui_width: # Wide big window + self.appState.narrowWindow = False + # show menu buttons + self.statusBar.menuButton.show() + self.statusBar.toolButton.show() + self.statusBar.animButton.show() + self.statusBar.drawCharPickerButton.show() + else: # narrow small window + self.appState.narrowWindow = True + # hide menu buttons + self.statusBar.menuButton.hide() + self.statusBar.toolButton.hide() + self.statusBar.animButton.hide() + self.statusBar.drawCharPickerButton.hide() + self.setWindowTitle(self.appState.curOpenFileName) mainColor = self.appState.theme['mainColor'] @@ -1897,6 +2100,7 @@ def drawStatusBar(self): if self.appState.realmaxY != realmaxY: resized = True if self.appState.realmaxX != realmaxX: + #self.notify("resized") resized = True if resized: @@ -1909,7 +2113,10 @@ def drawStatusBar(self): # 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 + if self.appState.narrowWindow: + self.line_1_offset = -2 # anchor left + else: + self.line_1_offset = realmaxX - 63 # anchor to the right by transport line_1_offset = self.line_1_offset statusBarLineNum = realmaxY - 2 @@ -1981,21 +2188,23 @@ def drawStatusBar(self): self.statusBar.menuButton.update_real_xy(x = statusBarLineNum) self.statusBar.toolButton.update_real_xy(x = statusBarLineNum) self.statusBar.animButton.update_real_xy(x = statusBarLineNum) + self.statusBar.colorPickerButton.update_real_xy(x = statusBarLineNum + 1) if self.appState.showCharSetButton: self.statusBar.charSetButton.update_real_xy(x = statusBarLineNum + 1) - # Put character picker button left of character picker - self.statusBar.drawCharPickerButton.update_real_xy(x = statusBarLineNum + 1) - self.statusBar.drawCharPickerButton.update_real_xy(y = self.chMap_offset-11) + if not self.appState.narrowWindow: + # Put character picker button left of character picker + self.statusBar.drawCharPickerButton.update_real_xy(x = statusBarLineNum + 1) + self.statusBar.drawCharPickerButton.update_real_xy(y = self.chMap_offset-11) #if self.appState.colorMode == "256": # self.statusBar.colorPickerButton.update_real_xy(x = statusBarLineNum + 1) canvasSizeBar = f"[{self.mov.sizeX}x{self.mov.sizeY}]" canvasSizeOffset = realmaxX - len(canvasSizeBar) - 1 # right of transport self.addstr(statusBarLineNum, canvasSizeOffset, canvasSizeBar, curses.color_pair(mainColor)) - frameBar = "F: %i/%i " % (self.mov.currentFrameNumber, self.mov.frameCount) - rangeBar = "R: %i-%i " % (self.appState.playbackRange[0], self.appState.playbackRange[1]) - fpsBar = ": %i " % (self.opts.framerate) - delayBar = "D: %.2f " % (self.mov.currentFrame.delay) + 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. @@ -2005,26 +2214,43 @@ def drawStatusBar(self): 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 + if self.appState.narrowWindow: + if self.appState.colorMode == "16": + self.chMap_offset = 1 # how far in to show the character map + else: + self.chMap_offset = 7 # how far in to show the character map + else: + self.chMap_offset = realmaxX - 50 # how far in to show the character map # > is hardcoded at 66. yeesh. chMap_next_offset = self.chMap_offset + 31 # Move the draw button to under the frameBar for 16 color mode - if self.appState.colorMode == "16": - self.statusBar.drawCharPickerButton.update_real_xy(x = statusBarLineNum) - self.statusBar.drawCharPickerButton.update_real_xy(y = frameBar_offset-5) - if realmaxX < 87: # too small to show the draw character. - self.statusBar.drawCharPickerButton.hide() - else: - self.statusBar.drawCharPickerButton.show() - if self.appState.colorMode == "256": - self.statusBar.drawCharPickerButton.show() + #if self.appState.colorMode == "16": + # self.statusBar.drawCharPickerButton.update_real_xy(x = statusBarLineNum) + # self.statusBar.drawCharPickerButton.update_real_xy(y = frameBar_offset-5) + # if realmaxX < 87: # too small to show the draw character. + # self.statusBar.drawCharPickerButton.hide() + # else: + # self.statusBar.drawCharPickerButton.show() + #if self.appState.colorMode == "256": + # self.statusBar.drawCharPickerButton.show() # Draw elements that aren't in the GUI framework self.addstr(statusBarLineNum, frameBar_offset, frameBar, curses.color_pair(mainColor)) self.addstr(statusBarLineNum, fpsBar_offset, fpsBar, curses.color_pair(mainColor)) self.addstr(statusBarLineNum, delayBar_offset, delayBar, curses.color_pair(mainColor)) self.addstr(statusBarLineNum, rangeBar_offset, rangeBar, curses.color_pair(mainColor)) + #if not self.appState.narrowWindow: # Wide big window + # self.addstr(statusBarLineNum, frameBar_offset, frameBar, curses.color_pair(mainColor)) + # self.addstr(statusBarLineNum, fpsBar_offset, fpsBar, curses.color_pair(mainColor)) + # self.addstr(statusBarLineNum, delayBar_offset, delayBar, curses.color_pair(mainColor)) + # self.addstr(statusBarLineNum, rangeBar_offset, rangeBar, curses.color_pair(mainColor)) + #else: # Narrow small window + # # Draw frame #. that's important. + # frameBar_offset = 0 + # fpsBar_offset = frameBar_offset + len(frameBar) + 1 + # self.addstr(statusBarLineNum, frameBar_offset, frameBar, curses.color_pair(mainColor)) + # self.addstr(statusBarLineNum, fpsBar_offset, fpsBar, curses.color_pair(mainColor)) # Move char button to the left of frameBar_offset @@ -2039,7 +2265,7 @@ def drawStatusBar(self): colorValue = curses.color_content(self.colorfg) debugstring = f"Fg: {self.colorfg}, bg: {self.colorbg}, cpairs: {cp}, {cp2}, pairs: {pairs}, ext: {extColors}, {colorValue}" self.addstr(statusBarLineNum-1, 0, debugstring, curses.color_pair(mainColor)) - debugstring2 = f"ButtonPress: {self.pressingButton}" + debugstring2 = f"ButtonPress: {self.pressingButton}, topLine: {self.appState.topLine}, firstCol: {self.appState.firstCol}, resized: {resized}" self.addstr(statusBarLineNum-2, 0, debugstring2, curses.color_pair(mainColor)) colorValue = curses.color_content(self.colorbg) debugstring3= f"bg: {colorValue}" @@ -2058,6 +2284,14 @@ def drawStatusBar(self): # Draw character map for f1-f10 (block characters) self.addstr(statusBarLineNum+1, self.chMap_offset-1, "<", curses.color_pair(clickColor) | curses.A_BOLD) self.addstr(statusBarLineNum+1, self.chMap_offset+31, ">", curses.color_pair(clickColor) | curses.A_BOLD) + + #if self.appState.charEncoding == "cp437": + # try: + # chMapString = self.chMapString.encode('cp437') + # except: # can't encode as cp437, so just render as blank + # chMapString = ' ' + + if self.colorfg > 8 and self.appState.colorMode == "16": # bright color self.addstr(statusBarLineNum+1, self.chMap_offset, self.chMapString, curses.color_pair(self.colorpair) | curses.A_BOLD) else: # normal color @@ -2065,7 +2299,6 @@ def drawStatusBar(self): # 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: @@ -2080,7 +2313,7 @@ def drawStatusBar(self): # y = y + 3 # draw 16-color picker - if self.appState.colorMode == "16": + if self.appState.colorMode == "16" and self.realmaxX > self.appState.full_ui_width: colorPickerFGOffset = 0 self.addstr(statusBarLineNum+1, colorPickerFGOffset, "FG:", curses.color_pair(mainColor)) #hiColor = 7 # for old way, using bold to get high colors @@ -2130,27 +2363,37 @@ def drawStatusBar(self): self.addstr(statusBarLineNum + 1, locationStringOffset - 1, "*", curses.color_pair(3) | curses.A_BOLD) else: self.addstr(statusBarLineNum + 1, locationStringOffset - 1, " ", curses.color_pair(3) | curses.A_BOLD) - # Draw Range, FPS and Delay buttons - self.addstr(statusBarLineNum, 13 + line_1_offset, "<", curses.color_pair(clickColor) | curses.A_BOLD) # FPS buttons - self.addstr(statusBarLineNum, 17 + line_1_offset, ">", curses.color_pair(clickColor) | curses.A_BOLD) - if self.appState.modified: - self.addstr(statusBarLineNum + 1, realmaxX - 1, "*", curses.color_pair(4) | curses.A_BOLD) - else: - self.addstr(statusBarLineNum + 1, realmaxX - 1, " ", curses.color_pair(4) | curses.A_BOLD) - if not self.playing: - self.addstr(statusBarLineNum, 2 + line_1_offset, "F", curses.color_pair(clickColor) | curses.A_BOLD) # Frame button - self.addstr(statusBarLineNum, 31 + line_1_offset, "R", curses.color_pair(clickColor) | curses.A_BOLD) # Range button - self.addstr(statusBarLineNum, 23 + line_1_offset, "D", curses.color_pair(clickColor) | curses.A_BOLD) # Delay button + if self.realmaxX >= self.appState.full_ui_width: + # Draw Range, FPS and Delay buttons + self.addstr(statusBarLineNum, 13 + line_1_offset, "<", curses.color_pair(clickColor) | curses.A_BOLD) # FPS buttons + self.addstr(statusBarLineNum, 17 + line_1_offset, ">", curses.color_pair(clickColor) | curses.A_BOLD) + if self.appState.modified: + self.addstr(statusBarLineNum + 1, realmaxX - 1, "*", curses.color_pair(4) | curses.A_BOLD) + else: + self.addstr(statusBarLineNum + 1, realmaxX - 1, " ", curses.color_pair(4) | curses.A_BOLD) + if not self.playing: + self.addstr(statusBarLineNum, 2 + line_1_offset, "F", curses.color_pair(clickColor) | curses.A_BOLD) # Frame button + self.addstr(statusBarLineNum, 31 + line_1_offset, "R", curses.color_pair(clickColor) | curses.A_BOLD) # Range button + self.addstr(statusBarLineNum, 23 + line_1_offset, "D", curses.color_pair(clickColor) | curses.A_BOLD) # Delay button # draw transport transportString = "|< << |> >> >|" transportOffset = realmaxX - len(transportString) - 9 self.transportOffset = transportOffset - if self.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 = "|< << |> >> >|" + if self.appState.narrowWindow: # small window, only show |> + transportOffset = realmaxX - 12 + if self.playing: + playButtonString = "||" + else: + playButtonString = "|>" + self.addstr(statusBarLineNum, transportOffset, playButtonString, curses.color_pair(clickColor) | curses.A_BOLD) + + else: # wide window, show full transport + if self.playing: + transportString = "|< << || >> >|" + self.addstr(statusBarLineNum, transportOffset, transportString, curses.color_pair(mainColor)) + self.addstr(statusBarLineNum, transportOffset+6, "||", curses.color_pair(clickColor) | curses.A_BOLD) + else: + transportString = "|< << |> >> >|" self.addstr(statusBarLineNum, transportOffset, transportString, curses.color_pair(clickColor) | curses.A_BOLD) # Draw the new status bar if self.commandMode: @@ -2189,6 +2432,16 @@ def drawStatusBar(self): prev_tip.set_location(row = statusBarLineNum, column = trans_prev_offset) next_tip = self.statusBar.other_tooltips.get_tip("k") next_tip.set_location(row = statusBarLineNum, column = trans_next_offset) + + # hide un-used tool tips in narrow view + if self.appState.narrowWindow: + play_tip.alwaysHidden = True + prev_tip.alwaysHidden = True + next_tip.alwaysHidden = True + else: + play_tip.alwaysHidden = False + prev_tip.alwaysHidden = False + next_tip.alwaysHidden = False #if self.appState.colorMode == "16": # colorPicker_tip.hide() @@ -2198,8 +2451,8 @@ def drawStatusBar(self): 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 self.xy[1] > self.mov.sizeX: # cursor is past right edge of the canvas + self.xy[1] = self.mov.sizeX # if it's off screen.. fix that, too if self.xy[0] - self.appState.topLine > realmaxY - 3: self.xy[0] = realmaxY - 3 + self.appState.topLine @@ -2270,6 +2523,11 @@ def mainLoop(self): mouseX, mouseY = 0, 0 self.pressingButton = False self.drawStatusBar() # to make sure the inital state looks correct + + intense_burning = ['idkfa'] + sumbitch = [] + sumbitch_len = 25 + curses.noecho() while 1: # Real "main loop" - get user input, aka "edit mode" self.testWindowSize() @@ -2279,7 +2537,8 @@ def mainLoop(self): # spot for refresh curses.panel.update_panels() self.stdscr.refresh() - c = self.stdscr.getch() + c = self.stdscr.getch() # non-wide characters, for old ncurses + #c = ord(self.stdscr.get_wch()) # wide characters, for ncursesw self.testWindowSize() #if c in ["\x1b\x1b\x5b\x42"]: self.notify("alt-down") @@ -2356,13 +2615,20 @@ def mainLoop(self): self.addLine() elif c == 39: # alt-' - erase line self.delLine() - elif c == 121: # alt-y - Eyedrop + elif c == 121 or c == ord('u'): # alt-y - Eyedrop, alt-u is what Aciddraw used self.eyeDrop(self.xy[1] - 1, self.xy[0]) # cursor position elif c == ord('P'): # alt-P - pick up charcter self.pickUpDrawingChar(self.xy[1] - 1, self.xy[0]) #self.notify(f"Picked up character: {self.appState.drawChar}") + elif c == ord(' '): # alt-space - insert drawing character + drawChar = self.appState.drawChar + x_param = self.xy[1] + y_param = self.xy[0] + self.insertChar(ord(drawChar), fg=self.colorfg, bg=self.colorbg, x=x_param, y=y_param, moveCursor=True, pushUndo=True) elif c == ord('l'): # alt-l - color under cursor self.insertColor(fg=self.colorfg, bg=self.colorbg, pushUndo=True) + elif c == ord('L'): # alt-L - search and replace color + self.replaceColorUnderCursor() elif c == 73: # alt-I - Character Inspector self.showCharInspector() elif c == 105: # alt-i - File/Canvas Information @@ -2380,7 +2646,7 @@ def mainLoop(self): 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() + #self.statusBar.toolButton.on_click() elif c == 99: # alt-c - color picker self.commandMode = False #if self.appState.colorMode == "256": @@ -2388,7 +2654,8 @@ def mainLoop(self): # self.statusBar.colorPicker.switchTo() #else: # self.statusBar.colorPickerButton.on_click() - self.selectColorPicker() + #self.selectColorPicker() + self.statusBar.colorPickerButton.on_click() # Animation Keystrokes elif c == 68: #alt-D - set delay for current frame self.getDelayValue() @@ -2438,47 +2705,59 @@ def mainLoop(self): 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]) + startPoint=(self.xy[0] + self.appState.topLine, self.xy[1] + self.appState.firstCol) self.startSelecting(firstkey=c) # start selecting text elif c in [ord('1')]: # esc-1 copy of F1 - insert extended character self.insertChar(self.chMap['f1'], fg=self.colorfg, bg=self.colorbg) - self.hardRefresh() + #self.hardRefresh() + self.stdscr.refresh() c = None elif c in [ord('2')]: # esc-2 copy of F2 - insert extended character self.insertChar(self.chMap['f2'], fg=self.colorfg, bg=self.colorbg) - self.hardRefresh() + #self.hardRefresh() + #self.refresh() + #self.refresh(refreshScreen=False) + self.stdscr.refresh() c = None elif c in [ord('3')]: # F3 - insert extended character self.insertChar(self.chMap['f3'], fg=self.colorfg, bg=self.colorbg) - self.hardRefresh() + #self.hardRefresh() + self.stdscr.refresh() c = None elif c in [ord('4')]: # F4 - insert extended character self.insertChar(self.chMap['f4'], fg=self.colorfg, bg=self.colorbg) - self.hardRefresh() + #self.refresh() + self.stdscr.refresh() c = None elif c in [ord('5')]: # F5 - insert extended character self.insertChar(self.chMap['f5'], fg=self.colorfg, bg=self.colorbg) - self.hardRefresh() + #self.hardRefresh() + self.stdscr.refresh() c = None elif c in [ord('6')]: # F6 - insert extended character self.insertChar(self.chMap['f6'], fg=self.colorfg, bg=self.colorbg) - self.hardRefresh() + #self.hardRefresh() + self.stdscr.refresh() c = None elif c in [ord('7')]: # F7 - insert extended character self.insertChar(self.chMap['f7'], fg=self.colorfg, bg=self.colorbg) - self.hardRefresh() + #self.hardRefresh() + self.stdscr.refresh() c = None elif c in [ord('8')]: # F8 - insert extended character self.insertChar(self.chMap['f8'], fg=self.colorfg, bg=self.colorbg) - self.hardRefresh() + #self.hardRefresh() + self.stdscr.refresh() c = None elif c in [ord('9')]: # F9 - insert extended character self.insertChar(self.chMap['f9'], fg=self.colorfg, bg=self.colorbg) - self.hardRefresh() + #self.hardRefresh() + self.stdscr.refresh() c = None elif c in [ord('0')]: # F10 - insert extended character self.insertChar(self.chMap['f10'], fg=self.colorfg, bg=self.colorbg) - self.hardRefresh() + #self.hardRefresh() + self.stdscr.refresh() c = None elif c == 27: # 2nd esc byte - possibly alt-arrow. # eg: alt-down: 27 27 91 66 or \x1b\x1b\x5b\x42 @@ -2528,7 +2807,7 @@ def mainLoop(self): self.pushingToClip = False if self.appState.debug: if c == ord('X'): # esc-X - drop into pdb debugger - pdb.set_trace() + self.jumpToPythonConsole() else: self.notify("keystroke: %d" % c) # alt-unknown self.commandMode = False @@ -2569,6 +2848,8 @@ def mainLoop(self): self.backspace() elif c in [330]: # delete self.deleteKeyPop() + elif c in [383]: # shift-delete - delete from opposite direction + self.reverseDelete() elif c in [9, 353]: # 9 = tab, 353 = shift-tab #if self.appState.colorMode == "256": #self.statusBar.colorPickerButton.on_click() @@ -2666,7 +2947,8 @@ def mainLoop(self): #self.notify("Released from drag, hopefully.") if self.appState.debug: # clear mouse state lines - blank_line = " " * 80 + winHeight,winWidth = self.realstdscr.getmaxyx() + blank_line = " " * winWidth self.addstr(self.statusBarLineNum-3, 0, blank_line, curses.color_pair(3) | curses.A_BOLD) self.addstr(self.statusBarLineNum-4, 0, blank_line, curses.color_pair(3) | curses.A_BOLD) # print mouse state @@ -2677,7 +2959,7 @@ def mainLoop(self): pass #if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX \ # and mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum: - if mouseY + self.appState.topLine < self.mov.sizeY and mouseX < self.mov.sizeX: + if mouseY + self.appState.topLine < self.mov.sizeY and mouseX + self.appState.firstCol < self.mov.sizeX: # we're in the canvas, not playing if mouseState & curses.BUTTON1_PRESSED: @@ -2712,13 +2994,27 @@ def mainLoop(self): if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 + try: + curses.BUTTON5_PRESSED + except: + curses.BUTTON5_PRESSED = 0 + try: + curses.BUTTON4_PRESSED + except: + curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up - self.move_cursor_up() + if self.appState.scrollColors: + self.nextFgColor() + else: + self.move_cursor_up() elif mouseState & curses.BUTTON5_PRESSED: # wheel down - self.move_cursor_down() + if self.appState.scrollColors: + self.prevFgColor() + else: + self.move_cursor_down() elif self.appState.cursorMode == "Move": # select mode/move the cursor if self.pressingButton: - self.xy[1] = mouseX + 1 # set cursor position + self.xy[1] = mouseX + 1 + self.appState.firstCol # set cursor position self.xy[0] = mouseY + self.appState.topLine elif self.appState.cursorMode == "Draw": if self.pressingButton: @@ -2731,7 +3027,7 @@ def mainLoop(self): # Insert the selected character. drawChar = self.appState.drawChar try: - x_param = mouseX + 1 + x_param = mouseX + 1 + self.appState.firstCol y_param = mouseY + self.appState.topLine self.insertChar(ord(drawChar), fg=self.colorfg, bg=self.colorbg, x=x_param, y=y_param, moveCursor=False, pushUndo=False) except IndexError: @@ -2754,7 +3050,7 @@ def mainLoop(self): if self.appState.debug: self.addstr(self.statusBarLineNum-2, 20, "Paint painting.", curses.color_pair(6) | curses.A_BOLD) try: - x_param = mouseX + 1 + x_param = mouseX + 1 + self.appState.firstCol y_param = mouseY + self.appState.topLine #self.insertChar(ord(drawChar), fg=self.colorfg, bg=self.colorbg, x=x_param, y=y_param, moveCursor=False, pushUndo=False) self.pasteFromClipboard(startPoint = [y_param, x_param], clipBuffer=self.appState.brush, transparent=True, pushUndo=False) @@ -2769,24 +3065,24 @@ def mainLoop(self): elif self.appState.cursorMode == "Color": # Change the color under the cursor if self.pressingButton: # also set cursor position - self.xy[1] = mouseX + 1 # set cursor position + self.xy[1] = mouseX + 1 + self.appState.firstCol # set cursor position self.xy[0] = mouseY + self.appState.topLine - self.insertColor(fg=self.colorfg, bg=self.colorbg, x=mouseX+1, y=mouseY + self.appState.topLine, pushUndo=False) + self.insertColor(fg=self.colorfg, bg=self.colorbg, x=mouseX+1 + self.appState.firstCol, y=mouseY + self.appState.topLine, pushUndo=False) self.refresh() elif self.appState.cursorMode == "Erase": # Erase character under the cursor # also set cursor position if self.pressingButton: - self.xy[1] = mouseX + 1 # set cursor position + self.xy[1] = mouseX + 1 + self.appState.firstCol# set cursor position self.xy[0] = mouseY + self.appState.topLine color_fg = self.appState.defaultFgColor color_bg = self.appState.defaultBgColor - self.insertChar(ord(' '), fg=color_fg, bg=color_bg, x=mouseX, y=mouseY + self.appState.topLine, pushUndo=False) + self.insertChar(ord(' '), fg=color_fg, bg=color_bg, x=mouseX + self.appState.firstCol, y=mouseY + self.appState.topLine, pushUndo=False) elif self.appState.cursorMode == "Eyedrop": # Change the color under the cursor - self.eyeDrop(mouseX, mouseY + self.appState.topLine) + self.eyeDrop(mouseX + self.appState.firstCol, mouseY + self.appState.topLine) self.statusBar.setCursorModeMove() self.drawStatusBar() elif self.appState.cursorMode == "Select": # this does not exist lol - self.xy[1] = mouseX + 1 # set cursor position + self.xy[1] = mouseX + 1 + self.appState.firstCol # set cursor position self.xy[0] = mouseY + self.appState.topLine self.startSelecting(mouse=True) @@ -2805,6 +3101,14 @@ def mainLoop(self): if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 + try: + curses.BUTTON5_PRESSED + except: + curses.BUTTON5_PRESSED = 0 + try: + curses.BUTTON4_PRESSED + except: + curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON1_PRESSED or mouseState & curses.BUTTON4_PRESSED or mouseState & curses.BUTTON5_PRESSED or b1_press > 0 or mouseState == curses.BUTTON1_DOUBLE_CLICKED: #print('\033[?1003h') #self.notify("Farfenugen") @@ -2828,42 +3132,43 @@ def mainLoop(self): 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() + if not self.appState.narrowWindow: + if mouseX in [tOffset + 6, tOffset + 7]: # clicked play button + self.clickHighlight(tOffset + 6, "|>") + self.startPlaying() + self.metaKey = 0 + elif mouseX in [tOffset + 3, tOffset + 4]: # goto prev frame + self.clickHighlight(tOffset + 3, "<<") + self.mov.prevFrame() + elif mouseX in [tOffset + 9, tOffset + 10]: # goto next frame + self.clickHighlight(tOffset + 9, ">>") + self.mov.nextFrame() + elif mouseX in [tOffset, tOffset + 1]: # goto first frame + self.clickHighlight(tOffset, "|<") + self.mov.gotoFrame(1) + elif mouseX in [tOffset + 12, tOffset + 13]: # goto last frame + self.clickHighlight(tOffset + 12, ">|") + self.mov.nextFrame() + self.mov.gotoFrame(self.mov.frameCount) + if mouseX == 13 + offset: # clicked FPS down + self.clickHighlight(13 + offset, "<") + self.decreaseFPS() + elif mouseX == 17 + offset: # clicked FPS up + self.clickHighlight(17 + offset, ">") + self.increaseFPS() + elif mouseX == 23 + offset: # clicked Delay button + self.clickHighlight(23 + offset, "D") + self.getDelayValue() + elif mouseX == 31 + offset: # clicked Range button + self.clickHighlight(31 + offset, "R") + self.getPlaybackRange() + elif mouseX == 2 + offset: # clicked Frame button + self.clickHighlight(2 + offset, "F") + self.gotoFrameGetInput() elif mouseY == self.statusBarLineNum+1: # clicked bottom bar - if self.appState.colorMode == "16": + if self.appState.colorMode == "16" and self.realmaxX >= self.appState.full_ui_width: if mouseX in range(3,19): # clicked a fg color fg = mouseX - 2 self.setFgColor(fg) @@ -2916,7 +3221,7 @@ def mainLoop(self): realmaxY,realmaxX = self.realstdscr.getmaxyx() cmode = self.appState.cursorMode #if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX \ - if mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum and mouseX < self.mov.sizeX: # we're in the canvas + if mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum and mouseX + self.appState.firstCol < self.mov.sizeX: # we're in the canvas if cmode == "Draw" or cmode == "Color" or cmode == "Erase" or cmode == "Paint": self.undo.push() else: # Not in the canvas, so give the GUI a click @@ -2938,30 +3243,95 @@ def mainLoop(self): # 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]) + startPoint=(self.xy[0] + self.appState.topLine, self.xy[1] + self.appState.firstCol) self.startSelecting(firstkey=c) # start selecting text # pass c to something here that starts selecting and moves # the cursor based on c, and knows whether it's selecting # via shift-arrow or mouse. elif c == None: pass - elif c <= 128 and c >= 32: # normal printable character + elif c <= 128 and c >= 32: # printable ASCII character self.insertChar(c, fg=self.colorfg, bg=self.colorbg) + + sumbitch.append(chr(c)) # cheat codes + if len(sumbitch) >= sumbitch_len: + sumbitch.pop(0) + for heat in intense_burning: + if ''.join(sumbitch).endswith(heat): + self.activate_heat_code() + self.appState.renderMouseCursor = False + #else: + # self.notify(f"Weird character: {chr(c)} or {c}") #self.drawStatusBar() if self.appState.viewModeShowInfo: self.showFileInformation() self.refresh(refreshScreen=False) #self.hardRefresh() - def selectColorPicker(self): + def selectColorPicker(self, message=None): #if self.appState.colorMode == "256": self.appState.colorPickerSelected = True #self.statusBar.colorPicker.handler.showColorPicker() - self.statusBar.colorPicker.showFgPicker() + self.statusBar.colorPicker.showFgPicker(message=message) #if self.appState.colorMode == "16": # self.statusBar.colorPicker_bg_16.showFgPicker() self.appState.colorPickerSelected = False + def replaceColorUnderCursor(self): + self.commandMode = False + #self.notify("Pick a new color") + # Search and replace color under cursor + # Save old (UI) color setting, so we can use the color picker and then set the color back when done + ui_fg = self.colorfg + ui_bg = self.colorbg + #charColor = [self.colorfg, self.colorbg] + #old_color_pair = + # Get color pair under cursor + #old_fg, new_bg = self.mov.currentFrame.newColorMap[line][col] + old_fg, old_bg = self.mov.currentFrame.newColorMap[self.xy[0]][self.xy[1]-1] + oldCharColor = [old_fg, old_bg] + # Print a message for the user to set the New color + self.clearStatusLine() + printMessage = "Please pick the New color." + self.addstr(self.statusBarLineNum, 0, printMessage, curses.color_pair(self.appState.theme['notificationColor'])) + self.stdscr.refresh() + # Use color picker to pick new destination color (pair) + #self.selectColorPicker(message=printMessage) + self.selectColorPicker() + self.clearStatusLine() + new_fg = self.colorfg + new_bg = self.colorbg + newCharColor = [new_fg, new_bg] + # Use movie search and replace color function + + askingAboutRange = False + if self.mov.hasMultipleFrames(): + self.promptPrint("Apply to all frames in playback range (Y/N)? ") + askingAboutRange = True + else: # only 1 frame in movie, so just apply to akk without asking + self.undo.push() + self.mov.search_and_replace_color_pair(oldCharColor, newCharColor) + askingAboutRange = False + while askingAboutRange: + prompt_ch = self.stdscr.getch() + if chr(prompt_ch) in ['y', 'Y']: # yes, all in range + self.undo.push() + self.mov.search_and_replace_color_pair(oldCharColor, newCharColor, frange=self.appState.playbackRange) + askingAboutRange = False + if chr(prompt_ch) in ['n', 'N']: # No, only current frame + self.undo.push() + self.mov.search_and_replace_color_pair(oldCharColor, newCharColor) + askingAboutRange = False + elif prompt_ch == 27: # esc, cancel + askingAboutRange = False + + #self.notify(f"Replaced {oldCharColor} with {newCharColor}") + # Set UI color back to what it should be + self.setFgColor(ui_fg) + self.setBgColor(ui_bg) + self.stdscr.refresh() + + def cloneToNewFrame(self): """ Take current frame, clone it to a new one, insert it immediately after current frame """ self.undo.push() @@ -3005,18 +3375,22 @@ def shiftMovieLeft(self): #self.notify("Shifted frames to the right.") self.refresh() - def enterViewMode(self): + def enterViewMode(self, mov=None, opts=None): self.statusBar.hide() self.stdscr.clear() + old_xy = self.xy old_top_line = self.appState.topLine + old_first_col = self.appState.firstCol self.appState.topLine = 0 oldDrawBorders = self.appState.drawBorders # to turn back on when done self.appState.playOnlyMode = True - self.startPlaying() + self.startPlaying(mov=mov, opts=opts) # Return to normal when done self.appState.playOnlyMode = False self.appState.topLine = old_top_line + self.appState.firstCol = old_first_col + self.xy = old_xy self.statusBar.show() self.appState.drawBorders = oldDrawBorders self.cursorOn() @@ -3068,10 +3442,15 @@ def move_cursor_pgdown(self): # middle of screen, go to bottom elif self.xy[0] - self.appState.topLine < bottomLine: self.xy[0] = bottomLine + + if self.xy[1] < self.appState.firstCol + 1:# Scrolled off screen ot the left, Need to scroll left + self.appState.firstCol = self.xy[1] - 1 + self.appState.renderMouseCursor = False def move_cursor_topleft(self): self.appState.topLine = 0 + self.appState.firstCol = 0 self.xy[0] = 0 self.xy[1] = 1 self.refresh() @@ -3080,13 +3459,30 @@ def move_cursor_topleft(self): def move_cursor_left(self): # pressed LEFT key if self.xy[1] > 1: self.xy[1] = self.xy[1] - 1 + if self.xy[1] < self.appState.firstCol + 1:# Scrolled off screen ot the left, Need to scroll left + self.appState.firstCol = self.xy[1] - 1 self.appState.renderMouseCursor = False def move_cursor_right(self): # pressed RIGHT key if self.xy[1] < self.mov.sizeX: self.xy[1] = self.xy[1] + 1 + #lastCol = min(mov.sizeX, self.appState.realmaxX) + if self.xy[1] - self.appState.firstCol > self.appState.realmaxX: # scrolled off screen to the right, need to scroll right + self.appState.firstCol = self.xy[1] - self.appState.realmaxX self.appState.renderMouseCursor = False + def scroll_viewer_left(self): # pressed LEFT key in viewer mode + if self.appState.firstCol > 0: + self.appState.firstCol = self.appState.firstCol - 1 + + def scroll_viewer_right(self): + # if we are already scrolled right + last_column_shown = self.appState.firstCol + self.appState.realmaxX + if last_column_shown > self.mov.sizeX: + pass # do nothing + else: # otherwise, scroll right one + self.appState.firstCol += 1 + def move_cursor_up(self): # pressed UP key if self.xy[0] > 0: self.xy[0] = self.xy[0] - 1 @@ -3103,10 +3499,15 @@ def move_cursor_down(self): # pressed DOWN key def move_cursor_home(self): self.xy[1] = 1 + self.appState.firstCol = 0 + if self.xy[1] < self.appState.firstCol + 1: # Scrolled off screen ot the left, Need to scroll left + self.appState.firstCol = self.xy[1] - 1 self.appState.renderMouseCursor = False def move_cursor_end(self): self.xy[1] = self.mov.sizeX + if self.xy[1] > self.appState.realmaxX: # scrolled off screen to the right, need to scroll right + self.appState.firstCol = self.xy[1] - self.appState.realmaxX self.appState.renderMouseCursor = False def move_cursor_to_line_and_column(self, line, col): @@ -3175,6 +3576,23 @@ def safeQuit(self): if exiting: self.verySafeQuit() + def jumpToPythonConsole(self): + self.getReadyToSuspend() + pdb.set_trace() + self.resumeFromSuspend() + + def getReadyToSuspend(self): + # Get the terminal ready for fun times + curses.nocbreak() + self.stdscr.keypad(0) + curses.echo() + + def resumeFromSuspend(self): + # Get the terminal ready for fun times + curses.cbreak() + self.stdscr.keypad(1) + curses.noecho() + def verySafeQuit(self): # non-interactive part.. close out curses screen and exit. curses.nocbreak() self.stdscr.keypad(0) @@ -3315,6 +3733,9 @@ def openFromMenu(self): self.stdscr.clear() self.hardRefresh() + def toggleColorScrolling(self): + self.appState.scrollColors = not self.appState.scrollColors + def toggleSideBar(self): if self.appState.sideBarEnabled: self.appState.sideBarEnabled = False @@ -3551,7 +3972,7 @@ def showCharSetPicker(self): self.pressingButton = False if self.pushingToClip: self.pushingToClip = False - if cursorMode != "Draw" and cursorMode != "Paint": + if self.appState.cursorMode != "Draw" and self.appState.cursorMode != "Paint": print('\033[?1003l') # disable mouse reporting self.hardRefresh() curses.mousemask(1) @@ -3587,6 +4008,14 @@ def showCharSetPicker(self): if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 + try: + curses.BUTTON5_PRESSED + except: + curses.BUTTON5_PRESSED = 0 + try: + curses.BUTTON4_PRESSED + except: + curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up # scroll up # if the item isn't at the top of teh screen, move it up @@ -3622,7 +4051,7 @@ 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'] + default_masks = ['*.dur', '*.durf', '*.asc', '*.ans', '*.txt', '*.diz', '*.nfo', '*.ice', '*.ansi'] masks = default_masks if self.appState.workingLoadDirectory: if os.path.exists(self.appState.workingLoadDirectory): @@ -3662,6 +4091,8 @@ def openFilePicker(self): current_line_number = 0 search_string = '' mask_all = False + tabbed = False + show_modtime = True # show a column for file modified times top_line = 0 # topmost viewable line, for scrolling prompting = True # Turn on keyboard buffer waiting here, if necessary.. @@ -3677,27 +4108,48 @@ def openFilePicker(self): else: file_list = full_file_list - # draw list of files from top of the window to bottomk + # draw list of files from top of the window to bottom realmaxY,realmaxX = self.realstdscr.getmaxyx() page_size = realmaxY - 4 current_line_number = 0 + file_column_number = 0 + if show_modtime: + file_column_number = 20 if selected_item_number > top_line + page_size-1 or selected_item_number < top_line: # item is off the screen top_line = selected_item_number - int(page_size-3) # scroll so it's at the bottom for filename in file_list: if current_line_number >= top_line and current_line_number - top_line < page_size: # If we're within screen size - if selected_item_number == current_line_number: # if file is selected - self.addstr(current_line_number - top_line, 0, file_list[current_line_number], curses.A_REVERSE) + if selected_item_number == current_line_number and not tabbed: # if file is selected + if file_list[current_line_number] in folders: + self.addstr(current_line_number - top_line, 0, file_list[current_line_number], curses.A_REVERSE) + else: + # not a folder, print modified column + if show_modtime: + full_path = f"{current_directory}/{file_list[current_line_number]}" + file_modtime_string = durfile.get_file_mod_date_time(full_path) + self.addstr(current_line_number - top_line, 0, file_modtime_string, curses.color_pair(self.appState.theme['menuItemColor'])) + self.addstr(current_line_number - top_line, file_column_number, file_list[current_line_number], curses.A_REVERSE) else: if file_list[current_line_number] in folders: self.addstr(current_line_number - top_line, 0, file_list[current_line_number], curses.color_pair(self.appState.theme['menuTitleColor'])) else: - self.addstr(current_line_number - top_line, 0, file_list[current_line_number], curses.color_pair(self.appState.theme['promptColor'])) + if show_modtime: + full_path = f"{current_directory}/{file_list[current_line_number]}" + file_modtime_string = durfile.get_file_mod_date_time(full_path) + self.addstr(current_line_number - top_line, 0, file_modtime_string, curses.color_pair(self.appState.theme['menuItemColor'])) + self.addstr(current_line_number - top_line, file_column_number, file_list[current_line_number], curses.color_pair(self.appState.theme['promptColor'])) current_line_number += 1 if mask_all: - self.addstr(realmaxY - 4, 0, f"[X]", curses.color_pair(self.appState.theme['clickColor'])) + if tabbed: + self.addstr(realmaxY - 4, 0, f"[X]", curses.A_REVERSE) + else: + self.addstr(realmaxY - 4, 0, f"[X]", curses.color_pair(self.appState.theme['clickColor'])) else: - self.addstr(realmaxY - 4, 0, f"[ ]", curses.color_pair(self.appState.theme['clickColor'])) + if tabbed: + self.addstr(realmaxY - 4, 0, f"[ ]", curses.A_REVERSE) + else: + self.addstr(realmaxY - 4, 0, f"[ ]", curses.color_pair(self.appState.theme['clickColor'])) self.addstr(realmaxY - 4, 4, f"Show All Files", curses.color_pair(self.appState.theme['menuItemColor'])) #self.addstr(realmaxY - 4, 20, f"[PGUP]", curses.color_pair(self.appState.theme['clickColor'])) #self.addstr(realmaxY - 4, 27, f"[PGDOWN]", curses.color_pair(self.appState.theme['clickColor'])) @@ -3745,14 +4197,56 @@ def openFilePicker(self): # show it on screen self.addstr(realmaxY - 1, 0, f"{file_info}") - self.stdscr.refresh() + + + #self.stdscr.refresh() + # Read keyboard input c = self.stdscr.getch() + + if tabbed: + # Change focus from files to other elements, ie: Show All Files checkbox + #c = self.stdscr.getch() + if c in [9, curses.KEY_UP, curses.KEY_DOWN]: # 9 = Tab + # Change focus away, back to file lister + tabbed = False + elif c in [ord(' '), 13, curses.KEY_ENTER]: # 13 = enter + # Check or uncheck Show All Files + mask_all = not mask_all + if mask_all: + masks = ['*.*'] + else: + masks = default_masks + + # update file list + matched_files = [] + file_list = [] + for file in os.listdir(current_directory): + for mask in masks: + if fnmatch.fnmatch(file.lower(), mask.lower()): + matched_files.append(file) + break + for dirname in folders: + file_list.append(dirname) + file_list += sorted(matched_files) + # reset ui + selected_item_number = 0 + search_string = "" + full_file_list = file_list + elif c in [27]: # esc + tabbed = False + prompting = False + c = None + self.stdscr.clear() + if c == curses.KEY_LEFT: pass elif c == curses.KEY_RIGHT: pass + elif c in [9]: # 9 == tab key + tabbed = not tabbed + elif c == curses.KEY_UP: # move cursor up if selected_item_number > 0: @@ -3872,7 +4366,7 @@ def openFilePicker(self): 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 + if mouseCol < len(file_list[top_line+mouseLine] + file_modtime_string): # clicked within item width selected_item_number = top_line+mouseLine if mouseState == curses.BUTTON1_DOUBLE_CLICKED: if file_list[selected_item_number] in folders: # clicked directory @@ -3941,6 +4435,14 @@ def openFilePicker(self): if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 + try: + curses.BUTTON5_PRESSED + except: + curses.BUTTON5_PRESSED = 0 + try: + curses.BUTTON4_PRESSED + except: + curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up # scroll up # if the item isn't at the top of teh screen, move it up @@ -3954,6 +4456,8 @@ def openFilePicker(self): selected_item_number += 1 if selected_item_number == len(file_list) - top_line: top_line += 1 + elif c == None: + pass else: # add to search string search_string += chr(c) selected_item_number = 0 @@ -4019,7 +4523,7 @@ def convertToCurrentFormat(self, fileColorMode = None): # should this and load # 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.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 @@ -4113,16 +4617,17 @@ def loadFromFile(self, shortfile, loadFormat): # shortfile = non full path file except UnicodeDecodeError: f.close() if self.appState.charEncoding == 'utf-8': - if not self.appState.playOnlyMode: + if not self.appState.playOnlyMode and self.appState.debug: self.notify("This appears to be a CP437 ANSI/ASCII - Converting to Unicode.") f = open(filename, 'r', encoding='cp437') raw_text = f.read() # Load file into a new frame, make a new movie, - default_width= 80 # default with for ANSI file + #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) + newFrame = dur_ansiparse.parse_ansi_escape_codes(raw_text, filename = filename, appState=self.appState, caller=self, debug=self.appState.debug, maxWidth=self.appState.wrapWidth) self.appState.topLine = 0 + self.appState.firstCol = 0 newMovieOpts = Options(width=newFrame.width, height=newFrame.height) newMovie = Movie(newMovieOpts) # add the frame with the loaded ANSI file to the movie @@ -4143,7 +4648,7 @@ def loadFromFile(self, shortfile, loadFormat): # shortfile = non full path file 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: + if self.appState.debug: self.notify(f"16 color file. Switching to 16 color mode and reloading file.") self.switchTo16ColorMode() self.loadFromFile(shortfile, 'ascii') @@ -4185,16 +4690,20 @@ def loadFromFile(self, shortfile, loadFormat): # shortfile = non full path file if self.appState.debug2: self.notify(f"JSON found. Loading JSON dur file.") f.seek(0) fileColorMode, fileCharEncoding = durfile.get_dur_file_colorMode_and_charMode(f) + self.appState.fileColorMode = fileColorMode - if fileColorMode == "256" and fileColorMode != self.appState.colorMode: - #self.notify(f"Warning: Loading a 256 color file in 16 color mode. Some colors may not be displayed.") + if fileColorMode == "256" and fileColorMode != self.appState.colorMode and self.appState.maxColors == 16: if not self.appState.playOnlyMode: + self.notify(f"Loaded 256 color file in 16 color mode.") + if fileColorMode == "256" and fileColorMode != self.appState.colorMode and self.appState.maxColors > 255: + #self.notify(f"Warning: Loading a 256 color file in 16 color mode. Some colors may not be displayed.") + if self.appState.debug: self.notify(f"256 color file. Switching to 256 color mode.") self.switchTo256ColorMode() self.loadFromFile(shortfile, 'dur') - if fileCharEncoding != self.appState.charEncoding: - self.notify(f"Warning: File uses {fileCharEncoding} character encoding, but Durdraw is in {self.appState.charEncoding} mode.") + #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'] @@ -4213,7 +4722,7 @@ def loadFromFile(self, shortfile, loadFormat): # shortfile = non full path file 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: + if self.appState.debug: self.notify(f"16 color file. Switching to 16 color mode.") self.switchTo16ColorMode() self.loadFromFile(shortfile, 'dur') @@ -4228,6 +4737,9 @@ def loadFromFile(self, shortfile, loadFormat): # shortfile = non full path file if self.appState.colorMode == "256": if self.mov.contains_background_colors(): self.mov.strip_backgrounds() + + # Remove un-printable characters, eg: errant \n from old .dur files, where enter accidentally inserted ^Ms. + self.mov.strip_unprintable_characters() return True @@ -4347,7 +4859,7 @@ def save(self): encoding = "utf-8" prompting = False elif c in [13, curses.KEY_ENTER]: - encoding = "default" + encoding = self.appState.charEncoding prompting = False elif c == 27: # 27 = esc = cancel self.notify("Canceled. File not saved.") @@ -4649,8 +5161,9 @@ def saveAnsimationFile(self, filename, lastLineNum=False, lastColNum=False, firs try: f.write(string) saved = True - except UnicodeEncodeError: - self.notify("Error: Some characters were not compatible with this encoding. File not saved.") + except UnicodeEncodeError as encodeError: + self.notify("Error: Some characters were not compatible with this encoding. File not saved.", pause=True) + self.notify(f"{encodeError}", pause=True) saved = False #f.close() #return False @@ -4684,7 +5197,9 @@ def saveAnsiFile(self, filename, lastLineNum=False, lastColNum=False, firstColNu if not firstColNum: firstColNum = 0 # Don't crop leftmost blank columns if not firstLineNum: - firstLineNum = self.findFrameFirstLine(self.mov.currentFrame) + #firstLineNum = self.findFrameFirstLine(self.mov.currentFrame) + firstLineNum = 0 # also don't crop leading blank lines. rude + error_encoding = False for lineNum in range(firstLineNum, lastLineNum): # y == lines for colNum in range(firstColNum, lastColNum): char = self.mov.currentFrame.content[lineNum][colNum] @@ -4707,21 +5222,34 @@ def saveAnsiFile(self, filename, lastLineNum=False, lastColNum=False, firstColNu colorCode = self.ansi.getColorCodeIrc(1,0) else: colorCode = self.ansi.getColorCode(1,0) + # Make sure encoding this character encodes correctly + try: + #char.encode('cp437') + char.encode(encoding) + except UnicodeEncodeError as encodeError: + self.notify("Error: Some characters were not compatible with this encoding. File not saved.", pause=True) + self.notify(f"line: {lineNum}, col: {colNum} ", pause=True) + saved = False + error_encoding = True + break # If we don't have extended ncurses 6 color pairs, # we don't have background colors.. so write the background as black/0 string = string + colorCode + char if ircColors: string = string + '\n' else: - string = string + '\r\n' - 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 + #string = string + '\r\n' + string = string + '\n' + if not error_encoding: + try: + f.write(string) + saved = True + except UnicodeEncodeError as encodeError: + self.notify("Error: Some characters were not compatible with this encoding. File not saved.", pause=True) + self.notify(f"{encodeError}", pause=True) + saved = False + #f.close() + #return False if ircColors: string2 = string + '\n' else: @@ -4779,10 +5307,13 @@ 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 lineNum in range(0, frame.sizeY): for colNum in range(0, frame.sizeX): - if not frame.content[lineNum][colNum] in [' ', '']: - return lineNum # we found a non-empty character + try: + if not frame.content[lineNum][colNum] in [' ', '']: + return lineNum # we found a non-empty character + except Exception as E: + pdb.set_trace() return 1 # blank frame, only save the first line. def findFrameLastLine(self, frame): @@ -4800,10 +5331,16 @@ 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 + #for colNum in reversed(list(range(0, frame.sizeX))): + # for lineNum in reversed(list(range(0, frame.sizeY))): + for colNum in reversed(range(0, frame.sizeX)): + for lineNum in reversed(range(0, frame.sizeY)): + try: + if not frame.content[lineNum][colNum] in [' ', '']: + return colNum + 1 # we found a non-empty character + except Exception as E: + pdb.set_trace() + #pdb.set_trace() return 1 # blank frame, only save the first line. def findFrameFirstCol(self, frame): @@ -4811,7 +5348,7 @@ def findFrameFirstCol(self, frame): 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): + for lineNum in range(0, frame.sizeY): if not frame.content[lineNum][colNum] in [' ', '']: return colNum # we found a non-empty character return 1 # blank frame, only save the first line. @@ -4832,14 +5369,14 @@ def savePngFile(self, filename, lastLineNum=None, firstLineNum=None, firstColNum 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): + if not self.saveAnsiFile(tmpAnsiFileName, lastLineNum=lastLineNum, firstLineNum=firstLineNum, firstColNum=firstColNum, lastColNum=lastColNum, encoding="cp437"): self.notify("Saving ansi failed, make sure you can write to current directory.") return False devnull = open('/dev/null', 'w') if font == 'ansi': - ansiLoveReturn = subprocess.call(['ansilove', tmpAnsiFileName, tmpPngFileName], stdout=devnull) + ansiLoveReturn = subprocess.call(['ansilove', '-c', str(self.mov.sizeX + 1), tmpAnsiFileName, tmpPngFileName], stdout=devnull) else: # amiga font - ansiLoveReturn = subprocess.call(['ansilove', tmpAnsiFileName, "-f", font], stdout=devnull) + ansiLoveReturn = subprocess.call(['ansilove', '-c', str(self.mov.sizeX + 1), tmpAnsiFileName, "-f", font], stdout=devnull) devnull.close() os.remove(tmpAnsiFileName) if ansiLoveReturn == 0: # this doesnt seem right either, as ansilove always @@ -4851,7 +5388,9 @@ def savePngFile(self, filename, lastLineNum=None, firstLineNum=None, firstColNum if not lastColNum: lastColNum = self.findFrameLastCol(self.mov.currentFrame) if not firstColNum: - firstColNum = self.findFrameFirstCol(self.mov.currentFrame) + #firstColNum = self.findFrameFirstCol(self.mov.currentFrame) + # ^ don't truncate the first columns. That's rude. + firstColNum = 0 characterWidth = 8 # 9 pixels wide from PIL import Image cropImage = Image.open(tmpPngFileName) @@ -4908,6 +5447,7 @@ def saveGifFile(self, filename, font="ansi"): self.mov.nextFrame() self.refresh() # open all the pngs so we can save them to gif + from PIL import Image pngImages = [Image.open(fn) for fn in tmpPngNames] sleep_time = (1000.0 / self.opts.framerate) / 1000.0 # or 0.1 == good, too self.pngsToGif(filename, pngImages, sleep_time) @@ -4975,6 +5515,14 @@ def showStaticHelp(self): if self.playing == True: self.stdscr.nodelay(1) + def activate_heat_code(self): + # hell's inferno + if self.appState.inferno == None: + inferno_fullpath = pathlib.Path(__file__).parent.joinpath("help/inferno.dur") + self.appState.inferno, self.appState.inferno_opts = self.appState.loadDurFileToMov(inferno_fullpath) + self.enterViewMode(mov=self.appState.inferno, opts=self.appState.inferno_opts) + #self.notify("heat code activated") + def hardRefresh(self): #self.stdscr.clear() self.stdscr.redrawwin() @@ -4990,14 +5538,14 @@ def refresh(self, refreshScreen=True): # rename to redraw()? else: mov = self.mov # Figure out the last line to draw - lastLineToDraw = topLine + self.realmaxY - 2 # right above the status line + lastLineToDraw = topLine + self.appState.realmaxY - 2 # right above the status line if self.appState.playOnlyMode: lastLineToDraw += 2 if lastLineToDraw > mov.sizeY: lastLineToDraw = mov.sizeY screenLineNum = 0 - firstCol = 0 - lastCol = min(mov.sizeX, self.appState.realmaxX) + firstCol = self.appState.firstCol + lastCol = min(mov.sizeX, self.appState.realmaxX + firstCol) # Draw each character for linenum in range(topLine, lastLineToDraw): line = mov.currentFrame.content[linenum] @@ -5009,10 +5557,10 @@ def refresh(self, refreshScreen=True): # rename to redraw()? # draw brush preview # If we're drawing within the brush area: if linenum in range(self.appState.mouse_line + self.appState.topLine, self.appState.mouse_line + self.appState.brush.sizeX + self.appState.topLine): - if colnum in range(self.appState.mouse_col, self.appState.mouse_col + self.appState.brush.sizeY): + if colnum in range(self.appState.mouse_col + self.appState.firstCol, self.appState.mouse_col + self.appState.brush.sizeY + self.appState.firstCol): #brush_line = linenum - self.appState.mouse_line brush_line = linenum - self.appState.mouse_line - self.appState.topLine - brush_col = colnum - self.appState.mouse_col + brush_col = colnum - self.appState.mouse_col - self.appState.firstCol try: brushChar = self.appState.brush.content[brush_col][brush_line] except IndexError: @@ -5027,7 +5575,7 @@ def refresh(self, refreshScreen=True): # rename to redraw()? if self.appState.renderMouseCursor: charContent = brushChar charColor = self.appState.brush.newColorMap[brush_col][brush_line] - if linenum == self.appState.mouse_line + self.appState.topLine and colnum == self.appState.mouse_col: + if linenum == self.appState.mouse_line + self.appState.topLine and colnum == self.appState.mouse_col + self.appState.firstCol: if self.appState.cursorMode == "Draw" and not self.playing and not self.appState.playingHelpScreen: # Drawing preview instead if self.appState.renderMouseCursor: charContent = self.appState.drawChar @@ -5037,12 +5585,62 @@ def refresh(self, refreshScreen=True): # rename to redraw()? cursesColorPair = self.ansi.colorPairMap[tuple(charColor)] except: # Or if we can't, fail to the terminal's default color cursesColorPair = 0 + injecting = False + if self.appState.can_inject and self.appState.colorMode == "256": + injecting = True if charColor[0] > 8 and charColor[0] <= 16 and self.appState.colorMode == "16": # bright color - self.addstr(screenLineNum, colnum, charContent, curses.color_pair(cursesColorPair) | curses.A_BOLD) + self.addstr(screenLineNum, colnum - self.appState.firstCol, charContent, curses.color_pair(cursesColorPair) | curses.A_BOLD) elif charColor[0] > 7 and charColor[0] <= 15 and self.appState.colorMode == "256": # bright color - self.addstr(screenLineNum, colnum, charContent, curses.color_pair(cursesColorPair) | curses.A_BOLD) - else: - self.addstr(screenLineNum, colnum, charContent, curses.color_pair(cursesColorPair)) + # Let's try injecting! + if injecting: + moveCode = f"\x1b[{screenLineNum + 1};{colnum - self.appState.firstCol + 1}H" + colorCode = self.ansi.getColorCode256(charColor[0],charColor[1]) + sys.stdout.write(moveCode) + sys.stdout.write(colorCode) + sys.stdout.write(charContent) + pass + #self.addstr(screenLineNum, colnum - self.appState.firstCol, charContent) + else: + self.addstr(screenLineNum, colnum - self.appState.firstCol, charContent, curses.color_pair(cursesColorPair) | curses.A_BOLD) + # If the mouse cursor is over Fg: 1 Bg:1 in 16 color mode, aka Black on Black + # then print with defualt charaacters instead. This should prevent the cursor from + # disappearing, as well as let you preview "invisible" text under the cursor. + elif not self.appState.playOnlyMode and colnum + 1 == self.xy[1] and linenum == self.xy[0]: # under the cursor + if self.appState.colorMode == "16": + visible_color_pair = self.ansi.colorPairMap[(self.appState.defaultFgColor, self.appState.defaultBgColor)] + #self.addstr(screenLineNum, colnum, "X", visible_color_pair) + # black on black + if charColor[0] == 1 and charColor[1] == 0 or \ + charColor[0] == 1 and charColor[1] == 8: + # make it show + self.addstr(screenLineNum, colnum, charContent, visible_color_pair) + # not black on black + else: # 16 color Normal character, under the cursor. No funny business. Print to the screen + self.addstr(screenLineNum, colnum - self.appState.firstCol, charContent, curses.color_pair(cursesColorPair)) + else: # 256 color Normal character, under the cursor. No funny business. Print to the screen + self.addstr(screenLineNum, colnum - self.appState.firstCol, charContent, curses.color_pair(cursesColorPair)) + else: # Normal character. No funny business. Print to the screen + #injecting = True + if injecting and charColor[1] != 0: + #self.stdscr.move(screenLineNum, colnum - self.appState.firstCol) + #self.stdscr.refresh() + moveCode = f"\x1b[{screenLineNum + 1};{colnum - self.appState.firstCol + 1}H" + colorCode = self.ansi.getColorCode256(charColor[0],charColor[1]) + #sys.stdout.write(f"\x1b[38:5:{charColor[0]}m") # FG + sys.stdout.write(moveCode) + sys.stdout.write(colorCode) + #sys.stdout.write(f"\x1b[48:5:{charColor[1]}m") # BG + sys.stdout.write(charContent) + #time.sleep(0.001) + #self.stdscr.refresh() + #self.notify("Injected", wait_time=40) + #sys.stdout.write("\x1b[48:5:46m") + #sys.stdout.write("\x1b[38:5:19m") + #self.addstr(screenLineNum, colnum - self.appState.firstCol, f"{charContent}") + pass + #self.addstr(screenLineNum, colnum - self.appState.firstCol, charContent) + else: + self.addstr(screenLineNum, colnum - self.appState.firstCol, charContent, curses.color_pair(cursesColorPair)) # draw border on right edge of line if self.appState.drawBorders and screenLineNum + self.appState.topLine < self.mov.sizeY: self.addstr(screenLineNum, mov.sizeX, ": ", curses.color_pair(self.appState.theme['borderColor'])) @@ -5051,10 +5649,12 @@ def refresh(self, refreshScreen=True): # rename to redraw()? #if self.appState.drawBorders and screenLineNum < self.realmaxY - 3 : if self.appState.drawBorders and screenLineNum + self.appState.topLine == self.mov.sizeY: if screenLineNum < self.statusBarLineNum: - self.addstr(screenLineNum, 0, "." * mov.sizeX, curses.color_pair(self.appState.theme['borderColor'])) + borderWidth = min(mov.sizeX, self.realmaxX) + self.addstr(screenLineNum, 0, "." * borderWidth, curses.color_pair(self.appState.theme['borderColor'])) self.addstr(screenLineNum, mov.sizeX, ": ", curses.color_pair(self.appState.theme['borderColor'])) screenLineNum += 1 - spaceMultiplier = mov.sizeX + 1 + #spaceMultiplier = mov.sizeX + 1 + spaceMultiplier = self.realmaxY for x in range(screenLineNum, self.realmaxY - 2): self.addstr(x, 0, " " * spaceMultiplier) curses.panel.update_panels() @@ -5065,11 +5665,14 @@ def refresh(self, refreshScreen=True): # rename to redraw()? def addColToCanvas(self): self.undo.push() # window is big enough + fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor for frameNum in range(0, len(self.mov.frames)): for x in range(len(self.mov.frames[frameNum].content)): self.mov.frames[frameNum].content[x].insert(self.xy[1] - 1, ' ') - self.mov.frames[frameNum].newColorMap[x].insert(self.xy[1] - 1, [1,0]) + self.mov.frames[frameNum].newColorMap[x].insert(self.xy[1] - 1, [fg,bg]) + self.mov.frames[frameNum].sizeX += 1 self.mov.sizeX += 1 + #self.mov.currentFrame.sizeX += 1 self.opts.sizeX += 1 self.hardRefresh() @@ -5080,7 +5683,9 @@ def delColFromCanvas(self): for x in range(len(self.mov.frames[frameNum].content)): # line self.mov.frames[frameNum].content[x].pop(self.xy[1] - 1) self.mov.frames[frameNum].newColorMap[x].pop(self.xy[1] - 1) + self.mov.frames[frameNum].sizeX -= 1 self.mov.sizeX -= 1 + #self.mov.currentFrame.sizeX -= 1 self.opts.sizeX -= 1 self.hardRefresh() if self.xy[1] == self.mov.sizeX + 1: @@ -5089,10 +5694,14 @@ def delColFromCanvas(self): def addLineToCanvas(self): self.undo.push() + fg = self.appState.defaultFgColor + bg = self.appState.defaultBgColor for frameNum in range(0, len(self.mov.frames)): self.mov.frames[frameNum].content.insert(self.xy[0] + 1, list(' ' * self.mov.sizeX)) - self.mov.frames[frameNum].newColorMap.insert(self.xy[0] + 1, [[1,0]] * self.mov.sizeX) + self.mov.frames[frameNum].newColorMap.insert(self.xy[0] + 1, [[fg,bg]] * self.mov.sizeX) + self.mov.frames[frameNum].sizeY += 1 self.mov.sizeY += 1 + #self.mov.currentFrame.sizeY += 1 self.opts.sizeY += 1 def delLineFromCanvas(self): @@ -5101,7 +5710,9 @@ def delLineFromCanvas(self): for frameNum in range(0, len(self.mov.frames)): self.mov.frames[frameNum].content.pop(self.xy[0]) self.mov.frames[frameNum].newColorMap.pop(self.xy[0]) + self.mov.frames[frameNum].sizeY -= 1 self.mov.sizeY -= 1 + #self.mov.currentFrame.sizeY -= 1 self.opts.sizeY -= 1 self.hardRefresh() if self.xy[0] == self.mov.sizeY: # We're on the last line, and just deleted it. @@ -5109,19 +5720,20 @@ def delLineFromCanvas(self): def addCol(self, frange=None): """Insert column at position of cursor""" + fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor self.undo.push() if frange: # framge range for frameNum in range(frange[0] - 1, frange[1]): for x in range(len(self.mov.frames[frameNum].content)): self.mov.frames[frameNum].content[x].insert(self.xy[1] - 1, ' ') self.mov.frames[frameNum].content[x].pop() - self.mov.frames[frameNum].newColorMap[x].insert(self.xy[1] - 1, [1,0]) + self.mov.frames[frameNum].newColorMap[x].insert(self.xy[1] - 1, [fg,bg]) self.mov.frames[frameNum].newColorMap[x].pop() else: for x in range(len(self.mov.currentFrame.content)): self.mov.currentFrame.content[x].insert(self.xy[1] - 1, ' ') self.mov.currentFrame.content[x].pop() - self.mov.currentFrame.newColorMap[x].insert(self.xy[1] - 1, [1,0]) + self.mov.currentFrame.newColorMap[x].insert(self.xy[1] - 1, [fg,bg]) self.mov.currentFrame.newColorMap[x].pop() # insert bit here to shift color map to the right from the column # onward. how: start at top right character, work down to bottom @@ -5132,24 +5744,26 @@ def addCol(self, frange=None): def delCol(self, frange=None): """Erase column at position of cursor""" self.undo.push() + fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor if frange: # framge range for frameNum in range(frange[0] - 1, frange[1]): for x in range(len(self.mov.frames[frameNum].content)): # Pop current column from every self.mov.frames[frameNum].content[x].pop(self.xy[1] - 1) # line & add a blank self.mov.frames[frameNum].content[x].append(' ') # at the end of each line. self.mov.frames[frameNum].newColorMap[x].pop(self.xy[1] - 1) # line & add a blank - self.mov.frames[frameNum].newColorMap[x].append([1,0]) # at the end of each line. + self.mov.frames[frameNum].newColorMap[x].append([fg,bg]) # at the end of each line. else: for x in range(len(self.mov.currentFrame.content)): # Pop current column from every self.mov.currentFrame.content[x].pop(self.xy[1] - 1) # line & add a blank self.mov.currentFrame.content[x].append(' ') # at the end of each line. self.mov.currentFrame.newColorMap[x].pop(self.xy[1] - 1) # line & add a blank - self.mov.currentFrame.newColorMap[x].append([1,0]) # at the end of each line. + self.mov.currentFrame.newColorMap[x].append([fg,bg]) # at the end of each line. self.hardRefresh() def delLine(self, frange=None): """delete current line""" self.undo.push() + fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor if frange: for frameNum in range(frange[0] - 1, frange[1]): self.mov.frames[frameNum].content.pop(self.xy[0]) @@ -5157,30 +5771,31 @@ def delLine(self, frange=None): 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 + self.mov.frames[frameNum].newColorMap[len(self.mov.frames[frameNum].newColorMap) - 1] = [[fg,bg]] * self.mov.sizeX else: self.mov.currentFrame.content.pop(self.xy[0]) self.mov.currentFrame.content.append([]) self.mov.currentFrame.content[len(self.mov.currentFrame.content) - 1] = list(' ' * self.mov.sizeX) self.mov.currentFrame.newColorMap.pop(self.xy[0]) self.mov.currentFrame.newColorMap.append([]) - self.mov.currentFrame.newColorMap[len(self.mov.currentFrame.newColorMap) - 1] = [[1,0]] * self.mov.sizeX + self.mov.currentFrame.newColorMap[len(self.mov.currentFrame.newColorMap) - 1] = [[fg,bg]] * self.mov.sizeX self.hardRefresh() def addLine(self, frange=None): """Insert new line""" + fg, bg = self.appState.defaultFgColor, self.appState.defaultBgColor self.undo.push() if frange: for frameNum in range(frange[0] - 1, frange[1]): self.mov.frames[frameNum].content.insert(self.xy[0], list(' ' * self.mov.sizeX)) self.mov.frames[frameNum].content.pop() - self.mov.frames[frameNum].newColorMap.insert(self.xy[0], [[1,0]] * self.mov.sizeX) + self.mov.frames[frameNum].newColorMap.insert(self.xy[0], [[fg,bg]] * self.mov.sizeX) self.mov.frames[frameNum].newColorMap.pop() self.refresh() else: self.mov.currentFrame.content.insert(self.xy[0], list(' ' * self.mov.sizeX)) self.mov.currentFrame.content.pop() - self.mov.currentFrame.newColorMap.insert(self.xy[0], [[1,0]] * self.mov.sizeX) + self.mov.currentFrame.newColorMap.insert(self.xy[0], [[fg,bg]] * self.mov.sizeX) self.mov.currentFrame.newColorMap.pop() self.refresh() @@ -5225,7 +5840,7 @@ def startSelecting(self, firstkey=None, mouse=False): # firstkey is the key th 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) + self.addstr(linenum - self.appState.topLine, colnum - self.appState.firstCol, mov.currentFrame.content[linenum][colnum], curses.color_pair(cursesColorPair) | curses.A_REVERSE) width = lastColNum - firstColNum + 1 height = lastLineNum - firstLineNum + 1 # end draw block area @@ -5277,8 +5892,14 @@ def startSelecting(self, firstkey=None, mouse=False): # firstkey is the key th # self.mov.currentFrame.flip_horizontal() if chr(prompt_ch) in ['t', 'T']: # Cut to clipboard self.clearStatusBar() - self.promptPrint("Cut across all frames in playback range (Y/N)? ") - askingAboutRange = True + if self.mov.hasMultipleFrames(): + self.promptPrint("Cut across all frames in playback range (Y/N)? ") + askingAboutRange = True + else: + self.copySegmentToClipboard([firstLineNum, firstColNum], height, width) + self.undo.push() + self.deleteSegment([firstLineNum, firstColNum], height, width) + askingAboutRange = False while askingAboutRange: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['y', 'Y']: # yes, all range @@ -5296,8 +5917,13 @@ def startSelecting(self, firstkey=None, mouse=False): # firstkey is the key th 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 + if self.mov.hasMultipleFrames(): + self.promptPrint("Delete across all frames in playback range (Y/N)? ") + askingAboutRange = True + else: + self.undo.push() + self.deleteSegment([firstLineNum, firstColNum], height, width) + askingAboutRange = False while askingAboutRange: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['y', 'Y']: # yes, all range @@ -5313,8 +5939,13 @@ def startSelecting(self, firstkey=None, mouse=False): # firstkey is the key th 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 + if self.mov.hasMultipleFrames(): + self.promptPrint("Color across all frames in playback range (Y/N)? ") + askingAboutRange = True + else: + self.undo.push() + self.colorSegment([firstLineNum, firstColNum], height, width) + askingAboutRange = False while askingAboutRange: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['y', 'Y']: # yes, all range @@ -5366,8 +5997,13 @@ def startSelecting(self, firstkey=None, mouse=False): # firstkey is the key th prompting = False else: self.clearStatusBar() - self.promptPrint("Fill across all frames in playback range (Y/N)? ") - askingAboutRange = True + if self.mov.hasMultipleFrames(): + self.promptPrint("Fill across all frames in playback range (Y/N)? ") + askingAboutRange = True + else: # Just one frame, so don't worry about the range. + self.undo.push() + self.fillSegment([firstLineNum, firstColNum], height, width, fillChar=drawChar) + askingAboutRange = False while askingAboutRange: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['y', 'Y']: # yes, all range @@ -5401,6 +6037,14 @@ def startSelecting(self, firstkey=None, mouse=False): # firstkey is the key th if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 + try: + curses.BUTTON5_PRESSED + except: + curses.BUTTON5_PRESSED = 0 + try: + curses.BUTTON4_PRESSED + except: + curses.BUTTON4_PRESSED = 0 if mouseState & curses.BUTTON4_PRESSED: # wheel up if mouseY < self.mov.sizeY and mouseX < self.mov.sizeX \ and mouseY + self.appState.topLine < self.appState.topLine + self.statusBarLineNum: @@ -5415,7 +6059,7 @@ def startSelecting(self, firstkey=None, mouse=False): # firstkey is the key th 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[1] = mouseX + 1 + self.appState.firstCol # set cursor position self.xy[0] = mouseY + self.appState.topLine @@ -5428,8 +6072,13 @@ def startSelecting(self, firstkey=None, mouse=False): # firstkey is the key th def askHowToPaste(self): self.clearStatusBar() - self.promptPrint("Paste across all frames in playback range (Y/N)? ") - askingAboutRange = True + if self.mov.hasMultipleFrames(): + self.promptPrint("Paste across all frames in playback range (Y/N)? ") + askingAboutRange = True + else: # only one frame + self.undo.push() + self.pasteFromClipboard() + askingAboutRange = False while askingAboutRange: prompt_ch = self.stdscr.getch() if chr(prompt_ch) in ['y', 'Y']: # yes, all range @@ -5571,8 +6220,8 @@ def deleteSegment(self, startPoint, height, width, frange=None): charColumn = startPoint[1] + colNum charLine = startPoint[0] + lineNum character = ord(" ") - charFg = 1 - charBg = 0 + charFg = self.appState.defaultFgColor + charBg = self.appState.defaultBgColor if charColumn < self.mov.sizeX + 1 and charLine < self.mov.sizeY: if not frange: self.insertChar(character, fg=charFg, bg=charBg, x=charColumn, y=charLine, pushUndo=False) diff --git a/durdraw/durdraw_ui_widgets.py b/durdraw/durdraw_ui_widgets.py index c81ec7a..f79796a 100644 --- a/durdraw/durdraw_ui_widgets.py +++ b/durdraw/durdraw_ui_widgets.py @@ -278,7 +278,7 @@ def show(self): #self.showFgPicker() self.handler.show() - def showFgPicker(self): + def showFgPicker(self, message=None): """ Returns the color picked by the user """ # Draw foreground colors 1-5 in a panel. @@ -286,7 +286,7 @@ def showFgPicker(self): # to select color (arrows and return). Then return the # selected color. self.hidden = False - color = self.handler.showFgPicker() + color = self.handler.showFgPicker(message=message) if not self.caller.appState.sideBarShowing: self.hide() return color @@ -429,7 +429,16 @@ def __init__(self, caller, x=0, y=0, appState=None): settingsMenu.add_item("VGA Colors", caller.enableTrueCGAColors, "v") settingsMenu.add_item("ZX Spectrum Colors", caller.enableTrueSpeccyColors, "z") settingsMenu.add_item("C64 Colors", caller.enableTrueC64Colors, "c") - settingsMenu.add_item("Deafult Colors", caller.resetColorsToDefault, "d") + #settingsMenu.add_item("Deafult Colors", caller.resetColorsToDefault, "d") + settingsMenu.add_item("Toggle Mouse", caller.toggleMouse, "m") + settingsMenu.add_item("Toggle Color Scroll", caller.toggleColorScrolling, "s") + settingsMenu.add_item("Toggle Wide Wrapping", caller.toggleWideWrapping, "w") + if self.appState.mental: # Experimental stuff + settingsMenu.add_item("Toggle iCE Colors (MENTAL)", caller.toggleIceColors, "i") + settingsMenu.add_item("Toggle Injecting (MENTAL)", caller.toggleInjecting, "j") + if self.appState.debug: + settingsMenu.add_item("Toggle Debug", caller.toggleDebug, "d") + settingsMenu.add_item("Python Console", caller.jumpToPythonConsole, "p") settingsMenu.is_submenu = True #settingsMenu.add_item("Show/Hide Sidebar", caller.toggleSideBar, "s") settingsMenu.set_x(self.x - 1) @@ -443,6 +452,7 @@ def __init__(self, caller, x=0, y=0, appState=None): transformMenu.add_item("Bounce", caller.transform_bounce, "b") transformMenu.add_item("Repeat", caller.transform_repeat, "r") transformMenu.add_item("Reverse", caller.transform_reverse, "v") + transformMenu.add_item("Apply NeoFetch Keys", caller.apply_neofetch_keys, "n") #transformMenu.add_item("Show/Hide Sidebar", caller.toggleSideBar, "s") transformMenu.set_x(self.x - 1) transformMenu.set_y(transformMenuColumn) @@ -468,6 +478,7 @@ def __init__(self, caller, x=0, y=0, appState=None): mainMenu.add_item("Color Picker", caller.selectColorPicker, "l", shortcut="tab") mainMenu.add_item("Viewer Mode", caller.enterViewMode, "v", shortcut="esc-V") mainMenu.add_item("Find /", caller.searchForStringPrompt, "/", shortcut="esc-F") + mainMenu.add_item("Replace Color", caller.replaceColorUnderCursor, "e", shortcut="esc-L") mainMenu.add_item("Settings", caller.openSettingsMenu, "t", has_submenu=True) mainMenu.add_item("Help", caller.showHelp, "h", shortcut="esc-h") mainMenu.add_item("Quit", caller.safeQuit, "q", shortcut="esc-q") diff --git a/durdraw/durdraw_ui_widgets_curses.py b/durdraw/durdraw_ui_widgets_curses.py index 2a0d846..7730d85 100644 --- a/durdraw/durdraw_ui_widgets_curses.py +++ b/durdraw/durdraw_ui_widgets_curses.py @@ -136,7 +136,10 @@ def show(self): self.panel.move(self.menuOriginLine, self.menu.y) self.panel.show() except: # The window was probably too short, so panel.move() returns ERR. - return False + curses_cursorOn() + self.menu.hide() + response = "Close" # default thing to do when done, returned to menu wrapper + return response self.curses_win.keypad(True) curses.panel.update_panels() curses.doupdate() @@ -333,7 +336,8 @@ def pickChar(self): prompting = True curses.flushinp() while prompting: - c = self.window.getch() + #c = self.window.getch() + c = self.window.get_wch() time.sleep(0.01) if c in [curses.KEY_F1]: self.caller.appState.drawChar = chr(self.caller.caller.caller.chMap['f1']) @@ -367,7 +371,11 @@ def pickChar(self): prompting = False elif c in [27, 13, curses.KEY_ENTER]: # 27 = esc, 13 = enter, cancel prompting = False - else: + elif type(c) == str: # Is a printable/unicode character + if c.isprintable(): + self.caller.appState.drawChar = c + prompting = False + else: # is an integer, but probably still a printable character try: if chr(c).isprintable(): newChar = chr(c) @@ -523,7 +531,7 @@ def updateFgPicker(self): bg += 1 if bg == 8: bg = 0 - if fg == self.colorPicker.caller.colorfg or fg == 0 and colorbg == 8: + if fg == self.colorPicker.caller.colorfg or fg == 0 and bg == 8: if fg == 1: # black if self.appState.colorPickerSelected: if fg == bg: @@ -543,7 +551,7 @@ def updateFgPicker(self): else: curses_addstr(self.window, line, col, 'F', color_pair | curses.A_UNDERLINE | curses.A_BOLD) if self.colorMode == "16": - if fg > 8: + if fg > 8 and self.appState.iceColors == False: curses_addstr(self.window, line, col, 'F', color_pair | curses.A_UNDERLINE | curses.A_BOLD) else: curses_addstr(self.window, line, col, 'F', color_pair | curses.A_UNDERLINE ) @@ -555,12 +563,12 @@ def updateFgPicker(self): curses_addstr(self.window, line, col, 'F', color_pair | curses.A_BOLD) if self.colorMode == "16": if fg == bg: - if fg > 8: + if fg > 8 and self.appState.iceColors == False: curses_addstr(self.window, line, col, 'X', color_pair | curses.A_BOLD) else: curses_addstr(self.window, line, col, 'X', color_pair) else: - if fg > 8: + if fg > 8 and self.appState.iceColors == False: curses_addstr(self.window, line, col, 'F', color_pair | curses.A_BOLD) else: curses_addstr(self.window, line, col, 'F', color_pair) @@ -574,7 +582,7 @@ def updateFgPicker(self): # 16 color, black background (8), showing color 8 (grey) elif self.colorMode == "16" and fg == 9 and bg == 9: # draw fill character unmodified - if fg > 8: + if fg > 8 and self.appState.iceColors == False: curses_addstr(self.window, line, col, self.fillChar, color_pair | curses.A_BOLD) else: curses_addstr(self.window, line, col, self.fillChar, color_pair) @@ -607,7 +615,7 @@ def updateFgPicker(self): curses_addstr(self.window, line, col, self.fillChar, color_pair) elif self.colorMode == "16": # draw bright colors in 16 color mode - if fg > 8: + if fg > 8 and self.appState.iceColors == False: curses_addstr(self.window, line, col, self.fillChar, color_pair | curses.A_BOLD) #debug_string = str(color_pair) #self.colorPicker.caller.notify(debug_string) @@ -622,9 +630,9 @@ def updateFgPicker(self): 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): + def showFgPicker(self, message=None): #self.colorPicker.caller.notify(f"showFgPicker") - self.showColorPicker(type="fg") + self.showColorPicker(type="fg", message=message) def move_up_256(self): if self.colorMode == "256": @@ -650,7 +658,7 @@ def move_down_256(self): color = self.totalColors self.colorPicker.caller.setFgColor(color) - def showColorPicker(self, type="fg"): + def showColorPicker(self, type="fg", message=None): #self.colorPicker.caller.notify(f"showColorPicker") """ Shows picker, has UI loop for letting user pick color with keyboard or mouse """ if type == "fg": @@ -688,7 +696,7 @@ def showColorPicker(self, type="fg"): #self.colorPicker.caller.drawStatusBar() self.update() c = self.window.getch() - if c in [98, curses.KEY_LEFT]: + if c in [98, curses.KEY_LEFT, ord('h')]: if color == 0: color = self.totalColors else: @@ -697,7 +705,7 @@ def showColorPicker(self, type="fg"): self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() - elif c in [102, curses.KEY_RIGHT]: + elif c in [102, curses.KEY_RIGHT, ord('l')]: color += 1 #if color >= curses.COLORS: if color > self.totalColors: @@ -706,7 +714,7 @@ def showColorPicker(self, type="fg"): self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() - elif c == curses.KEY_UP: + elif c in [curses.KEY_UP, ord('k')]: if self.colorMode == "256": if color < 32: color -= 16 @@ -722,7 +730,7 @@ def showColorPicker(self, type="fg"): self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() - elif c == curses.KEY_DOWN: + elif c in [curses.KEY_DOWN, ord('j')]: if self.colorMode == "256": if color < 16: color += 16 @@ -740,7 +748,7 @@ def showColorPicker(self, type="fg"): color = 1 self.colorPicker.caller.setFgColor(color) self.updateFgPicker() - self.colorPicker.caller.drawStatusBar() + elf.colorPicker.caller.drawStatusBar() elif c == curses.KEY_END: #color = 255 color = self.totalColors @@ -771,20 +779,24 @@ def showColorPicker(self, type="fg"): if not self.appState.hasMouseScroll: curses.BUTTON5_PRESSED = 0 curses.BUTTON4_PRESSED = 0 - if mouseState & curses.BUTTON4_PRESSED: # wheel up + if mouseState & curses.BUTTON4_PRESSED: # wheel down? color += 1 - if color >= curses.COLORS: - color = 0 - #self.colorPicker.caller.colorfg = color + if color > appState.totalFgColors: + if appState.colorMode == "16": + color = 1 + elif appState.colorMode == "256": + color = 0 self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() - elif mouseState & curses.BUTTON5_PRESSED: # wheel down - if color == 0: - color = curses.COLORS - 1 - else: - color -= 1 - #self.colorPicker.caller.colorfg = color + elif mouseState & curses.BUTTON5_PRESSED: # wheel up? + color -= 1 + if appState.colorMode == "16": + if color <= 0: + color = appState.totalFgColors + elif appState.colorMode == "256": + if color < 0: + color = appState.totalFgColors self.colorPicker.caller.setFgColor(color) self.updateFgPicker() self.colorPicker.caller.drawStatusBar() @@ -794,14 +806,6 @@ def showColorPicker(self, type="fg"): self.gotClick(mouseX, mouseY) prompting = False - #clickedCol = mouseX - self.x - #clickedLine = mouseY - self.origin - #if mouseY < self.origin + self.height: - # if self.colorGrid[clickedLine][clickedCol] != 0: - # # We're in the grid. Set color - # color = self.colorGrid[clickedLine][clickedCol] - # self.colorPicker.caller.setFgColor(color) - # self.updateFgPicker() elif mouseY < mov.sizeY and mouseX < mov.sizeX \ and mouseY + appState.topLine < appState.topLine + self.colorPicker.caller.statusBarLineNum: # we did click in the canvas. @@ -825,6 +829,9 @@ def showColorPicker(self, type="fg"): #self.hide() prompting = False + # Show message, eg: "pick new color" + #curses_addstr(self.window, self.appState.realmaxX - 2, 0, message, curses.color_pair(self.appState.theme['notificationColor'])) + if not self.appState.colorPickerSelected: if self.appState.sideBarShowing: self.hideBorder() @@ -850,10 +857,22 @@ def gotClick(self, mouseX, mouseY): 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() + if self.appState.colorMode == "16": + if self.colorGrid[clickedLine][clickedCol] != 0: + color = self.colorGrid[clickedLine][clickedCol] + self.colorPicker.caller.setFgColor(color) + self.updateFgPicker() + elif self.appState.colorMode == "256": + if self.colorGrid[clickedLine][clickedCol] != 0: + color = self.colorGrid[clickedLine][clickedCol] + self.colorPicker.caller.setFgColor(color) + self.updateFgPicker() + if self.colorGrid[clickedLine][clickedCol] == 0: + if clickedLine == 0 and clickedCol == 0: # true black, not a fake black + color = self.colorGrid[clickedLine][clickedCol] + self.colorPicker.caller.setFgColor(color) + self.updateFgPicker() + def gotDoubleClick(self, mouseX, mouseY): # set a BG color @@ -930,6 +949,7 @@ def __init__(self, button, window, on_click, appState=None): def draw(self, plusX=0, plusY=9): if not self.button.invisible: + self.color = curses.color_pair(self.appState.theme['clickColor']) | curses.A_BOLD # reload color from theme textColor = self.color if self.button.selected: #curses_notify(self.window, "Tacos") diff --git a/durdraw/durf/bsd.durf b/durdraw/durf/bsd.durf new file mode 100644 index 0000000..a11c9bd Binary files /dev/null and b/durdraw/durf/bsd.durf differ diff --git a/durdraw/durf/cm-eye.durf b/durdraw/durf/cm-eye.durf new file mode 100644 index 0000000..84af3d7 Binary files /dev/null and b/durdraw/durf/cm-eye.durf differ diff --git a/durdraw/durf/linux-fire.durf b/durdraw/durf/linux-fire.durf new file mode 100644 index 0000000..18c3754 Binary files /dev/null and b/durdraw/durf/linux-fire.durf differ diff --git a/durdraw/durf/linux-tux.durf b/durdraw/durf/linux-tux.durf new file mode 100644 index 0000000..52b3c85 Binary files /dev/null and b/durdraw/durf/linux-tux.durf differ diff --git a/durdraw/durf/unixbox.durf b/durdraw/durf/unixbox.durf new file mode 100644 index 0000000..a68dfe1 Binary files /dev/null and b/durdraw/durf/unixbox.durf differ diff --git a/durdraw/durfetch.py b/durdraw/durfetch.py new file mode 100644 index 0000000..2e3d018 --- /dev/null +++ b/durdraw/durfetch.py @@ -0,0 +1,108 @@ +# Front-end to Durdraw and Neofetch + +import argparse +import glob +import os +import pathlib +import random + +import durdraw.main as durdraw_main +import durdraw.neofetcher as neofetcher + + +def all_durf_files(): + return all_internal_durf_files() + +def all_internal_durf_files(): + # Return a list of all files from every location everywhere, throughout the universe + # .. or at least the fetch animations built-in to durdraw (in the durdraw/durf/ path). + internal_durf_path = pathlib.Path(__file__).parent.joinpath("durf/") + internal_durf_files = glob.glob(f"{internal_durf_path}/*.durf") + all_files = internal_durf_files + #all_files = ['bsd.durf', 'linux-tux.durf', 'linux-fire.durf', 'unixbox.durf', 'cm-eye.durf'] + return all_files + +def get_internal_durf_path(): + return str(pathlib.Path(__file__).parent.joinpath("durf/")) + +def remove_suffix(text, suffix): + # For compatibility with older Python versions, older than 3.8. + # thanks, David Foster @ https://stackoverflow.com/questions/1038824/how-do-i-remove-a-substring-from-the-end-of-a-string-remove-a-suffix-of-the-str + return text[:-len(suffix)] if text.endswith(suffix) and len(suffix) != 0 else text + +def make_epilog(): + text = "Available animations for -l:\n\n" + internal_file_list = [] + for filename in all_internal_durf_files(): + #basename = os.path.basename(filename) + #print(basename) + #name = os.path.basename(filename).removesuffix('.durf') + name = remove_suffix(os.path.basename(filename), '.durf') + internal_file_list.append(name) + internal_file_list.sort() + for name in internal_file_list: + text += f"{name}\n" + text += "\n" + text += "\n" + return text + +def auto_load_file(neofetch_data, rand=False, fake_os=None): + files = [] + if fake_os: + os_name = fake_os + else: + os_name = neofetch_data['OS'].lower() + if rand: + files = all_durf_files() + return random.choice(files) + #if 'bsd' in neofetch_data['OS'].lower(): + bsd_list = ['bsd', 'macos'] + linux_list = ['linux'] + # Match BSD OSs + if any(substring in os_name for substring in bsd_list): + files = ['bsd.durf'] + # list of BSD + else: + files = ['linux-fire.durf', 'linux-tux.durf'] + return random.choice(files) + +def main(): + + epilog_text = make_epilog() + + #print(epilog_text) + + parser = argparse.ArgumentParser(description="An animated fetcher. A front-end for Durdraw and Neofetch integration.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=epilog_text) + parser.add_argument("filename", nargs='*', help=".durf ASCII and ANSI art file or files to use") + #parser.add_argument("--list", help="List available Durfetch screens", action=store_true) + parser_source_mutex = parser.add_mutually_exclusive_group() + parser_source_mutex.add_argument("-r", "--rand", help="Pick a random animation to play", action="store_true") + parser_source_mutex.add_argument("-l", "--load", help="Load an internal animation", nargs=1, type=str) + parser_fake_os_mutex = parser.add_mutually_exclusive_group() + parser_fake_os_mutex.add_argument("--linux", help="Show a Linux animation", action="store_true") + parser_fake_os_mutex.add_argument("--bsd", help="Show a BSD animation", action="store_true") + #parser.add_argument("-l", nargs="?", default="list") + args = parser.parse_args() + neofetch_data = neofetcher.run() + #print(args.filename, args.list, args.l, neofetch_data) + #if args.filename == None: # no file name passed, so pick an appropriate one. + faked = None + if args.linux: + faked = "linux" + if args.bsd: + faked = "bsd" + if args.load: + filename = [get_internal_durf_path() + "/" + args.load[0] + ".durf"] + if not os.path.isfile(filename[0]): + print(f"Error: Could not find an animation by that name at {filename[0]}") + exit(1) + elif args.filename == []: # no file name passed, so pick an appropriate one. + filename = [get_internal_durf_path() + "/" + auto_load_file(neofetch_data, rand=args.rand, fake_os=faked)] + else: + filename = args.filename + #print(filename) + + durdraw_args = ["--fetch", "--play"] + filename # filename is alist + durdraw_main.main(fetch_args=durdraw_args) + + diff --git a/durdraw/help/durhelp-16-long.dur b/durdraw/help/durhelp-16-long.dur index 9dea4a8..f363a48 100644 Binary files a/durdraw/help/durhelp-16-long.dur and b/durdraw/help/durhelp-16-long.dur differ diff --git a/durdraw/help/durhelp-256-long.dur b/durdraw/help/durhelp-256-long.dur index abc2f72..528534d 100644 Binary files a/durdraw/help/durhelp-256-long.dur and b/durdraw/help/durhelp-256-long.dur differ diff --git a/durdraw/help/inferno.dur b/durdraw/help/inferno.dur new file mode 100644 index 0000000..e2da75a Binary files /dev/null and b/durdraw/help/inferno.dur differ diff --git a/durdraw/main.py b/durdraw/main.py index 3a7a347..5d4dafb 100644 --- a/durdraw/main.py +++ b/durdraw/main.py @@ -13,6 +13,7 @@ from durdraw.durdraw_ui_curses import UserInterface as UI_Curses from durdraw.durdraw_options import Options import durdraw.help +import durdraw.neofetcher as neofetcher class ArgumentChecker: """ Place to hold any methods for verifying CLI arguments, beyond @@ -25,8 +26,8 @@ def undosize(size_s): else: raise argparse.ArgumentTypeError("Undo size must be between 1 and 1000.") -def main(): - DUR_VER = '0.27.1' +def main(fetch_args=None): + DUR_VER = '0.28.0' DUR_FILE_VER = 7 DEBUG_MODE = False # debug = makes debug_write available, sends verbose notifications durlogo = 'Durdraw' @@ -36,14 +37,9 @@ def main(): 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", + parserFilenameMutex.add_argument("-p", "--play", help="Just play .dur, .ANS or .ASC file or files, then exit", nargs='+') parser.add_argument("-d", "--delayexit", help="Wait X seconds after playback before exiting (requires -p)", nargs=1, type=float) - #parserStartScreenMutex.add_argument("-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') @@ -52,6 +48,7 @@ def main(): parser.add_argument("-W", "--width", help="Set canvas width", nargs=1, type=int) parser.add_argument("-H", "--height", help="Set canvas height", nargs=1, type=int) parser.add_argument("-m", "--max", help="Maximum canvas size for terminal (overrides -W and -H)", action="store_true") + parser.add_argument("--wrap", help="Number of columns to wrap lines at when loading ASCII and ANSI files (default 80)", nargs=1, type=int) parser.add_argument("--nomouse", help="Disable mouse support", action="store_true") parser.add_argument("--cursor", help="Cursor mode (block, underscore, or pipe)", nargs=1) @@ -62,48 +59,46 @@ def main(): parser.add_argument("--cp437", help="Display extended characters on the screen using Code Page 437 (IBM-PC/MS-DOS) encoding instead of Utf-8. (Requires CP437 capable terminal and font) (beta)", action="store_true") parser.add_argument("--export-ansi", action="store_true", help="Export loaded art to an .ansi file and exit") parser.add_argument("-u", "--undosize", help="Set the number of undo history states - default is 100. More requires more RAM, less saves RAM.", nargs=1, type=int) + #--mental -- Enable experimental (not ready for prime time) options + parser.add_argument("--mental", action="store_true", help=argparse.SUPPRESS) + parser.add_argument("--fetch", help="Replace fetch strings with Neofetch output", action="store_true") parser.add_argument("-V", "--version", help="Show version number and exit", action="store_true") parser.add_argument("--debug", action="store_true", help=argparse.SUPPRESS) - args = parser.parse_args() + + args = parser.parse_args(fetch_args) + if args.version: print(DUR_VER) exit(0) if args.times and not args.play: print("-x option requires -p") exit(1) + + # Initialize application app = AppState() # to store run-time preferences from CLI, environment stuff, etc. app.setDurVer(DUR_VER) app.setDurFileVer(DUR_FILE_VER) app.setDebug(DEBUG_MODE) + term_size = os.get_terminal_size() + + # Parse general command-line paramaters if args.debug: app.setDebug(True) if args.undosize: app.undoHistorySize = int(args.undosize[0]) - 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 + app.maximize_canvas() + if args.wrap: + app.wrapWidth = args.wrap[0] + app.showStartupScreen=False + app.quickStart = True if args.nomouse: app.hasMouse = False if args.notheme: @@ -125,8 +120,28 @@ def main(): app.blackbg = False else: app.blackbg = True - # load configuration file and theme + # load configuration file if app.loadConfigFile(): + # Load main optiona + if 'Main' in app.configFile: + mainConfig = app.configFile['Main'] + # load color mode + if 'color-mode' in mainConfig: + app.colorMode = mainConfig['color-mode'] + if 'disable-mouse' in mainConfig: + if mainConfig.getboolean('disable-mouse'): + app.hasMouse = False + if 'max-canvas' in mainConfig: + if mainConfig.getboolean('max-canvas'): + app.maximize_canvas() + if 'cursor-mode' in mainConfig: + app.screenCursorMode = mainConfig['cursor-mode'] + if 'scroll-colors' in mainConfig: + if mainConfig.getboolean('scroll-colors'): + app.scrollColors = True + if 'mental-mode' in mainConfig: + app.mental = True + # load theme set in config fileFalse if app.colorMode == "256": app.loadThemeFromConfig("Theme-256") else: @@ -147,23 +162,23 @@ def main(): # Load help file - first look for resource path, eg: python module dir durhelp_fullpath = '' - durhelp_fullpath = pathlib.Path(__file__).parent.joinpath("help/durhelp.dur") + #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") + #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") + #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.durhelp256_page2_fullpath = durhelp256_page2_fullpath app.durhelp16_fullpath = durhelp16_fullpath - app.durhelp16_page2_fullpath = durhelp16_page2_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) + #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) @@ -194,10 +209,7 @@ def main(): 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)) + print("Canvas size: %i columns, %i lines" % (app.width, app.height)) if args.wait: try: choice = input("Press Enter to Continue...") @@ -210,6 +222,17 @@ def main(): if args.play: app.playOnlyMode = True app.editorRunning = False + + if args.mental: + # Enable exprimental options + app.mental = True + + if args.fetch: + #app.playOnlyMode = True + app.fetchMode = True + app.fetchData = neofetcher.run() + #app.editorRunning = False + #app.drawBorders = False #ui = curses.wrapper(UI_Curses, app) ui = UI_Curses(app) if app.hasMouse: @@ -218,7 +241,7 @@ def main(): ui.enableTransBackground() if args.filename: ui.loadFromFile(args.filename, 'dur') - if args.play: + if args.play or args.fetch: # Just play files and exit app.drawBorders = False if args.times: @@ -226,6 +249,8 @@ def main(): for movie in args.play: ui.stdscr.clear() ui.loadFromFile(movie, 'dur') + if app.fetchMode: + ui.replace_neofetch_keys() ui.startPlaying() if args.delayexit: time.sleep(args.delayexit[0]) diff --git a/durdraw/neofetcher.py b/durdraw/neofetcher.py new file mode 100644 index 0000000..948b987 --- /dev/null +++ b/durdraw/neofetcher.py @@ -0,0 +1,57 @@ +# Run Neofetch. Extract data from it. Return a dict containing neofetch data. + +import pathlib +import subprocess + +neo_keys = ['OS', 'Host', 'Kernel', 'Uptime', 'Packages', 'Shell', 'Resolution', 'DE', 'WM', 'WM Theme', 'Terminal', 'Terminal Font', 'CPU', 'GPU', 'Memory'] + + + +def isAppAvail(name): # looks for program 'name' in path + try: + devnull = open(os.devnull) + subprocess.Popen([name], stdout=devnull, stderr=devnull).communicate() + except OSError as e: + #if e.errno == os.errno.ENOENT: + # return False + return False + return True + +def fetcher_available_old(): # old because it looks internally the local path instead of global path + """ Returne True if Neofetch was able to run successfully. """ + try: + neofetch_path = pathlib.Path(__file__).parent.joinpath("neofetch/neofetch") + subprocess.run([neofetch_path, '--stdout']) + return True + except: + return False + +def fetcher_available(): + return isAppAvail("neofetch") + +def run(): + # make an empty dict of Neofetch keys for us to populate and return + neofetch_results = {} + for key in neo_keys: + neofetch_results[key] = '' + # Run neofetch, capture the output + #neofetch_path = pathlib.Path(__file__).parent.joinpath("neofetch/neofetch") + neofetch_path = "neofetch" # obviously not a full path. just the executable run from the $PATH + neofetch_output = subprocess.check_output([neofetch_path, '--stdout']).decode() + neofetch_lines = neofetch_output.split('\n')[2:] + # Parse the neofetch output into neofetch_results{} + for line in neofetch_lines: + if line == '': + break + try: + key = line.split(': ')[0].strip() + value = line.split(': ')[1].strip() + except: + break + if key in neo_keys: + neofetch_results[key] = value + return neofetch_results + +if __name__ == "__main__": + results = run() + print(results) diff --git a/durfetch b/durfetch new file mode 100755 index 0000000..22062b3 --- /dev/null +++ b/durfetch @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +# Launches Durfetch + +from durdraw import durfetch +#from durdraw.durdraw_options import Options +#from durdraw.durdraw_movie import * + +if __name__ == "__main__": + #try: + durfetch.main() + #except Exception as e: + # print(f"Exception: {e}") + # print("Caught exception. Exiting.") + # exit(1) + diff --git a/examples/linux-fetch-colorful.dur b/examples/linux-fetch-colorful.dur new file mode 100644 index 0000000..09ebb9a Binary files /dev/null and b/examples/linux-fetch-colorful.dur differ diff --git a/examples/linux-fetch.dur b/examples/linux-fetch.dur new file mode 100644 index 0000000..a5cf7f4 Binary files /dev/null and b/examples/linux-fetch.dur differ diff --git a/setup.py b/setup.py index 5762099..6ede5af 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='durdraw', - version='0.27.1', + version='0.28.0', author='Sam Foster', author_email='samfoster@gmail.com', description='Animated Color ASCII and Unicode Art Editor', @@ -18,7 +18,7 @@ packages=['durdraw'], install_requires=['pillow', 'windows-curses;platform_system=="Windows"'], include_package_data = True, - package_data = {'durdraw': ['help/*', 'charsets/*', 'themes/*', 'plugins/*']}, + package_data = {'durdraw': ['help/*', 'charsets/*', 'themes/*', 'plugins/*', 'neofetch/*', 'durf/*']}, #package_data = {'durdraw': ["charsets/*"]}, data_files = [ # ('share/icons', ['data/durdraw.png']), @@ -49,6 +49,7 @@ entry_points={ 'console_scripts': [ 'durdraw = durdraw.main:main', + 'durfetch = durdraw.durfetch:main', ], }, )