diff --git a/INSTALL b/INSTALL deleted file mode 100644 index 8865734f..00000000 --- a/INSTALL +++ /dev/null @@ -1,368 +0,0 @@ -Installation Instructions -************************* - - Copyright (C) 1994-1996, 1999-2002, 2004-2016 Free Software -Foundation, Inc. - - Copying and distribution of this file, with or without modification, -are permitted in any medium without royalty provided the copyright -notice and this notice are preserved. This file is offered as-is, -without warranty of any kind. - -Basic Installation -================== - - Briefly, the shell command './configure && make && make install' -should configure, build, and install this package. The following -more-detailed instructions are generic; see the 'README' file for -instructions specific to this package. Some packages provide this -'INSTALL' file but do not implement all of the features documented -below. The lack of an optional feature in a given package is not -necessarily a bug. More recommendations for GNU packages can be found -in *note Makefile Conventions: (standards)Makefile Conventions. - - The 'configure' shell script attempts to guess correct values for -various system-dependent variables used during compilation. It uses -those values to create a 'Makefile' in each directory of the package. -It may also create one or more '.h' files containing system-dependent -definitions. Finally, it creates a shell script 'config.status' that -you can run in the future to recreate the current configuration, and a -file 'config.log' containing compiler output (useful mainly for -debugging 'configure'). - - It can also use an optional file (typically called 'config.cache' and -enabled with '--cache-file=config.cache' or simply '-C') that saves the -results of its tests to speed up reconfiguring. Caching is disabled by -default to prevent problems with accidental use of stale cache files. - - If you need to do unusual things to compile the package, please try -to figure out how 'configure' could check whether to do them, and mail -diffs or instructions to the address given in the 'README' so they can -be considered for the next release. If you are using the cache, and at -some point 'config.cache' contains results you don't want to keep, you -may remove or edit it. - - The file 'configure.ac' (or 'configure.in') is used to create -'configure' by a program called 'autoconf'. You need 'configure.ac' if -you want to change it or regenerate 'configure' using a newer version of -'autoconf'. - - The simplest way to compile this package is: - - 1. 'cd' to the directory containing the package's source code and type - './configure' to configure the package for your system. - - Running 'configure' might take a while. While running, it prints - some messages telling which features it is checking for. - - 2. Type 'make' to compile the package. - - 3. Optionally, type 'make check' to run any self-tests that come with - the package, generally using the just-built uninstalled binaries. - - 4. Type 'make install' to install the programs and any data files and - documentation. When installing into a prefix owned by root, it is - recommended that the package be configured and built as a regular - user, and only the 'make install' phase executed with root - privileges. - - 5. Optionally, type 'make installcheck' to repeat any self-tests, but - this time using the binaries in their final installed location. - This target does not install anything. Running this target as a - regular user, particularly if the prior 'make install' required - root privileges, verifies that the installation completed - correctly. - - 6. You can remove the program binaries and object files from the - source code directory by typing 'make clean'. To also remove the - files that 'configure' created (so you can compile the package for - a different kind of computer), type 'make distclean'. There is - also a 'make maintainer-clean' target, but that is intended mainly - for the package's developers. If you use it, you may have to get - all sorts of other programs in order to regenerate files that came - with the distribution. - - 7. Often, you can also type 'make uninstall' to remove the installed - files again. In practice, not all packages have tested that - uninstallation works correctly, even though it is required by the - GNU Coding Standards. - - 8. Some packages, particularly those that use Automake, provide 'make - distcheck', which can by used by developers to test that all other - targets like 'make install' and 'make uninstall' work correctly. - This target is generally not run by end users. - -Compilers and Options -===================== - - Some systems require unusual options for compilation or linking that -the 'configure' script does not know about. Run './configure --help' -for details on some of the pertinent environment variables. - - You can give 'configure' initial values for configuration parameters -by setting variables in the command line or in the environment. Here is -an example: - - ./configure CC=c99 CFLAGS=-g LIBS=-lposix - - *Note Defining Variables::, for more details. - -Compiling For Multiple Architectures -==================================== - - You can compile the package for more than one kind of computer at the -same time, by placing the object files for each architecture in their -own directory. To do this, you can use GNU 'make'. 'cd' to the -directory where you want the object files and executables to go and run -the 'configure' script. 'configure' automatically checks for the source -code in the directory that 'configure' is in and in '..'. This is known -as a "VPATH" build. - - With a non-GNU 'make', it is safer to compile the package for one -architecture at a time in the source code directory. After you have -installed the package for one architecture, use 'make distclean' before -reconfiguring for another architecture. - - On MacOS X 10.5 and later systems, you can create libraries and -executables that work on multiple system types--known as "fat" or -"universal" binaries--by specifying multiple '-arch' options to the -compiler but only a single '-arch' option to the preprocessor. Like -this: - - ./configure CC="gcc -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ - CXX="g++ -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ - CPP="gcc -E" CXXCPP="g++ -E" - - This is not guaranteed to produce working output in all cases, you -may have to build one architecture at a time and combine the results -using the 'lipo' tool if you have problems. - -Installation Names -================== - - By default, 'make install' installs the package's commands under -'/usr/local/bin', include files under '/usr/local/include', etc. You -can specify an installation prefix other than '/usr/local' by giving -'configure' the option '--prefix=PREFIX', where PREFIX must be an -absolute file name. - - You can specify separate installation prefixes for -architecture-specific files and architecture-independent files. If you -pass the option '--exec-prefix=PREFIX' to 'configure', the package uses -PREFIX as the prefix for installing programs and libraries. -Documentation and other data files still use the regular prefix. - - In addition, if you use an unusual directory layout you can give -options like '--bindir=DIR' to specify different values for particular -kinds of files. Run 'configure --help' for a list of the directories -you can set and what kinds of files go in them. In general, the default -for these options is expressed in terms of '${prefix}', so that -specifying just '--prefix' will affect all of the other directory -specifications that were not explicitly provided. - - The most portable way to affect installation locations is to pass the -correct locations to 'configure'; however, many packages provide one or -both of the following shortcuts of passing variable assignments to the -'make install' command line to change installation locations without -having to reconfigure or recompile. - - The first method involves providing an override variable for each -affected directory. For example, 'make install -prefix=/alternate/directory' will choose an alternate location for all -directory configuration variables that were expressed in terms of -'${prefix}'. Any directories that were specified during 'configure', -but not in terms of '${prefix}', must each be overridden at install time -for the entire installation to be relocated. The approach of makefile -variable overrides for each directory variable is required by the GNU -Coding Standards, and ideally causes no recompilation. However, some -platforms have known limitations with the semantics of shared libraries -that end up requiring recompilation when using this method, particularly -noticeable in packages that use GNU Libtool. - - The second method involves providing the 'DESTDIR' variable. For -example, 'make install DESTDIR=/alternate/directory' will prepend -'/alternate/directory' before all installation names. The approach of -'DESTDIR' overrides is not required by the GNU Coding Standards, and -does not work on platforms that have drive letters. On the other hand, -it does better at avoiding recompilation issues, and works well even -when some directory options were not specified in terms of '${prefix}' -at 'configure' time. - -Optional Features -================= - - If the package supports it, you can cause programs to be installed -with an extra prefix or suffix on their names by giving 'configure' the -option '--program-prefix=PREFIX' or '--program-suffix=SUFFIX'. - - Some packages pay attention to '--enable-FEATURE' options to -'configure', where FEATURE indicates an optional part of the package. -They may also pay attention to '--with-PACKAGE' options, where PACKAGE -is something like 'gnu-as' or 'x' (for the X Window System). The -'README' should mention any '--enable-' and '--with-' options that the -package recognizes. - - For packages that use the X Window System, 'configure' can usually -find the X include and library files automatically, but if it doesn't, -you can use the 'configure' options '--x-includes=DIR' and -'--x-libraries=DIR' to specify their locations. - - Some packages offer the ability to configure how verbose the -execution of 'make' will be. For these packages, running './configure ---enable-silent-rules' sets the default to minimal output, which can be -overridden with 'make V=1'; while running './configure ---disable-silent-rules' sets the default to verbose, which can be -overridden with 'make V=0'. - -Particular systems -================== - - On HP-UX, the default C compiler is not ANSI C compatible. If GNU CC -is not installed, it is recommended to use the following options in -order to use an ANSI C compiler: - - ./configure CC="cc -Ae -D_XOPEN_SOURCE=500" - -and if that doesn't work, install pre-built binaries of GCC for HP-UX. - - HP-UX 'make' updates targets which have the same time stamps as their -prerequisites, which makes it generally unusable when shipped generated -files such as 'configure' are involved. Use GNU 'make' instead. - - On OSF/1 a.k.a. Tru64, some versions of the default C compiler cannot -parse its '' header file. The option '-nodtk' can be used as a -workaround. If GNU CC is not installed, it is therefore recommended to -try - - ./configure CC="cc" - -and if that doesn't work, try - - ./configure CC="cc -nodtk" - - On Solaris, don't put '/usr/ucb' early in your 'PATH'. This -directory contains several dysfunctional programs; working variants of -these programs are available in '/usr/bin'. So, if you need '/usr/ucb' -in your 'PATH', put it _after_ '/usr/bin'. - - On Haiku, software installed for all users goes in '/boot/common', -not '/usr/local'. It is recommended to use the following options: - - ./configure --prefix=/boot/common - -Specifying the System Type -========================== - - There may be some features 'configure' cannot figure out -automatically, but needs to determine by the type of machine the package -will run on. Usually, assuming the package is built to be run on the -_same_ architectures, 'configure' can figure that out, but if it prints -a message saying it cannot guess the machine type, give it the -'--build=TYPE' option. TYPE can either be a short name for the system -type, such as 'sun4', or a canonical name which has the form: - - CPU-COMPANY-SYSTEM - -where SYSTEM can have one of these forms: - - OS - KERNEL-OS - - See the file 'config.sub' for the possible values of each field. If -'config.sub' isn't included in this package, then this package doesn't -need to know the machine type. - - If you are _building_ compiler tools for cross-compiling, you should -use the option '--target=TYPE' to select the type of system they will -produce code for. - - If you want to _use_ a cross compiler, that generates code for a -platform different from the build platform, you should specify the -"host" platform (i.e., that on which the generated programs will -eventually be run) with '--host=TYPE'. - -Sharing Defaults -================ - - If you want to set default values for 'configure' scripts to share, -you can create a site shell script called 'config.site' that gives -default values for variables like 'CC', 'cache_file', and 'prefix'. -'configure' looks for 'PREFIX/share/config.site' if it exists, then -'PREFIX/etc/config.site' if it exists. Or, you can set the -'CONFIG_SITE' environment variable to the location of the site script. -A warning: not all 'configure' scripts look for a site script. - -Defining Variables -================== - - Variables not defined in a site shell script can be set in the -environment passed to 'configure'. However, some packages may run -configure again during the build, and the customized values of these -variables may be lost. In order to avoid this problem, you should set -them in the 'configure' command line, using 'VAR=value'. For example: - - ./configure CC=/usr/local2/bin/gcc - -causes the specified 'gcc' to be used as the C compiler (unless it is -overridden in the site shell script). - -Unfortunately, this technique does not work for 'CONFIG_SHELL' due to an -Autoconf limitation. Until the limitation is lifted, you can use this -workaround: - - CONFIG_SHELL=/bin/bash ./configure CONFIG_SHELL=/bin/bash - -'configure' Invocation -====================== - - 'configure' recognizes the following options to control how it -operates. - -'--help' -'-h' - Print a summary of all of the options to 'configure', and exit. - -'--help=short' -'--help=recursive' - Print a summary of the options unique to this package's - 'configure', and exit. The 'short' variant lists options used only - in the top level, while the 'recursive' variant lists options also - present in any nested packages. - -'--version' -'-V' - Print the version of Autoconf used to generate the 'configure' - script, and exit. - -'--cache-file=FILE' - Enable the cache: use and save the results of the tests in FILE, - traditionally 'config.cache'. FILE defaults to '/dev/null' to - disable caching. - -'--config-cache' -'-C' - Alias for '--cache-file=config.cache'. - -'--quiet' -'--silent' -'-q' - Do not print messages saying which checks are being made. To - suppress all normal output, redirect it to '/dev/null' (any error - messages will still be shown). - -'--srcdir=DIR' - Look for the package's source code in directory DIR. Usually - 'configure' can determine that directory automatically. - -'--prefix=DIR' - Use DIR as the installation prefix. *note Installation Names:: for - more details, including other options available for fine-tuning the - installation locations. - -'--no-create' -'-n' - Run the configure checks, but stop before creating any output - files. - -'configure' also accepts some other, not widely useful, options. Run -'configure --help' for more details. diff --git a/chafa/Makefile.am b/chafa/Makefile.am index 0e2b7f4d..f1d97681 100644 --- a/chafa/Makefile.am +++ b/chafa/Makefile.am @@ -16,6 +16,8 @@ libchafa_la_SOURCES = \ chafa-canvas.c \ chafa-canvas-config.c \ chafa-features.c \ + chafa-frame.c \ + chafa-image.c \ chafa-symbol-map.c \ chafa-term-db.c \ chafa-term-info.c \ @@ -28,6 +30,8 @@ chafainclude_HEADERS = \ chafa-canvas-config.h \ chafa-common.h \ chafa-features.h \ + chafa-frame.h \ + chafa-image.h \ chafa-symbol-map.h \ chafa-term-db.h \ chafa-term-info.h \ diff --git a/chafa/chafa-canvas-config.c b/chafa/chafa-canvas-config.c index 778fa227..c51e13bf 100644 --- a/chafa/chafa-canvas-config.c +++ b/chafa/chafa-canvas-config.c @@ -92,6 +92,14 @@ * @CHAFA_OPTIMIZATION_ALL: All optimizations enabled. **/ +/** + * ChafaPassthrough: + * @CHAFA_PASSTHROUGH_NONE: No passthrough guards will be used. + * @CHAFA_PASSTHROUGH_SCREEN: Passthrough guards for GNU Screen will be used. + * @CHAFA_PASSTHROUGH_TMUX: Passthrough guards for tmux will be used. + * @CHAFA_PASSTHROUGH_MAX: Last supported passthrough mode plus one. + **/ + /* Private */ void @@ -937,3 +945,52 @@ chafa_canvas_config_set_fg_only_enabled (ChafaCanvasConfig *config, gboolean fg_ config->fg_only_enabled = fg_only_enabled; } + +/** + * chafa_canvas_config_get_passthrough: + * @config: A #ChafaCanvasConfig + * + * Returns @config's #ChafaPassthrough setting. This defaults to + * #CHAFA_PASSTHROUGH_NONE. + * + * Passthrough is needed to transmit certain escape codes to the outer terminal + * when running in an inner terminal environment like tmux. When enabled, this + * will happen automatically as needed, dictated by information contained in a + * #ChafaTermInfo. + * + * Returns: The #ChafaPassthrough setting + * + * Since: 1.14 + **/ +ChafaPassthrough +chafa_canvas_config_get_passthrough (const ChafaCanvasConfig *config) +{ + g_return_val_if_fail (config != NULL, CHAFA_PASSTHROUGH_NONE); + g_return_val_if_fail (config->refs > 0, CHAFA_PASSTHROUGH_NONE); + + return config->passthrough; +} + +/** + * chafa_canvas_config_set_passthrough: + * @config: A #ChafaCanvasConfig + * @passthrough: A #ChafaPassthrough value + * + * Indicates which passthrough mode to use. This defaults to + * #CHAFA_PASSTHROUGH_NONE. + * + * Passthrough is needed to transmit certain escape codes to the outer terminal + * when running in an inner terminal environment like tmux. When enabled, this + * will happen automatically as needed, dictated by information contained in a + * #ChafaTermInfo. + * + * Since: 1.14 + **/ +void +chafa_canvas_config_set_passthrough (ChafaCanvasConfig *config, ChafaPassthrough passthrough) +{ + g_return_if_fail (config != NULL); + g_return_if_fail (config->refs > 0); + + config->passthrough = passthrough; +} diff --git a/chafa/chafa-canvas-config.h b/chafa/chafa-canvas-config.h index d2e3d116..ba1b9220 100644 --- a/chafa/chafa-canvas-config.h +++ b/chafa/chafa-canvas-config.h @@ -107,6 +107,18 @@ typedef enum } ChafaOptimizations; +/* Passthrough modes */ + +typedef enum +{ + CHAFA_PASSTHROUGH_NONE, + CHAFA_PASSTHROUGH_SCREEN, + CHAFA_PASSTHROUGH_TMUX, + + CHAFA_PASSTHROUGH_MAX +} +ChafaPassthrough; + /* Canvas config */ typedef struct ChafaCanvasConfig ChafaCanvasConfig; @@ -210,6 +222,11 @@ gboolean chafa_canvas_config_get_fg_only_enabled (const ChafaCanvasConfig *confi CHAFA_AVAILABLE_IN_1_8 void chafa_canvas_config_set_fg_only_enabled (ChafaCanvasConfig *config, gboolean fg_only_enabled); +CHAFA_AVAILABLE_IN_1_14 +ChafaPassthrough chafa_canvas_config_get_passthrough (const ChafaCanvasConfig *config); +CHAFA_AVAILABLE_IN_1_14 +void chafa_canvas_config_set_passthrough (ChafaCanvasConfig *config, ChafaPassthrough passthrough); + G_END_DECLS #endif /* __CHAFA_CANVAS_CONFIG_H__ */ diff --git a/chafa/chafa-canvas.c b/chafa/chafa-canvas.c index a8bcb12d..19caefc9 100644 --- a/chafa/chafa-canvas.c +++ b/chafa/chafa-canvas.c @@ -1245,6 +1245,114 @@ find_best_solid_char (ChafaCanvas *canvas) return best_char; } +static void +destroy_pixel_canvas (ChafaCanvas *canvas) +{ + if (canvas->pixel_canvas) + { + if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_SIXELS) + chafa_sixel_canvas_destroy (canvas->pixel_canvas); + else if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_KITTY) + chafa_kitty_canvas_destroy (canvas->pixel_canvas); + else if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_ITERM2) + chafa_iterm2_canvas_destroy (canvas->pixel_canvas); + + canvas->pixel_canvas = NULL; + } +} + +static void +draw_all_pixels (ChafaCanvas *canvas, ChafaPixelType src_pixel_type, + const guint8 *src_pixels, + gint src_width, gint src_height, gint src_rowstride) +{ + if (src_width == 0 || src_height == 0) + return; + + if (canvas->pixels) + { + g_free (canvas->pixels); + canvas->pixels = NULL; + } + + destroy_pixel_canvas (canvas); + + if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_SYMBOLS) + { + /* Symbol mode */ + + canvas->pixels = g_new (ChafaPixel, canvas->width_pixels * canvas->height_pixels); + + chafa_prepare_pixel_data_for_symbols (&canvas->fg_palette, &canvas->dither, + canvas->config.color_space, + canvas->config.preprocessing_enabled, + canvas->work_factor_int, + src_pixel_type, + src_pixels, + src_width, src_height, + src_rowstride, + canvas->pixels, + canvas->width_pixels, canvas->height_pixels); + + if (canvas->config.alpha_threshold == 0) + canvas->have_alpha = FALSE; + + update_cells (canvas); + canvas->needs_clear = FALSE; + + g_free (canvas->pixels); + canvas->pixels = NULL; + } + else if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_SIXELS) + { + /* Sixel mode */ + + canvas->fg_palette.alpha_threshold = canvas->config.alpha_threshold; + canvas->pixel_canvas = chafa_sixel_canvas_new (canvas->width_pixels, + canvas->height_pixels, + canvas->config.color_space, + &canvas->fg_palette, + &canvas->dither); + chafa_sixel_canvas_draw_all_pixels (canvas->pixel_canvas, + src_pixel_type, + src_pixels, + src_width, src_height, + src_rowstride); + } + else if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_KITTY) + { + ChafaColor bg_color; + + chafa_unpack_color (canvas->config.bg_color_packed_rgb, &bg_color); + bg_color.ch [3] = canvas->config.alpha_threshold < 1 ? 0x00 : 0xff; + + /* Kitty mode */ + + canvas->fg_palette.alpha_threshold = canvas->config.alpha_threshold; + canvas->pixel_canvas = chafa_kitty_canvas_new (canvas->width_pixels, + canvas->height_pixels); + chafa_kitty_canvas_draw_all_pixels (canvas->pixel_canvas, + src_pixel_type, + src_pixels, + src_width, src_height, + src_rowstride, + bg_color); + } + else /* if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_ITERM2) */ + { + /* iTerm2 mode */ + + canvas->fg_palette.alpha_threshold = canvas->config.alpha_threshold; + canvas->pixel_canvas = chafa_iterm2_canvas_new (canvas->width_pixels, + canvas->height_pixels); + chafa_iterm2_canvas_draw_all_pixels (canvas->pixel_canvas, + src_pixel_type, + src_pixels, + src_width, src_height, + src_rowstride); + } +} + /** * chafa_canvas_new: * @config: Configuration to use or %NULL for hardcoded defaults @@ -1306,6 +1414,7 @@ chafa_canvas_new (const ChafaCanvasConfig *config) canvas->work_factor_int = canvas->config.work_factor * 10 + 0.5f; canvas->needs_clear = TRUE; canvas->have_alpha = FALSE; + canvas->placement_id = -1; canvas->consider_inverted = !(canvas->config.fg_only_enabled || canvas->config.canvas_mode == CHAFA_CANVAS_MODE_FGBG); @@ -1406,6 +1515,9 @@ chafa_canvas_new_similar (ChafaCanvas *orig) chafa_dither_copy (&orig->dither, &canvas->dither); + canvas->placement_id = -1; + canvas->image = NULL; + return canvas; } @@ -1427,22 +1539,6 @@ chafa_canvas_ref (ChafaCanvas *canvas) g_atomic_int_inc (&canvas->refs); } -static void -destroy_pixel_canvas (ChafaCanvas *canvas) -{ - if (canvas->pixel_canvas) - { - if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_SIXELS) - chafa_sixel_canvas_destroy (canvas->pixel_canvas); - else if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_KITTY) - chafa_kitty_canvas_destroy (canvas->pixel_canvas); - else if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_ITERM2) - chafa_iterm2_canvas_destroy (canvas->pixel_canvas); - - canvas->pixel_canvas = NULL; - } -} - /** * chafa_canvas_unref: * @canvas: Canvas to remove a reference from @@ -1461,6 +1557,8 @@ chafa_canvas_unref (ChafaCanvas *canvas) if (g_atomic_int_dec_and_test (&canvas->refs)) { + if (canvas->image) + chafa_image_unref (canvas->image); chafa_canvas_config_deinit (&canvas->config); destroy_pixel_canvas (canvas); chafa_dither_deinit (&canvas->dither); @@ -1491,6 +1589,44 @@ chafa_canvas_peek_config (ChafaCanvas *canvas) return &canvas->config; } +/** + * chafa_canvas_set_image: + * @canvas: Canvas to place the image on + * @image: Image to place + * @placement_id: Placement ID to use, or -1 to assign it automatically + * + * Places @image on @canvas, replacing the latter's content. The image will + * be stretched to fit the canvas. + * + * The canvas will keep a reference to the image until it is replaced or the + * canvas itself is freed. + */ +void +chafa_canvas_set_image (ChafaCanvas *canvas, ChafaImage *image, + gint placement_id) +{ + ChafaFrame *frame; + + g_return_if_fail (canvas != NULL); + g_return_if_fail (canvas->refs > 0); + + if (placement_id == 0) + placement_id = -1; + + chafa_image_ref (image); + if (canvas->image) + chafa_image_unref (canvas->image); + canvas->image = image; + canvas->placement_id = placement_id; + + frame = image->frame; + if (!frame) + return; + + draw_all_pixels (canvas, frame->pixel_type, frame->data, + frame->width, frame->height, frame->rowstride); +} + /** * chafa_canvas_draw_all_pixels: * @canvas: Canvas whose pixel data to replace @@ -1517,91 +1653,8 @@ chafa_canvas_draw_all_pixels (ChafaCanvas *canvas, ChafaPixelType src_pixel_type g_return_if_fail (src_width >= 0); g_return_if_fail (src_height >= 0); - if (src_width == 0 || src_height == 0) - return; - - if (canvas->pixels) - { - g_free (canvas->pixels); - canvas->pixels = NULL; - } - - destroy_pixel_canvas (canvas); - - if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_SYMBOLS) - { - /* Symbol mode */ - - canvas->pixels = g_new (ChafaPixel, canvas->width_pixels * canvas->height_pixels); - - chafa_prepare_pixel_data_for_symbols (&canvas->fg_palette, &canvas->dither, - canvas->config.color_space, - canvas->config.preprocessing_enabled, - canvas->work_factor_int, - src_pixel_type, - src_pixels, - src_width, src_height, - src_rowstride, - canvas->pixels, - canvas->width_pixels, canvas->height_pixels); - - if (canvas->config.alpha_threshold == 0) - canvas->have_alpha = FALSE; - - update_cells (canvas); - canvas->needs_clear = FALSE; - - g_free (canvas->pixels); - canvas->pixels = NULL; - } - else if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_SIXELS) - { - /* Sixel mode */ - - canvas->fg_palette.alpha_threshold = canvas->config.alpha_threshold; - canvas->pixel_canvas = chafa_sixel_canvas_new (canvas->width_pixels, - canvas->height_pixels, - canvas->config.color_space, - &canvas->fg_palette, - &canvas->dither); - chafa_sixel_canvas_draw_all_pixels (canvas->pixel_canvas, - src_pixel_type, - src_pixels, - src_width, src_height, - src_rowstride); - } - else if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_KITTY) - { - ChafaColor bg_color; - - chafa_unpack_color (canvas->config.bg_color_packed_rgb, &bg_color); - bg_color.ch [3] = canvas->config.alpha_threshold < 1 ? 0x00 : 0xff; - - /* Kitty mode */ - - canvas->fg_palette.alpha_threshold = canvas->config.alpha_threshold; - canvas->pixel_canvas = chafa_kitty_canvas_new (canvas->width_pixels, - canvas->height_pixels); - chafa_kitty_canvas_draw_all_pixels (canvas->pixel_canvas, - src_pixel_type, - src_pixels, - src_width, src_height, - src_rowstride, - bg_color); - } - else /* if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_ITERM2) */ - { - /* iTerm2 mode */ - - canvas->fg_palette.alpha_threshold = canvas->config.alpha_threshold; - canvas->pixel_canvas = chafa_iterm2_canvas_new (canvas->width_pixels, - canvas->height_pixels); - chafa_iterm2_canvas_draw_all_pixels (canvas->pixel_canvas, - src_pixel_type, - src_pixels, - src_width, src_height, - src_rowstride); - } + draw_all_pixels (canvas, src_pixel_type, src_pixels, + src_width, src_height, src_rowstride); } /** @@ -1623,8 +1676,8 @@ void chafa_canvas_set_contents_rgba8 (ChafaCanvas *canvas, const guint8 *src_pixels, gint src_width, gint src_height, gint src_rowstride) { - chafa_canvas_draw_all_pixels (canvas, CHAFA_PIXEL_RGBA8_UNASSOCIATED, - src_pixels, src_width, src_height, src_rowstride); + draw_all_pixels (canvas, CHAFA_PIXEL_RGBA8_UNASSOCIATED, + src_pixels, src_width, src_height, src_rowstride); } /** @@ -1689,21 +1742,11 @@ chafa_canvas_print (ChafaCanvas *canvas, ChafaTermInfo *term_info) else if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_SIXELS && chafa_term_info_get_seq (term_info, CHAFA_TERM_SEQ_BEGIN_SIXELS)) { - gchar buf [CHAFA_TERM_SEQ_LENGTH_MAX + 1]; - gchar *out; - /* Sixel mode */ - out = chafa_term_info_emit_begin_sixels (term_info, buf, 0, 0, 0); - *out = '\0'; - str = g_string_new (buf); - - g_string_append_printf (str, "\"1;1;%d;%d", canvas->width_pixels, canvas->height_pixels); - chafa_sixel_canvas_build_ansi (canvas->pixel_canvas, str); - - out = chafa_term_info_emit_end_sixels (term_info, buf); - *out = '\0'; - g_string_append (str, buf); + str = g_string_new (""); + chafa_sixel_canvas_build_ansi (canvas->pixel_canvas, term_info, str, + canvas->config.passthrough); } else if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_KITTY && chafa_term_info_get_seq (term_info, CHAFA_TERM_SEQ_BEGIN_KITTY_IMMEDIATE_IMAGE_V1)) @@ -1712,7 +1755,9 @@ chafa_canvas_print (ChafaCanvas *canvas, ChafaTermInfo *term_info) str = g_string_new (""); chafa_kitty_canvas_build_ansi (canvas->pixel_canvas, term_info, str, - canvas->config.width, canvas->config.height); + canvas->config.width, canvas->config.height, + canvas->placement_id, + canvas->config.passthrough); } else if (canvas->config.pixel_mode == CHAFA_PIXEL_MODE_ITERM2) { diff --git a/chafa/chafa-canvas.h b/chafa/chafa-canvas.h index dc5cb9bd..1e217268 100644 --- a/chafa/chafa-canvas.h +++ b/chafa/chafa-canvas.h @@ -42,6 +42,10 @@ void chafa_canvas_unref (ChafaCanvas *canvas); CHAFA_AVAILABLE_IN_ALL const ChafaCanvasConfig *chafa_canvas_peek_config (ChafaCanvas *canvas); +CHAFA_AVAILABLE_IN_1_14 +void chafa_canvas_set_image (ChafaCanvas *canvas, ChafaImage *image, + gint placement_id); + CHAFA_AVAILABLE_IN_1_2 void chafa_canvas_draw_all_pixels (ChafaCanvas *canvas, ChafaPixelType src_pixel_type, const guint8 *src_pixels, diff --git a/chafa/chafa-frame.c b/chafa/chafa-frame.c new file mode 100644 index 00000000..db9b11da --- /dev/null +++ b/chafa/chafa-frame.c @@ -0,0 +1,180 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2018-2023 Hans Petter Jansson + * + * This file is part of Chafa, a program that turns images into character art. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Chafa. If not, see . */ + +#include "config.h" +#include "chafa.h" +#include "internal/chafa-private.h" + +/** + * SECTION:chafa-frame + * @title: ChafaFrame + * @short_description: A raster image frame that can be added to a ChafaImage + * + * A #ChafaFrame contains the specific of a single frame of image data. It can + * be added to a #ChafaImage. + **/ + +static ChafaFrame * +new_frame (gpointer data, + ChafaPixelType pixel_type, + gint width, gint height, gint rowstride, + gboolean data_is_owned) +{ + ChafaFrame *frame; + + frame = g_new0 (ChafaFrame, 1); + frame->refs = 1; + + frame->pixel_type = pixel_type; + frame->width = width; + frame->height = height; + frame->rowstride = rowstride; + frame->data = data; + frame->data_is_owned = data_is_owned; + + return frame; +} + +/** + * chafa_frame_new: + * @data: Pointer to an image data buffer to copy from + * @pixel_type: The #ChafaPixelType of the source data + * @width: Width of the image, in pixels + * @height: Height of the image, in pixels + * @rowstride: Number of bytes to advance from the start of one row to the next + * + * Creates a new #ChafaFrame containing a copy of the image data pointed to + * by @data. + * + * Returns: The new frame + * + * Since: 1.14 + **/ +ChafaFrame * +chafa_frame_new (gconstpointer data, + ChafaPixelType pixel_type, + gint width, gint height, gint rowstride) +{ + gpointer owned_data; + + owned_data = g_malloc (height * rowstride); + memcpy (owned_data, data, height * rowstride); + return new_frame (owned_data, pixel_type, width, height, rowstride, TRUE); +} + +/** + * chafa_frame_new_steal: + * @data: Pointer to an image data buffer to assign + * @pixel_type: The #ChafaPixelType of the buffer + * @width: Width of the image, in pixels + * @height: Height of the image, in pixels + * @rowstride: Number of bytes to advance from the start of one row to the next + * + * Creates a new #ChafaFrame, which takes ownership of the @data buffer. The + * buffer will be freed with #g_free() when the frame's reference count drops + * to zero. + * + * Returns: The new frame + * + * Since: 1.14 + **/ +ChafaFrame * +chafa_frame_new_steal (gpointer data, + ChafaPixelType pixel_type, + gint width, gint height, gint rowstride) +{ + return new_frame (data, pixel_type, width, height, rowstride, TRUE); +} + +/** + * chafa_frame_new_borrow: + * @data: Pointer to an image data buffer to assign + * @pixel_type: The #ChafaPixelType of the buffer + * @width: Width of the image, in pixels + * @height: Height of the image, in pixels + * @rowstride: Number of bytes to advance from the start of one row to the next + * + * Creates a new #ChafaFrame embedding the @data pointer. It's the caller's + * responsibility to ensure the pointer remains valid for the lifetime of + * the frame. The frame will not free the buffer when its reference count drops + * to zero. + * + * THIS IS DANGEROUS API which should only be used when the life cycle of the + * frame is short, stealing the buffer is impossible, and copying would cause + * unacceptable performance degradation. + * + * Use #chafa_frame_new() instead. + * + * Returns: The new frame + * + * Since: 1.14 + **/ +ChafaFrame * +chafa_frame_new_borrow (gpointer data, + ChafaPixelType pixel_type, + gint width, gint height, gint rowstride) +{ + return new_frame (data, pixel_type, width, height, rowstride, FALSE); +} + +/** + * chafa_frame_ref: + * @frame: Frame to add a reference to + * + * Adds a reference to @frame. + * + * Since: 1.14 + **/ +void +chafa_frame_ref (ChafaFrame *frame) +{ + gint refs; + + g_return_if_fail (frame != NULL); + refs = g_atomic_int_get (&frame->refs); + g_return_if_fail (refs > 0); + + g_atomic_int_inc (&frame->refs); +} + +/** + * chafa_frame_unref: + * @frame: Frame to remove a reference from + * + * Removes a reference from @frame. When the reference count drops to zero, + * the frame is freed and can no longer be used. + * + * Since: 1.14 + **/ +void +chafa_frame_unref (ChafaFrame *frame) +{ + gint refs; + + g_return_if_fail (frame != NULL); + refs = g_atomic_int_get (&frame->refs); + g_return_if_fail (refs > 0); + + if (g_atomic_int_dec_and_test (&frame->refs)) + { + if (frame->data_is_owned) + g_free (frame->data); + g_free (frame); + } +} diff --git a/chafa/chafa-frame.h b/chafa/chafa-frame.h new file mode 100644 index 00000000..8670ba9e --- /dev/null +++ b/chafa/chafa-frame.h @@ -0,0 +1,51 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2018-2023 Hans Petter Jansson + * + * This file is part of Chafa, a program that turns images into character art. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Chafa. If not, see . */ + +#ifndef __CHAFA_FRAME_H__ +#define __CHAFA_FRAME_H__ + +#if !defined (__CHAFA_H_INSIDE__) && !defined (CHAFA_COMPILATION) +# error "Only can be included directly." +#endif + +G_BEGIN_DECLS + +typedef struct ChafaFrame ChafaFrame; + +CHAFA_AVAILABLE_IN_1_14 +ChafaFrame *chafa_frame_new (gconstpointer data, + ChafaPixelType pixel_type, + gint width, gint height, gint rowstride); +CHAFA_AVAILABLE_IN_1_14 +ChafaFrame *chafa_frame_new_steal (gpointer data, + ChafaPixelType pixel_type, + gint width, gint height, gint rowstride); +CHAFA_AVAILABLE_IN_1_14 +ChafaFrame *chafa_frame_new_borrow (gpointer data, + ChafaPixelType pixel_type, + gint width, gint height, gint rowstride); + +CHAFA_AVAILABLE_IN_1_14 +void chafa_frame_ref (ChafaFrame *frame); +CHAFA_AVAILABLE_IN_1_14 +void chafa_frame_unref (ChafaFrame *frame); + +G_END_DECLS + +#endif /* __CHAFA_FRAME_H__ */ diff --git a/chafa/chafa-image.c b/chafa/chafa-image.c new file mode 100644 index 00000000..607885c6 --- /dev/null +++ b/chafa/chafa-image.c @@ -0,0 +1,120 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2018-2023 Hans Petter Jansson + * + * This file is part of Chafa, a program that turns images into character art. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Chafa. If not, see . */ + +#include "config.h" +#include "chafa.h" +#include "internal/chafa-private.h" + +/** + * SECTION:chafa-image + * @title: ChafaImage + * @short_description: An image that can be placed on a ChafaCanvas + * + * A #ChafaImage represents a raster image for placement on a #ChafaCanvas. It + * can currently hold a single #ChafaFrame. + **/ + +/** + * chafa_image_new: + * + * Creates a new #ChafaImage. The image is initially transparent and dimensionless. + * + * Returns: The new image + * + * Since: 1.14 + **/ +ChafaImage * +chafa_image_new (void) +{ + ChafaImage *image; + + image = g_new0 (ChafaImage, 1); + image->refs = 1; + + return image; +} + +/** + * chafa_image_ref: + * @image: Image to add a reference to + * + * Adds a reference to @image. + * + * Since: 1.14 + **/ +void +chafa_image_ref (ChafaImage *image) +{ + gint refs; + + g_return_if_fail (image != NULL); + refs = g_atomic_int_get (&image->refs); + g_return_if_fail (refs > 0); + + g_atomic_int_inc (&image->refs); +} + +/** + * chafa_image_unref: + * @image: Image to remove a reference from + * + * Removes a reference from @image. When the reference count drops to zero, + * the image is freed and can no longer be used. + * + * Since: 1.14 + **/ +void +chafa_image_unref (ChafaImage *image) +{ + gint refs; + + g_return_if_fail (image != NULL); + refs = g_atomic_int_get (&image->refs); + g_return_if_fail (refs > 0); + + if (g_atomic_int_dec_and_test (&image->refs)) + { + if (image->frame) + chafa_frame_unref (image->frame); + g_free (image); + } +} + +/** + * chafa_image_set_frame: + * @image: Image to assign frame to + * @frame: Frame to be assigned to image + * + * Assigns @frame as the content for @image. The image will keep its own + * reference to the frame. + * + * Since: 1.14 + **/ +void +chafa_image_set_frame (ChafaImage *image, ChafaFrame *frame) +{ + g_return_if_fail (image != NULL); + + if (frame) + chafa_frame_ref (frame); + if (image->frame) + chafa_frame_unref (image->frame); + + image->frame = frame; +} diff --git a/chafa/chafa-image.h b/chafa/chafa-image.h new file mode 100644 index 00000000..13da6adf --- /dev/null +++ b/chafa/chafa-image.h @@ -0,0 +1,43 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2018-2023 Hans Petter Jansson + * + * This file is part of Chafa, a program that turns images into character art. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Chafa. If not, see . */ + +#ifndef __CHAFA_IMAGE_H__ +#define __CHAFA_IMAGE_H__ + +#if !defined (__CHAFA_H_INSIDE__) && !defined (CHAFA_COMPILATION) +# error "Only can be included directly." +#endif + +G_BEGIN_DECLS + +typedef struct ChafaImage ChafaImage; + +CHAFA_AVAILABLE_IN_1_14 +ChafaImage *chafa_image_new (void); +CHAFA_AVAILABLE_IN_1_14 +void chafa_image_ref (ChafaImage *image); +CHAFA_AVAILABLE_IN_1_14 +void chafa_image_unref (ChafaImage *image); + +CHAFA_AVAILABLE_IN_1_14 +void chafa_image_set_frame (ChafaImage *image, ChafaFrame *frame); + +G_END_DECLS + +#endif /* __CHAFA_IMAGE_H__ */ diff --git a/chafa/chafa-term-db.c b/chafa/chafa-term-db.c index 09ecb40e..ce3bc882 100644 --- a/chafa/chafa-term-db.c +++ b/chafa/chafa-term-db.c @@ -289,6 +289,7 @@ static const SeqStr *color_fbterm_list [] = static const SeqStr kitty_seqs [] = { { CHAFA_TERM_SEQ_BEGIN_KITTY_IMMEDIATE_IMAGE_V1, "\033_Ga=T,f=%1,s=%2,v=%3,c=%4,r=%5,m=1\033\\" }, + { CHAFA_TERM_SEQ_BEGIN_KITTY_IMMEDIATE_VIRT_IMAGE_V1, "\033_Ga=T,U=1,q=2,f=%1,s=%2,v=%3,c=%4,r=%5,i=%6,m=1\033\\" }, { CHAFA_TERM_SEQ_END_KITTY_IMAGE, "\033_Gm=0\033\\" }, { CHAFA_TERM_SEQ_BEGIN_KITTY_IMAGE_CHUNK, "\033_Gm=1;" }, { CHAFA_TERM_SEQ_END_KITTY_IMAGE_CHUNK, "\033\\" }, @@ -304,6 +305,22 @@ static const SeqStr iterm2_seqs [] = { CHAFA_TERM_SEQ_MAX, NULL } }; +static const SeqStr tmux_seqs [] = +{ + { CHAFA_TERM_SEQ_BEGIN_TMUX_PASSTHROUGH, "\033Ptmux;" }, + { CHAFA_TERM_SEQ_END_TMUX_PASSTHROUGH, "\033\\" }, + + { CHAFA_TERM_SEQ_MAX, NULL } +}; + +static const SeqStr screen_seqs [] = +{ + { CHAFA_TERM_SEQ_BEGIN_SCREEN_PASSTHROUGH, "\033P" }, + { CHAFA_TERM_SEQ_END_SCREEN_PASSTHROUGH, "\033\\" }, + + { CHAFA_TERM_SEQ_MAX, NULL } +}; + static const SeqStr *fallback_list [] = { vt220_seqs, @@ -314,6 +331,8 @@ static const SeqStr *fallback_list [] = sixel_seqs, kitty_seqs, iterm2_seqs, + screen_seqs, + tmux_seqs, NULL }; @@ -369,12 +388,15 @@ detect_capabilities (ChafaTermInfo *ti, gchar **envp) const gchar *tmux; const gchar *ctx_backend; const gchar *lc_terminal; + const gchar *kitty_pid; + const gchar *mlterm; const gchar *nvim; const gchar *nvim_tui_enable_true_color; gchar *comspec = NULL; const SeqStr **color_seq_list = color_256_list; const SeqStr *gfx_seqs = NULL; const SeqStr *rep_seqs_local = NULL; + const SeqStr *inner_seqs = NULL; term = getenv_or_blank (envp, "TERM"); colorterm = getenv_or_blank (envp, "COLORTERM"); @@ -385,6 +407,8 @@ detect_capabilities (ChafaTermInfo *ti, gchar **envp) tmux = getenv_or_blank (envp, "TMUX"); ctx_backend = getenv_or_blank (envp, "CTX_BACKEND"); lc_terminal = getenv_or_blank (envp, "LC_TERMINAL"); + kitty_pid = getenv_or_blank (envp, "KITTY_PID"); + mlterm = getenv_or_blank (envp, "MLTERM"); nvim = getenv_or_blank (envp, "NVIM"); nvim_tui_enable_true_color = getenv_or_blank (envp, "NVIM_TUI_ENABLE_TRUE_COLOR"); @@ -445,7 +469,8 @@ detect_capabilities (ChafaTermInfo *ti, gchar **envp) color_seq_list = color_direct_list; /* Kitty has a unique graphics protocol */ - if (!strcmp (term, "xterm-kitty")) + if (!strcmp (term, "xterm-kitty") + || strlen (kitty_pid) > 0) gfx_seqs = kitty_seqs; /* iTerm2 supports truecolor and has a unique graphics protocol */ @@ -498,6 +523,7 @@ detect_capabilities (ChafaTermInfo *ti, gchar **envp) * yaft supports sixels and truecolor escape codes, but it remaps cell * colors to a 256-color palette. */ if (!strcmp (term, "mlterm") + || strlen (mlterm) > 0 || !strcmp (term, "yaft") || !strcmp (term, "yaft-256color")) { @@ -523,22 +549,28 @@ detect_capabilities (ChafaTermInfo *ti, gchar **envp) * like so: screen.xterm-256color */ if (!strncmp (term, "screen", 6)) { - color_seq_list = color_256_list; - /* 'tmux' also sets TERM=screen, but it supports truecolor codes. * You may have to add the following to .tmux.conf to prevent * remapping to 256 colors: * * tmux set-option -ga terminal-overrides ",screen-256color:Tc" */ if (strlen (tmux) > 0) + { color_seq_list = color_direct_list; + inner_seqs = tmux_seqs; + } + else + { + color_seq_list = color_256_list; + inner_seqs = screen_seqs; + } /* screen and older tmux do not support REP. Newer tmux does, * but there's no reliable way to tell which version we're dealing with. */ rep_seqs_local = NULL; - /* No graphics in screen and tmux */ - gfx_seqs = NULL; + /* Graphics is allowed in screen and tmux, with passthrough. */ + /* gfx_seqs = NULL; */ } /* If TERM is "linux", we're probably on the Linux console, which supports @@ -560,6 +592,7 @@ detect_capabilities (ChafaTermInfo *ti, gchar **envp) add_seq_list (ti, color_seq_list); add_seqs (ti, gfx_seqs); add_seqs (ti, rep_seqs_local); + add_seqs (ti, inner_seqs); } static ChafaTermDb * diff --git a/chafa/chafa-term-info.c b/chafa/chafa-term-info.c index d09d69cf..96a1d996 100644 --- a/chafa/chafa-term-info.c +++ b/chafa/chafa-term-info.c @@ -184,6 +184,11 @@ * @CHAFA_TERM_SEQ_ENABLE_ALT_SCREEN: Switch to the alternate screen buffer. * @CHAFA_TERM_SEQ_DISABLE_ALT_SCREEN: Switch back to the regular screen buffer. * @CHAFA_TERM_SEQ_MAX: Last control sequence plus one. + * @CHAFA_TERM_SEQ_BEGIN_SCREEN_PASSTHROUGH: Begins OSC passthrough for the GNU Screen multiplexer. + * @CHAFA_TERM_SEQ_END_SCREEN_PASSTHROUGH: Ends OSC passthrough for the GNU Screen multiplexer. + * @CHAFA_TERM_SEQ_BEGIN_TMUX_PASSTHROUGH: Begins OSC passthrough for the tmux multiplexer. + * @CHAFA_TERM_SEQ_END_TMUX_PASSTHROUGH: Ends OSC passthrough for the tmux multiplexer. + * @CHAFA_TERM_SEQ_BEGIN_KITTY_IMMEDIATE_VIRT_IMAGE_V1: Begin upload of virtual Kitty image for immediate display at cursor. * * An enumeration of the control sequences supported by #ChafaTermInfo. **/ @@ -424,6 +429,20 @@ emit_seq_5_args_uint (const ChafaTermInfo *term_info, gchar *out, ChafaTermSeq s return emit_seq_guint (term_info, out, seq, args, 5); } +static gchar * +emit_seq_6_args_uint (const ChafaTermInfo *term_info, gchar *out, ChafaTermSeq seq, guint arg0, guint arg1, guint arg2, guint arg3, guint arg4, guint arg5) +{ + guint args [6]; + + args [0] = arg0; + args [1] = arg1; + args [2] = arg2; + args [3] = arg3; + args [4] = arg4; + args [5] = arg5; + return emit_seq_guint (term_info, out, seq, args, 6); +} + static gchar * emit_seq_3_args_uint8 (const ChafaTermInfo *term_info, gchar *out, ChafaTermSeq seq, guint8 arg0, guint8 arg1, guint8 arg2) { @@ -882,7 +901,7 @@ chafa_term_info_emit_seq (ChafaTermInfo *term_info, ChafaTermSeq seq, ...) } /** - * chafa_term_info_emit_seq: + * chafa_term_info_parse_seq: * @term_info: A #ChafaTermInfo * @seq: A #ChafaTermSeq to attempt to parse * @input: Pointer to pointer to input data @@ -1006,6 +1025,10 @@ gchar *chafa_term_info_emit_##func_name(const ChafaTermInfo *term_info, gchar *d gchar *chafa_term_info_emit_##func_name(const ChafaTermInfo *term_info, gchar *dest, guint arg0, guint arg1, guint arg2, guint arg3, guint arg4) \ { return emit_seq_5_args_uint (term_info, dest, CHAFA_TERM_SEQ_##seq_name, arg0, arg1, arg2, arg3, arg4); } +#define DEFINE_EMIT_SEQ_6_none_guint(func_name, seq_name) \ +gchar *chafa_term_info_emit_##func_name(const ChafaTermInfo *term_info, gchar *dest, guint arg0, guint arg1, guint arg2, guint arg3, guint arg4, guint arg5) \ +{ return emit_seq_6_args_uint (term_info, dest, CHAFA_TERM_SEQ_##seq_name, arg0, arg1, arg2, arg3, arg4, arg5); } + #define DEFINE_EMIT_SEQ_3_none_guint8(func_name, seq_name) \ gchar *chafa_term_info_emit_##func_name(const ChafaTermInfo *term_info, gchar *dest, guint8 arg0, guint8 arg1, guint8 arg2) \ { return emit_seq_3_args_uint8 (term_info, dest, CHAFA_TERM_SEQ_##seq_name, arg0, arg1, arg2); } diff --git a/chafa/chafa-term-seq-def.h b/chafa/chafa-term-seq-def.h index d36f4c3f..46576d5c 100644 --- a/chafa/chafa-term-seq-def.h +++ b/chafa/chafa-term-seq-def.h @@ -2567,6 +2567,119 @@ CHAFA_TERM_SEQ_DEF(enable_alt_screen, ENABLE_ALT_SCREEN, 0, none, char) **/ CHAFA_TERM_SEQ_DEF(disable_alt_screen, DISABLE_ALT_SCREEN, 0, none, char) +/** + * chafa_term_info_emit_begin_screen_passthrough: + * @term_info: A #ChafaTermInfo + * @dest: String destination + * + * Prints the control sequence for #CHAFA_TERM_SEQ_BEGIN_SCREEN_PASSTHROUGH. + * + * @dest must have enough space to hold + * #CHAFA_TERM_SEQ_LENGTH_MAX bytes, even if the emitted sequence is + * shorter. The output will not be zero-terminated. + * + * Any control sequences between the beginning and end passthrough seqs + * must be escaped by turning \033 into \033\033. + * + * Returns: Pointer to first byte after emitted string + * + * Since: 1.14 + **/ +CHAFA_TERM_SEQ_DEF(begin_screen_passthrough, BEGIN_SCREEN_PASSTHROUGH, 0, none, char) + +/** + * chafa_term_info_emit_end_screen_passthrough: + * @term_info: A #ChafaTermInfo + * @dest: String destination + * + * Prints the control sequence for #CHAFA_TERM_SEQ_END_SCREEN_PASSTHROUGH. + * + * @dest must have enough space to hold + * #CHAFA_TERM_SEQ_LENGTH_MAX bytes, even if the emitted sequence is + * shorter. The output will not be zero-terminated. + * + * Any control sequences between the beginning and end passthrough seqs + * must be escaped by turning \033 into \033\033. + * + * Returns: Pointer to first byte after emitted string + * + * Since: 1.14 + **/ +CHAFA_TERM_SEQ_DEF(end_screen_passthrough, END_SCREEN_PASSTHROUGH, 0, none, char) + +/** + * chafa_term_info_emit_begin_tmux_passthrough: + * @term_info: A #ChafaTermInfo + * @dest: String destination + * + * Prints the control sequence for #CHAFA_TERM_SEQ_BEGIN_TMUX_PASSTHROUGH. + * + * @dest must have enough space to hold + * #CHAFA_TERM_SEQ_LENGTH_MAX bytes, even if the emitted sequence is + * shorter. The output will not be zero-terminated. + * + * Any control sequences between the beginning and end passthrough seqs + * must be escaped by turning \033 into \033\033. + * + * Returns: Pointer to first byte after emitted string + * + * Since: 1.14 + **/ +CHAFA_TERM_SEQ_DEF(begin_tmux_passthrough, BEGIN_TMUX_PASSTHROUGH, 0, none, char) + +/** + * chafa_term_info_emit_end_tmux_passthrough: + * @term_info: A #ChafaTermInfo + * @dest: String destination + * + * Prints the control sequence for #CHAFA_TERM_SEQ_END_TMUX_PASSTHROUGH. + * + * @dest must have enough space to hold + * #CHAFA_TERM_SEQ_LENGTH_MAX bytes, even if the emitted sequence is + * shorter. The output will not be zero-terminated. + * + * Any control sequences between the beginning and end passthrough seqs + * must be escaped by turning \033 into \033\033. + * + * Returns: Pointer to first byte after emitted string + * + * Since: 1.14 + **/ +CHAFA_TERM_SEQ_DEF(end_tmux_passthrough, END_TMUX_PASSTHROUGH, 0, none, char) + +/** + * chafa_term_info_emit_begin_kitty_immediate_virt_image_v1: + * @term_info: A #ChafaTermInfo + * @dest: String destination + * @bpp: Bits per pixel + * @width_pixels: Image width in pixels + * @height_pixels: Image height in pixels + * @width_cells: Target width in cells + * @height_cells: Target height in cells + * @id: Image ID + * + * Prints the control sequence for #CHAFA_TERM_SEQ_BEGIN_KITTY_IMMEDIATE_IMAGE_V1. + * + * @dest must have enough space to hold + * #CHAFA_TERM_SEQ_LENGTH_MAX bytes, even if the emitted sequence is + * shorter. The output will not be zero-terminated. + * + * @bpp must be set to either 24 for RGB data, 32 for RGBA, or 100 to embed a + * PNG file. + * + * This sequence must be followed by zero or more paired sequences of + * type #CHAFA_TERM_SEQ_BEGIN_KITTY_IMAGE_CHUNK and #CHAFA_TERM_SEQ_END_KITTY_IMAGE_CHUNK + * with base-64 encoded image data between them. + * + * When the image data has been transferred, #CHAFA_TERM_SEQ_END_KITTY_IMAGE must + * be emitted. + * + * Returns: Pointer to first byte after emitted string + * + * Since: 1.14 + **/ +CHAFA_TERM_SEQ_DEF(begin_kitty_immediate_virt_image_v1, BEGIN_KITTY_IMMEDIATE_VIRT_IMAGE_V1, 6, none, guint, CHAFA_TERM_SEQ_ARGS guint bpp, guint width_pixels, guint height_pixels, guint width_cells, guint height_cells, guint id) + #undef CHAFA_TERM_SEQ_AVAILABILITY #undef CHAFA_TERM_SEQ_ARGS diff --git a/chafa/chafa.h b/chafa/chafa.h index 82b93d41..dd58a966 100644 --- a/chafa/chafa.h +++ b/chafa/chafa.h @@ -30,6 +30,8 @@ G_BEGIN_DECLS #include #include +#include +#include #include #include #include diff --git a/chafa/internal/Makefile.am b/chafa/internal/Makefile.am index b55482f5..fb76cd6c 100644 --- a/chafa/internal/Makefile.am +++ b/chafa/internal/Makefile.am @@ -33,6 +33,8 @@ libchafa_internal_la_SOURCES = \ chafa-kitty-canvas.h \ chafa-palette.c \ chafa-palette.h \ + chafa-passthrough-encoder.c \ + chafa-passthrough-encoder.h \ chafa-pca.c \ chafa-pca.h \ chafa-pixops.c \ diff --git a/chafa/internal/chafa-canvas-internal.h b/chafa/internal/chafa-canvas-internal.h index 998babcd..8c63985d 100644 --- a/chafa/internal/chafa-canvas-internal.h +++ b/chafa/internal/chafa-canvas-internal.h @@ -78,6 +78,13 @@ struct ChafaCanvas * (ChafaSixelCanvas *), (ChafaKittyCanvas *), (ChafaIterm2Canvas *) */ gpointer pixel_canvas; + /* It's possible to have a single placement that covers the entire + * canvas. In this case, the image and its placement ID are stored here. + * The placement ID defaults to -1, meaning we'll avoid using an ID or fall + * back to some arbitrary value. */ + ChafaImage *image; + gint placement_id; + /* Our palettes. Kind of a big structure, so they go last. */ ChafaPalette fg_palette; ChafaPalette bg_palette; diff --git a/chafa/internal/chafa-kitty-canvas.c b/chafa/internal/chafa-kitty-canvas.c index 970d18fd..f3b20486 100644 --- a/chafa/internal/chafa-kitty-canvas.c +++ b/chafa/internal/chafa-kitty-canvas.c @@ -26,6 +26,7 @@ #include "internal/chafa-bitfield.h" #include "internal/chafa-indexed-image.h" #include "internal/chafa-kitty-canvas.h" +#include "internal/chafa-passthrough-encoder.h" #include "internal/chafa-pixops.h" #include "internal/chafa-string-util.h" @@ -45,6 +46,63 @@ typedef struct } DrawCtx; +/* Kitty's cell-based placeholders use Unicode diacritics to encode each + * cell's row/col offsets. The below table maps integers to code points + * using this scheme. */ + +#define ROWCOLUMN_UNICHAR 0x10eeeeU +#define ENCODING_DIACRITIC_MAX 297 + +static const guint32 encoding_diacritics [ENCODING_DIACRITIC_MAX] = +{ + 0x0305, 0x030d, 0x030e, 0x0310, 0x0312, 0x033d, 0x033e, 0x033f, + 0x0346, 0x034a, 0x034b, 0x034c, 0x0350, 0x0351, 0x0352, 0x0357, + 0x035b, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, 0x0368, 0x0369, + 0x036a, 0x036b, 0x036c, 0x036d, 0x036e, 0x036f, 0x0483, 0x0484, + 0x0485, 0x0486, 0x0487, 0x0592, 0x0593, 0x0594, 0x0595, 0x0597, + 0x0598, 0x0599, 0x059c, 0x059d, 0x059e, 0x059f, 0x05a0, 0x05a1, + 0x05a8, 0x05a9, 0x05ab, 0x05ac, 0x05af, 0x05c4, 0x0610, 0x0611, + 0x0612, 0x0613, 0x0614, 0x0615, 0x0616, 0x0617, 0x0657, 0x0658, + 0x0659, 0x065a, 0x065b, 0x065d, 0x065e, 0x06d6, 0x06d7, 0x06d8, + 0x06d9, 0x06da, 0x06db, 0x06dc, 0x06df, 0x06e0, 0x06e1, 0x06e2, + 0x06e4, 0x06e7, 0x06e8, 0x06eb, 0x06ec, 0x0730, 0x0732, 0x0733, + 0x0735, 0x0736, 0x073a, 0x073d, 0x073f, 0x0740, 0x0741, 0x0743, + 0x0745, 0x0747, 0x0749, 0x074a, 0x07eb, 0x07ec, 0x07ed, 0x07ee, + 0x07ef, 0x07f0, 0x07f1, 0x07f3, 0x0816, 0x0817, 0x0818, 0x0819, + 0x081b, 0x081c, 0x081d, 0x081e, 0x081f, 0x0820, 0x0821, 0x0822, + 0x0823, 0x0825, 0x0826, 0x0827, 0x0829, 0x082a, 0x082b, 0x082c, + + /* 128 */ + + 0x082d, 0x0951, 0x0953, 0x0954, 0x0f82, 0x0f83, 0x0f86, 0x0f87, + 0x135d, 0x135e, 0x135f, 0x17dd, 0x193a, 0x1a17, 0x1a75, 0x1a76, + 0x1a77, 0x1a78, 0x1a79, 0x1a7a, 0x1a7b, 0x1a7c, 0x1b6b, 0x1b6d, + 0x1b6e, 0x1b6f, 0x1b70, 0x1b71, 0x1b72, 0x1b73, 0x1cd0, 0x1cd1, + 0x1cd2, 0x1cda, 0x1cdb, 0x1ce0, 0x1dc0, 0x1dc1, 0x1dc3, 0x1dc4, + 0x1dc5, 0x1dc6, 0x1dc7, 0x1dc8, 0x1dc9, 0x1dcb, 0x1dcc, 0x1dd1, + 0x1dd2, 0x1dd3, 0x1dd4, 0x1dd5, 0x1dd6, 0x1dd7, 0x1dd8, 0x1dd9, + 0x1dda, 0x1ddb, 0x1ddc, 0x1ddd, 0x1dde, 0x1ddf, 0x1de0, 0x1de1, + 0x1de2, 0x1de3, 0x1de4, 0x1de5, 0x1de6, 0x1dfe, 0x20d0, 0x20d1, + 0x20d4, 0x20d5, 0x20d6, 0x20d7, 0x20db, 0x20dc, 0x20e1, 0x20e7, + 0x20e9, 0x20f0, 0x2cef, 0x2cf0, 0x2cf1, 0x2de0, 0x2de1, 0x2de2, + 0x2de3, 0x2de4, 0x2de5, 0x2de6, 0x2de7, 0x2de8, 0x2de9, 0x2dea, + 0x2deb, 0x2dec, 0x2ded, 0x2dee, 0x2def, 0x2df0, 0x2df1, 0x2df2, + 0x2df3, 0x2df4, 0x2df5, 0x2df6, 0x2df7, 0x2df8, 0x2df9, 0x2dfa, + 0x2dfb, 0x2dfc, 0x2dfd, 0x2dfe, 0x2dff, 0xa66f, 0xa67c, 0xa67d, + 0xa6f0, 0xa6f1, 0xa8e0, 0xa8e1, 0xa8e2, 0xa8e3, 0xa8e4, 0xa8e5, + + /* 256 */ + + 0xa8e6, 0xa8e7, 0xa8e8, 0xa8e9, 0xa8ea, 0xa8eb, 0xa8ec, 0xa8ed, + 0xa8ee, 0xa8ef, 0xa8f0, 0xa8f1, 0xaab0, 0xaab2, 0xaab3, 0xaab7, + 0xaab8, 0xaabe, 0xaabf, 0xaac1, 0xfe20, 0xfe21, 0xfe22, 0xfe23, + 0xfe24, 0xfe25, 0xfe26, 0x10a0f, 0x10a38, 0x1d185, 0x1d186, 0x1d187, + 0x1d188, 0x1d189, 0x1d1aa, 0x1d1ab, 0x1d1ac, 0x1d1ad, 0x1d242, 0x1d243, + 0x1d244 + + /* 297 */ +}; + ChafaKittyCanvas * chafa_kitty_canvas_new (gint width, gint height) { @@ -136,21 +194,40 @@ encode_chunk (GString *gs, const guint8 *start, const guint8 *end) chafa_base64_deinit (&base64); } -void -chafa_kitty_canvas_build_ansi (ChafaKittyCanvas *kitty_canvas, ChafaTermInfo *term_info, GString *out_str, - gint width_cells, gint height_cells) +static void +end_passthrough (ChafaPassthroughEncoder *ptenc) +{ + gchar buf [CHAFA_TERM_SEQ_LENGTH_MAX + 1]; + + if (ptenc->mode == CHAFA_PASSTHROUGH_SCREEN) + { + gint i; + + *chafa_term_info_emit_end_screen_passthrough (ptenc->term_info, buf) = '\0'; + + for (i = 0; buf [i]; i++) + { + chafa_passthrough_encoder_flush (ptenc); + chafa_passthrough_encoder_append_len (ptenc, buf + i, 1); + } + } + else if (ptenc->mode == CHAFA_PASSTHROUGH_TMUX) + { + *chafa_term_info_emit_end_tmux_passthrough (ptenc->term_info, buf) = '\0'; + + chafa_passthrough_encoder_flush (ptenc); + g_string_append (ptenc->out, buf); + } + + chafa_passthrough_encoder_flush (ptenc); +} + +static void +build_image_chunks (ChafaKittyCanvas *kitty_canvas, ChafaPassthroughEncoder *ptenc) { const guint8 *p, *last; gchar seq [CHAFA_TERM_SEQ_LENGTH_MAX + 1]; - *chafa_term_info_emit_begin_kitty_immediate_image_v1 (term_info, seq, - 32, - kitty_canvas->width, - kitty_canvas->height, - width_cells, - height_cells) = '\0'; - g_string_append (out_str, seq); - last = ((guint8 *) kitty_canvas->rgba_image) + kitty_canvas->width * kitty_canvas->height * sizeof (guint32); @@ -158,21 +235,208 @@ chafa_kitty_canvas_build_ansi (ChafaKittyCanvas *kitty_canvas, ChafaTermInfo *te { const guint8 *end; - end = p + 512; + end = p + (ptenc->mode == CHAFA_PASSTHROUGH_SCREEN ? 64 : 512); if (end > last) end = last; - *chafa_term_info_emit_begin_kitty_image_chunk (term_info, seq) = '\0'; - g_string_append (out_str, seq); + *chafa_term_info_emit_begin_kitty_image_chunk (ptenc->term_info, seq) = '\0'; + chafa_passthrough_encoder_append (ptenc, seq); - encode_chunk (out_str, p, end); + encode_chunk (ptenc->out, p, end); - *chafa_term_info_emit_end_kitty_image_chunk (term_info, seq) = '\0'; - g_string_append (out_str, seq); + *chafa_term_info_emit_end_kitty_image_chunk (ptenc->term_info, seq) = '\0'; + chafa_passthrough_encoder_append (ptenc, seq); + chafa_passthrough_encoder_reset (ptenc); + end_passthrough (ptenc); p = end; } - *chafa_term_info_emit_end_kitty_image (term_info, seq) = '\0'; - g_string_append (out_str, seq); + *chafa_term_info_emit_end_kitty_image (ptenc->term_info, seq) = '\0'; + chafa_passthrough_encoder_append (ptenc, seq); + chafa_passthrough_encoder_reset (ptenc); + end_passthrough (ptenc); +} + +static void +build_immediate (ChafaKittyCanvas *kitty_canvas, ChafaTermInfo *term_info, GString *out_str, + gint width_cells, gint height_cells) +{ + ChafaPassthroughEncoder ptenc; + gchar seq [CHAFA_TERM_SEQ_LENGTH_MAX + 1]; + + chafa_passthrough_encoder_begin (&ptenc, CHAFA_PASSTHROUGH_NONE, term_info, out_str); + + *chafa_term_info_emit_begin_kitty_immediate_image_v1 (term_info, seq, + 32, + kitty_canvas->width, + kitty_canvas->height, + width_cells, + height_cells) = '\0'; + chafa_passthrough_encoder_append (&ptenc, seq); + chafa_passthrough_encoder_flush (&ptenc); + + build_image_chunks (kitty_canvas, &ptenc); + + chafa_passthrough_encoder_end (&ptenc); +} + +static gboolean +screen_is_wide_diacritic (gint diacritic_index) +{ + if (diacritic_index == 35 || diacritic_index == 61 || diacritic_index == 62) + return TRUE; + + return FALSE; +} + +static void +build_begin_row (ChafaTermInfo *term_info, GString *out_str, + gint width_cells, gint row, ChafaPassthrough passthrough) +{ + gchar seq [CHAFA_TERM_SEQ_LENGTH_MAX * 2 + 1]; + gchar *p0; + + if (row > 0) + { + /* Screen advances the cursor by one position too much for some of the + * diacritics. We compensate for the first few, since they will come up + * fairly frequently. We don't compensate for every single instance, + * since Screen only exhibits this behavior when printing and scrolling + * up in the current dpy, and not when scrolling down or redrawing after + * switching dpys, making the corrected graphics illegible in those + * cases. + * + * I.e. there's no perfect workaround here, so we try to make the common + * case look good and the uncommon case not terrible. + * + * Another option would've been to save/restore the cursor position + * between rows, but we don't want to clobber the register, as the CLI + * tool uses it to home the cursor between animation frames. It's + * also good policy in general to reserve it for client use. */ + + p0 = chafa_term_info_emit_cursor_left (term_info, seq, width_cells + + ((passthrough == CHAFA_PASSTHROUGH_SCREEN + && screen_is_wide_diacritic (row)) ? 1 : 0)); + p0 = chafa_term_info_emit_cursor_down_scroll (term_info, p0); + g_string_append_len (out_str, seq, p0 - seq); + } +} + +static void +build_unicode_placement (ChafaTermInfo *term_info, + GString *out_str, + gint width_cells, + gint height_cells, + gint placement_id, + ChafaPassthrough passthrough) +{ + gchar seq [CHAFA_TERM_SEQ_LENGTH_MAX * 2 + 1]; + gchar *p0; + gchar *row; + gint row_ofs; + gint i, j; + + g_assert (placement_id >= 1); + g_assert (placement_id <= 255); + + width_cells = MIN (width_cells, ENCODING_DIACRITIC_MAX - 1); + height_cells = MIN (height_cells, ENCODING_DIACRITIC_MAX - 1); + + row = g_malloc (width_cells * (6 * 3) + 1); + + for (i = 0; i < height_cells; i++) + { + /* Reposition after previous row */ + + build_begin_row (term_info, out_str, width_cells, i, passthrough); + + /* Encode the image ID in the foreground color */ + + p0 = chafa_term_info_emit_set_color_fg_256 (term_info, seq, placement_id); + g_string_append_len (out_str, seq, p0 - seq); + + /* Print the row */ + + row_ofs = 0; + + for (j = 0; j < width_cells; j++) + { + row_ofs += g_unichar_to_utf8 (ROWCOLUMN_UNICHAR, row + row_ofs); + + /* Screen has issues with some diacritics. We can compensate for this once + * per row, but doing it for every col is pushing it. So we omit all offsets + * except the row offsets in the first col. This harms overlapping images + * and horizontal scrolling, but oh well. */ + + if (passthrough != CHAFA_PASSTHROUGH_SCREEN || j == 0) + row_ofs += g_unichar_to_utf8 (encoding_diacritics [i], row + row_ofs); + if (passthrough != CHAFA_PASSTHROUGH_SCREEN) + row_ofs += g_unichar_to_utf8 (encoding_diacritics [j], row + row_ofs); + } + + g_string_append_len (out_str, row, row_ofs); + } + + /* Reset foreground color */ + + p0 = chafa_term_info_emit_reset_color_fg (term_info, seq); + g_string_append_len (out_str, seq, p0 - seq); + + g_free (row); +} + +static void +build_unicode_virtual (ChafaKittyCanvas *kitty_canvas, ChafaTermInfo *term_info, GString *out_str, + gint width_cells, gint height_cells, gint placement_id, + ChafaPassthrough passthrough) +{ + ChafaPassthroughEncoder ptenc; + gchar seq [CHAFA_TERM_SEQ_LENGTH_MAX + 1]; + + chafa_passthrough_encoder_begin (&ptenc, passthrough, term_info, out_str); + + *chafa_term_info_emit_begin_kitty_immediate_virt_image_v1 (term_info, seq, + 32, + kitty_canvas->width, + kitty_canvas->height, + width_cells, + height_cells, + placement_id) = '\0'; + chafa_passthrough_encoder_append (&ptenc, seq); + chafa_passthrough_encoder_reset (&ptenc); + end_passthrough (&ptenc); + + build_image_chunks (kitty_canvas, &ptenc); + + end_passthrough (&ptenc); + chafa_passthrough_encoder_end (&ptenc); + + build_unicode_placement (term_info, out_str, width_cells, height_cells, + placement_id, passthrough); +} + +void +chafa_kitty_canvas_build_ansi (ChafaKittyCanvas *kitty_canvas, + ChafaTermInfo *term_info, GString *out_str, + gint width_cells, gint height_cells, + gint placement_id, + ChafaPassthrough passthrough) +{ + if (passthrough == CHAFA_PASSTHROUGH_NONE) + { + build_immediate (kitty_canvas, term_info, out_str, + width_cells, height_cells); + } + else + { + /* Make IDs in the first <256 range predictable, but as the range + * cycles we add one to skip over every ID==0 */ + if (placement_id > 255) + placement_id = 1 + (placement_id % 255); + + build_unicode_virtual (kitty_canvas, term_info, out_str, + width_cells, height_cells, + placement_id, passthrough); + } } diff --git a/chafa/internal/chafa-kitty-canvas.h b/chafa/internal/chafa-kitty-canvas.h index 1b9dde5b..23317d2d 100644 --- a/chafa/internal/chafa-kitty-canvas.h +++ b/chafa/internal/chafa-kitty-canvas.h @@ -40,7 +40,9 @@ void chafa_kitty_canvas_draw_all_pixels (ChafaKittyCanvas *kitty_canvas, gint src_width, gint src_height, gint src_rowstride, ChafaColor bg_color); void chafa_kitty_canvas_build_ansi (ChafaKittyCanvas *kitty_canvas, ChafaTermInfo *term_info, GString *out_str, - gint width_cells, gint height_cells); + gint width_cells, gint height_cells, + gint placement_id, + ChafaPassthrough passthrough); G_END_DECLS diff --git a/chafa/internal/chafa-passthrough-encoder.c b/chafa/internal/chafa-passthrough-encoder.c new file mode 100644 index 00000000..32fb056e --- /dev/null +++ b/chafa/internal/chafa-passthrough-encoder.c @@ -0,0 +1,180 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2018-2023 Hans Petter Jansson + * + * This file is part of Chafa, a program that turns images into character art. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Chafa. If not, see . */ + +#include "config.h" + +#include "chafa.h" +#include "internal/chafa-passthrough-encoder.h" + +#define ESCAPE_BUF_SIZE 1024 + +static const gint packet_size_max [CHAFA_PASSTHROUGH_MAX] = +{ + /* Screen's OSC buffer size was increased to 2560 in bfb05c34ba1f961a15ccea04c5. + * This was quite a while ago, but it appears it still hasn't made its way into + * some of the important OS distributions. */ + [CHAFA_PASSTHROUGH_SCREEN] = 200, + [CHAFA_PASSTHROUGH_TMUX] = 1000000 +}; + +static void +append_begin (ChafaPassthroughEncoder *ptenc) +{ + gchar seq [CHAFA_TERM_SEQ_LENGTH_MAX + 1]; + + if (ptenc->mode == CHAFA_PASSTHROUGH_SCREEN) + { + *chafa_term_info_emit_begin_screen_passthrough (ptenc->term_info, seq) = '\0'; + g_string_append (ptenc->out, seq); + } + else if (ptenc->mode == CHAFA_PASSTHROUGH_TMUX) + { + *chafa_term_info_emit_begin_tmux_passthrough (ptenc->term_info, seq) = '\0'; + g_string_append (ptenc->out, seq); + } +} + +static void +append_end (ChafaPassthroughEncoder *ptenc) +{ + gchar seq [CHAFA_TERM_SEQ_LENGTH_MAX + 1]; + + if (ptenc->mode == CHAFA_PASSTHROUGH_SCREEN) + { + *chafa_term_info_emit_end_screen_passthrough (ptenc->term_info, seq) = '\0'; + g_string_append (ptenc->out, seq); + } + else if (ptenc->mode == CHAFA_PASSTHROUGH_TMUX) + { + *chafa_term_info_emit_end_tmux_passthrough (ptenc->term_info, seq) = '\0'; + g_string_append (ptenc->out, seq); + } +} + +static void +append_packetized (ChafaPassthroughEncoder *ptenc, const gchar *in, gint len) +{ + while (len > 0) + { + gssize remain = packet_size_max [ptenc->mode] - ptenc->packet_size; + gint n; + + if (remain == 0) + { + append_end (ptenc); + ptenc->packet_size = 0; + remain = packet_size_max [ptenc->mode]; + } + + if (ptenc->packet_size == 0) + { + append_begin (ptenc); + } + + n = MIN (len, remain); + + g_string_append_len (ptenc->out, in, n); + + len -= n; + ptenc->packet_size += n; + in += n; + } +} + +static void +append_escaped (ChafaPassthroughEncoder *ptenc, const gchar *in, gint len) +{ + gchar buf [ESCAPE_BUF_SIZE]; + gint i, j; + + for (i = 0, j = 0; i < len; i++) + { + buf [j++] = in [i]; + if (in [i] == 0x1b) + buf [j++] = 0x1b; + + if (j + 2 > ESCAPE_BUF_SIZE) + { + append_packetized (ptenc, buf, j); + j = 0; + } + } + + append_packetized (ptenc, buf, j); +} + +void +chafa_passthrough_encoder_begin (ChafaPassthroughEncoder *ptenc, + ChafaPassthrough passthrough, + ChafaTermInfo *term_info, GString *out_str) +{ + ptenc->mode = passthrough; + ptenc->term_info = term_info; + chafa_term_info_ref (term_info); + ptenc->out = out_str; + ptenc->packet_size = 0; +} + +void +chafa_passthrough_encoder_end (ChafaPassthroughEncoder *ptenc) +{ + if (ptenc->packet_size > 0) + chafa_passthrough_encoder_flush (ptenc); + + chafa_term_info_unref (ptenc->term_info); +} + +void +chafa_passthrough_encoder_append_len (ChafaPassthroughEncoder *ptenc, const gchar *in, gint len) +{ + if (ptenc->mode == CHAFA_PASSTHROUGH_NONE) + { + g_string_append_len (ptenc->out, in, len); + } + else if (ptenc->mode == CHAFA_PASSTHROUGH_SCREEN) + { + append_packetized (ptenc, in, len); + } + else + { + append_escaped (ptenc, in, len); + } +} + +void +chafa_passthrough_encoder_append (ChafaPassthroughEncoder *ptenc, const gchar *in) +{ + chafa_passthrough_encoder_append_len (ptenc, in, strlen (in)); +} + +void +chafa_passthrough_encoder_flush (ChafaPassthroughEncoder *ptenc) +{ + if (ptenc->packet_size > 0) + { + append_end (ptenc); + ptenc->packet_size = 0; + } +} + +void +chafa_passthrough_encoder_reset (ChafaPassthroughEncoder *ptenc) +{ + ptenc->packet_size = 0; +} diff --git a/chafa/internal/chafa-passthrough-encoder.h b/chafa/internal/chafa-passthrough-encoder.h new file mode 100644 index 00000000..d63625b4 --- /dev/null +++ b/chafa/internal/chafa-passthrough-encoder.h @@ -0,0 +1,53 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2018-2023 Hans Petter Jansson + * + * This file is part of Chafa, a program that turns images into character art. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Chafa. If not, see . */ + +#ifndef __CHAFA_PASSTHROUGH_ENCODER_H__ +#define __CHAFA_PASSTHROUGH_ENCODER_H__ + +#include "chafa.h" + +G_BEGIN_DECLS + +typedef struct +{ + ChafaPassthrough mode; + ChafaTermInfo *term_info; + GString *out; + gint packet_size; +} +ChafaPassthroughEncoder; + + +void chafa_passthrough_encoder_begin (ChafaPassthroughEncoder *ptenc, + ChafaPassthrough passthrough, + ChafaTermInfo *term_info, + GString *out_str); +void chafa_passthrough_encoder_end (ChafaPassthroughEncoder *ptenc); + +void chafa_passthrough_encoder_append (ChafaPassthroughEncoder *ptenc, + const gchar *in); +void chafa_passthrough_encoder_append_len (ChafaPassthroughEncoder *ptenc, + const gchar *in, + gint len); +void chafa_passthrough_encoder_flush (ChafaPassthroughEncoder *ptenc); +void chafa_passthrough_encoder_reset (ChafaPassthroughEncoder *ptenc); + +G_END_DECLS + +#endif /* __CHAFA_PASSTHROUGH_ENCODER_H__ */ diff --git a/chafa/internal/chafa-private.h b/chafa/internal/chafa-private.h index 841fe005..ec9158e1 100644 --- a/chafa/internal/chafa-private.h +++ b/chafa/internal/chafa-private.h @@ -113,6 +113,28 @@ struct ChafaCanvasConfig guint preprocessing_enabled : 1; guint fg_only_enabled : 1; ChafaOptimizations optimizations; + ChafaPassthrough passthrough; +}; + +/* Frame */ + +struct ChafaFrame +{ + gint refs; + ChafaPixelType pixel_type; + gint width, height, rowstride; + + gpointer data; + + guint data_is_owned : 1; +}; + +/* Image */ + +struct ChafaImage +{ + gint refs; + ChafaFrame *frame; }; /* Canvas */ diff --git a/chafa/internal/chafa-sixel-canvas.c b/chafa/internal/chafa-sixel-canvas.c index 5ab8374b..30acdf41 100644 --- a/chafa/internal/chafa-sixel-canvas.c +++ b/chafa/internal/chafa-sixel-canvas.c @@ -24,6 +24,7 @@ #include "internal/chafa-batch.h" #include "internal/chafa-bitfield.h" #include "internal/chafa-indexed-image.h" +#include "internal/chafa-passthrough-encoder.h" #include "internal/chafa-sixel-canvas.h" #include "internal/chafa-string-util.h" @@ -32,7 +33,7 @@ typedef struct { ChafaSixelCanvas *sixel_canvas; - GString *out_str; + ChafaPassthroughEncoder *ptenc; } BuildSixelsCtx; @@ -402,12 +403,12 @@ build_sixel_row_worker (ChafaBatchInfo *batch, const BuildSixelsCtx *ctx) static void build_sixel_row_post (ChafaBatchInfo *batch, BuildSixelsCtx *ctx) { - g_string_append_len (ctx->out_str, batch->ret_p, batch->ret_n); + chafa_passthrough_encoder_append_len (ctx->ptenc, batch->ret_p, batch->ret_n); g_free (batch->ret_p); } static void -build_sixel_palette (ChafaSixelCanvas *sixel_canvas, GString *out_str) +build_sixel_palette (ChafaSixelCanvas *sixel_canvas, ChafaPassthroughEncoder *ptenc) { gchar str [256 * 20 + 1]; gchar *p = str; @@ -440,20 +441,62 @@ build_sixel_palette (ChafaSixelCanvas *sixel_canvas, GString *out_str) p = chafa_format_dec_u8 (p, (col->ch [2] * 100) / 255); } - g_string_append_len (out_str, str, p - str); + chafa_passthrough_encoder_append_len (ptenc, str, p - str); +} + +static void +end_sixels (ChafaPassthroughEncoder *ptenc, ChafaTermInfo *term_info) +{ + gchar buf [CHAFA_TERM_SEQ_LENGTH_MAX + 1]; + gint i; + + *chafa_term_info_emit_end_sixels (term_info, buf) = '\0'; + + if (ptenc->mode == CHAFA_PASSTHROUGH_SCREEN) + { + /* In GNU Screen, the end of an emitted sixel passthrough sequence should + * look something like this: \e P \e \e \\ \e P \\ \e \\ */ + + for (i = 0; buf [i]; i++) + { + chafa_passthrough_encoder_flush (ptenc); + chafa_passthrough_encoder_append_len (ptenc, buf + i, 1); + } + } + else + { + chafa_passthrough_encoder_append (ptenc, buf); + } + + chafa_passthrough_encoder_flush (ptenc); } void -chafa_sixel_canvas_build_ansi (ChafaSixelCanvas *sixel_canvas, GString *out_str) +chafa_sixel_canvas_build_ansi (ChafaSixelCanvas *sixel_canvas, ChafaTermInfo *term_info, + GString *str, ChafaPassthrough passthrough) { + ChafaPassthroughEncoder ptenc; BuildSixelsCtx ctx; + gchar buf [CHAFA_TERM_SEQ_LENGTH_MAX + 1]; g_assert (sixel_canvas->image->height % SIXEL_CELL_HEIGHT == 0); + chafa_passthrough_encoder_begin (&ptenc, passthrough, term_info, str); + + *chafa_term_info_emit_begin_sixels (term_info, buf, 0, 0, 0) = '\0'; + chafa_passthrough_encoder_append (&ptenc, buf); + + g_snprintf (buf, + CHAFA_TERM_SEQ_LENGTH_MAX, + "\"1;1;%d;%d", + sixel_canvas->image->width, + sixel_canvas->image->height); + chafa_passthrough_encoder_append (&ptenc, buf); + ctx.sixel_canvas = sixel_canvas; - ctx.out_str = out_str; + ctx.ptenc = &ptenc; - build_sixel_palette (sixel_canvas, out_str); + build_sixel_palette (sixel_canvas, &ptenc); chafa_process_batches (&ctx, (GFunc) build_sixel_row_worker, @@ -461,4 +504,7 @@ chafa_sixel_canvas_build_ansi (ChafaSixelCanvas *sixel_canvas, GString *out_str) sixel_canvas->image->height, chafa_get_n_actual_threads (), SIXEL_CELL_HEIGHT); + + end_sixels (&ptenc, term_info); + chafa_passthrough_encoder_end (&ptenc); } diff --git a/chafa/internal/chafa-sixel-canvas.h b/chafa/internal/chafa-sixel-canvas.h index 72a698e3..a6d5d2fa 100644 --- a/chafa/internal/chafa-sixel-canvas.h +++ b/chafa/internal/chafa-sixel-canvas.h @@ -41,7 +41,8 @@ void chafa_sixel_canvas_destroy (ChafaSixelCanvas *sixel_canvas); void chafa_sixel_canvas_draw_all_pixels (ChafaSixelCanvas *sixel_canvas, ChafaPixelType src_pixel_type, gconstpointer src_pixels, gint src_width, gint src_height, gint src_rowstride); -void chafa_sixel_canvas_build_ansi (ChafaSixelCanvas *sixel_canvas, GString *out_str); +void chafa_sixel_canvas_build_ansi (ChafaSixelCanvas *sixel_canvas, ChafaTermInfo *term_info, + GString *out_str, ChafaPassthrough passthrough); G_END_DECLS diff --git a/docs/chafa-docs.xml b/docs/chafa-docs.xml index 5cb4daf8..a018ed53 100644 --- a/docs/chafa-docs.xml +++ b/docs/chafa-docs.xml @@ -27,6 +27,8 @@ Chafa C API + + diff --git a/docs/chafa-sections.txt b/docs/chafa-sections.txt index c3429107..58ce810d 100644 --- a/docs/chafa-sections.txt +++ b/docs/chafa-sections.txt @@ -6,8 +6,11 @@ chafa_canvas_new_similar chafa_canvas_ref chafa_canvas_unref chafa_canvas_peek_config +chafa_canvas_set_image chafa_canvas_draw_all_pixels chafa_canvas_print +chafa_canvas_print_rows +chafa_canvas_print_rows_strv chafa_canvas_get_char_at chafa_canvas_set_char_at chafa_canvas_get_colors_at @@ -26,6 +29,7 @@ ChafaCanvasMode ChafaDitherMode ChafaColorExtractor ChafaOptimizations +ChafaPassthrough ChafaCanvasConfig chafa_canvas_config_new chafa_canvas_config_copy @@ -67,6 +71,8 @@ chafa_canvas_config_get_dither_intensity chafa_canvas_config_set_dither_intensity chafa_canvas_config_get_optimizations chafa_canvas_config_set_optimizations +chafa_canvas_config_get_passthrough +chafa_canvas_config_set_passthrough
@@ -112,6 +118,7 @@ CHAFA_VERSION_1_6 CHAFA_VERSION_1_8 CHAFA_VERSION_1_10 CHAFA_VERSION_1_12 +CHAFA_VERSION_1_14 ChafaPixelType chafa_calc_canvas_geometry
@@ -130,6 +137,8 @@ chafa_term_info_unref chafa_term_info_get_seq chafa_term_info_set_seq chafa_term_info_have_seq +chafa_term_info_emit_seq +chafa_term_info_parse_seq chafa_term_info_supplement chafa_term_info_emit_reset_terminal_soft chafa_term_info_emit_reset_terminal_hard @@ -146,7 +155,6 @@ chafa_term_info_emit_cursor_up_1 chafa_term_info_emit_cursor_down_1 chafa_term_info_emit_cursor_left_1 chafa_term_info_emit_cursor_right_1 -chafa_term_info_emit_set_scrolling_rows chafa_term_info_emit_cursor_up_scroll chafa_term_info_emit_cursor_down_scroll chafa_term_info_emit_insert_cells @@ -175,17 +183,109 @@ chafa_term_info_emit_set_color_fgbg_256 chafa_term_info_emit_set_color_fg_direct chafa_term_info_emit_set_color_bg_direct chafa_term_info_emit_set_color_fgbg_direct +chafa_term_info_emit_reset_color_fg +chafa_term_info_emit_reset_color_bg +chafa_term_info_emit_reset_color_fgbg +chafa_term_info_emit_set_default_fg +chafa_term_info_emit_set_default_bg +chafa_term_info_emit_reset_default_fg +chafa_term_info_emit_reset_default_bg +chafa_term_info_emit_query_default_fg +chafa_term_info_emit_query_default_bg chafa_term_info_emit_repeat_char +chafa_term_info_emit_set_scrolling_rows +chafa_term_info_emit_reset_scrolling_rows +chafa_term_info_emit_save_cursor_pos +chafa_term_info_emit_restore_cursor_pos chafa_term_info_emit_begin_sixels chafa_term_info_emit_end_sixels chafa_term_info_emit_enable_sixel_scrolling chafa_term_info_emit_disable_sixel_scrolling +chafa_term_info_emit_set_sixel_advance_down +chafa_term_info_emit_set_sixel_advance_right chafa_term_info_emit_begin_kitty_immediate_image_v1 +chafa_term_info_emit_begin_kitty_immediate_virt_image_v1 chafa_term_info_emit_end_kitty_image chafa_term_info_emit_begin_kitty_image_chunk chafa_term_info_emit_end_kitty_image_chunk chafa_term_info_emit_begin_iterm2_image chafa_term_info_emit_end_iterm2_image +chafa_term_info_emit_begin_screen_passthrough +chafa_term_info_emit_end_screen_passthrough +chafa_term_info_emit_enable_alt_screen +chafa_term_info_emit_disable_alt_screen +chafa_term_info_emit_begin_tmux_passthrough +chafa_term_info_emit_end_tmux_passthrough +chafa_term_info_emit_return_key +chafa_term_info_emit_backspace_key +chafa_term_info_emit_delete_key +chafa_term_info_emit_delete_ctrl_key +chafa_term_info_emit_delete_shift_key +chafa_term_info_emit_insert_key +chafa_term_info_emit_insert_ctrl_key +chafa_term_info_emit_insert_shift_key +chafa_term_info_emit_home_key +chafa_term_info_emit_home_ctrl_key +chafa_term_info_emit_home_shift_key +chafa_term_info_emit_end_key +chafa_term_info_emit_end_ctrl_key +chafa_term_info_emit_end_shift_key +chafa_term_info_emit_up_key +chafa_term_info_emit_up_ctrl_key +chafa_term_info_emit_up_shift_key +chafa_term_info_emit_down_key +chafa_term_info_emit_down_ctrl_key +chafa_term_info_emit_down_shift_key +chafa_term_info_emit_left_key +chafa_term_info_emit_left_ctrl_key +chafa_term_info_emit_left_shift_key +chafa_term_info_emit_right_key +chafa_term_info_emit_right_ctrl_key +chafa_term_info_emit_right_shift_key +chafa_term_info_emit_page_up_key +chafa_term_info_emit_page_up_ctrl_key +chafa_term_info_emit_page_up_shift_key +chafa_term_info_emit_page_down_key +chafa_term_info_emit_page_down_ctrl_key +chafa_term_info_emit_page_down_shift_key +chafa_term_info_emit_tab_key +chafa_term_info_emit_tab_shift_key +chafa_term_info_emit_f1_key +chafa_term_info_emit_f1_ctrl_key +chafa_term_info_emit_f1_shift_key +chafa_term_info_emit_f2_key +chafa_term_info_emit_f2_ctrl_key +chafa_term_info_emit_f2_shift_key +chafa_term_info_emit_f3_key +chafa_term_info_emit_f3_ctrl_key +chafa_term_info_emit_f3_shift_key +chafa_term_info_emit_f4_key +chafa_term_info_emit_f4_ctrl_key +chafa_term_info_emit_f4_shift_key +chafa_term_info_emit_f5_key +chafa_term_info_emit_f5_ctrl_key +chafa_term_info_emit_f5_shift_key +chafa_term_info_emit_f6_key +chafa_term_info_emit_f6_ctrl_key +chafa_term_info_emit_f6_shift_key +chafa_term_info_emit_f7_key +chafa_term_info_emit_f7_ctrl_key +chafa_term_info_emit_f7_shift_key +chafa_term_info_emit_f8_key +chafa_term_info_emit_f8_ctrl_key +chafa_term_info_emit_f8_shift_key +chafa_term_info_emit_f9_key +chafa_term_info_emit_f9_ctrl_key +chafa_term_info_emit_f9_shift_key +chafa_term_info_emit_f10_key +chafa_term_info_emit_f10_ctrl_key +chafa_term_info_emit_f10_shift_key +chafa_term_info_emit_f11_key +chafa_term_info_emit_f11_ctrl_key +chafa_term_info_emit_f11_shift_key +chafa_term_info_emit_f12_key +chafa_term_info_emit_f12_ctrl_key +chafa_term_info_emit_f12_shift_key
@@ -199,3 +299,22 @@ chafa_term_db_get_default chafa_term_db_detect chafa_term_db_get_fallback_info
+ +
+chafa-image +ChafaImage +chafa_image_new +chafa_image_ref +chafa_image_unref +chafa_image_set_frame +
+ +
+chafa-frame +ChafaFrame +chafa_frame_new +chafa_frame_new_borrow +chafa_frame_new_steal +chafa_frame_ref +chafa_frame_unref +
diff --git a/docs/chafa.xml b/docs/chafa.xml index 8f335870..66bdc68c 100644 --- a/docs/chafa.xml +++ b/docs/chafa.xml @@ -267,6 +267,17 @@ for when used with "-c none", where it defaults to 0. + + + +Graphics protocol passthrough [auto, none, screen, tmux]. Used to show pixel +graphics from within multiplexers. Defaults to auto, which will enable +passthrough if the Kitty terminal is detected along with one of the supported +multiplexers. Other combinations must be enabled manually; use with the -f +option to select the appropriate graphics protocol. + + + diff --git a/tools/chafa/Makefile.am b/tools/chafa/Makefile.am index fa9ebb66..2350466e 100644 --- a/tools/chafa/Makefile.am +++ b/tools/chafa/Makefile.am @@ -13,6 +13,8 @@ chafa_SOURCES = \ gif-loader.h \ media-loader.c \ media-loader.h \ + placement-counter.c \ + placement-counter.h \ png-loader.c \ png-loader.h \ named-colors.c \ diff --git a/tools/chafa/chafa.c b/tools/chafa/chafa.c index 3512f684..24c4fe0d 100644 --- a/tools/chafa/chafa.c +++ b/tools/chafa/chafa.c @@ -43,6 +43,7 @@ #include "font-loader.h" #include "media-loader.h" #include "named-colors.h" +#include "placement-counter.h" /* Include after glib.h for G_OS_WIN32 */ #ifdef G_OS_WIN32 @@ -106,6 +107,8 @@ typedef struct gint optimization_level; gint n_threads; ChafaOptimizations optimizations; + ChafaPassthrough passthrough; + gboolean passthrough_set; guint32 fg_color; gboolean fg_color_set; guint32 bg_color; @@ -141,6 +144,8 @@ static TermSize detected_term_size; static gboolean using_detected_size = FALSE; static volatile sig_atomic_t interrupted_by_user = FALSE; +static PlacementCounter *placement_counter; + #ifdef HAVE_TERMIOS_H static struct termios saved_termios; #endif @@ -471,6 +476,8 @@ print_summary (void) " intelligently [0-9]. 0 disables, 9 enables every\n" " available optimization. Defaults to 5, except for when\n" " used with \"-c none\", where it defaults to 0.\n" + " --passthrough=MODE Graphics protocol passthrough [auto, none, screen,\n" + " tmux]. Used to show pixel graphics through multiplexers.\n" " --polite=BOOL Polite mode [on, off]. Defaults to on. Turning this off\n" " may enhance presentation and prevent interference from\n" " other programs, but risks leaving the terminal in an\n" @@ -910,6 +917,34 @@ parse_dither_grain_arg (G_GNUC_UNUSED const gchar *option_name, const gchar *val return result; } +static gboolean +parse_passthrough_arg (G_GNUC_UNUSED const gchar *option_name, const gchar *value, G_GNUC_UNUSED gpointer data, GError **error) +{ + gboolean result = TRUE; + + options.passthrough_set = TRUE; + + if (!g_ascii_strcasecmp (value, "none")) + options.passthrough = CHAFA_PASSTHROUGH_NONE; + else if (!g_ascii_strcasecmp (value, "screen")) + options.passthrough = CHAFA_PASSTHROUGH_SCREEN; + else if (!g_ascii_strcasecmp (value, "tmux")) + options.passthrough = CHAFA_PASSTHROUGH_TMUX; + else if (!g_ascii_strcasecmp (value, "auto")) + { + options.passthrough = CHAFA_PASSTHROUGH_NONE; + options.passthrough_set = FALSE; + } + else + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Passthrough must be one of [auto, none, screen, tmux]."); + result = FALSE; + } + + return result; +} + static gboolean parse_glyph_file_arg (G_GNUC_UNUSED const gchar *option_name, const gchar *value, G_GNUC_UNUSED gpointer data, GError **error) { @@ -1440,10 +1475,12 @@ tty_options_deinit (void) } static void -detect_terminal (ChafaTermInfo **term_info_out, ChafaCanvasMode *mode_out, ChafaPixelMode *pixel_mode_out) +detect_terminal (ChafaTermInfo **term_info_out, ChafaCanvasMode *mode_out, + ChafaPixelMode *pixel_mode_out, ChafaPassthrough *passthrough_out) { ChafaCanvasMode mode; ChafaPixelMode pixel_mode; + ChafaPassthrough passthrough; ChafaTermInfo *term_info; ChafaTermInfo *fallback_info; gchar **envp; @@ -1477,6 +1514,29 @@ detect_terminal (ChafaTermInfo **term_info_out, ChafaCanvasMode *mode_out, Chafa else pixel_mode = CHAFA_PIXEL_MODE_SYMBOLS; + if (chafa_term_info_have_seq (term_info, CHAFA_TERM_SEQ_BEGIN_SCREEN_PASSTHROUGH)) + { + /* We can do passthrough for sixels and iterm too, but we won't do so + * automatically, since they'll break with inner TE updates. */ + if (pixel_mode != CHAFA_PIXEL_MODE_KITTY) + pixel_mode = CHAFA_PIXEL_MODE_SYMBOLS; + + passthrough = CHAFA_PASSTHROUGH_SCREEN; + } + else if (chafa_term_info_have_seq (term_info, CHAFA_TERM_SEQ_BEGIN_TMUX_PASSTHROUGH)) + { + /* We can do passthrough for sixels and iterm too, but we won't do so + * automatically, since they'll break with inner TE updates. */ + if (pixel_mode != CHAFA_PIXEL_MODE_KITTY) + pixel_mode = CHAFA_PIXEL_MODE_SYMBOLS; + + passthrough = CHAFA_PASSTHROUGH_TMUX; + } + else + { + passthrough = CHAFA_PASSTHROUGH_NONE; + } + /* Make sure we have fallback sequences in case the user forces * a mode that's technically unsupported by the terminal. */ fallback_info = chafa_term_db_get_fallback_info (chafa_term_db_get_default ()); @@ -1486,10 +1546,103 @@ detect_terminal (ChafaTermInfo **term_info_out, ChafaCanvasMode *mode_out, Chafa *term_info_out = term_info; *mode_out = mode; *pixel_mode_out = pixel_mode; + *passthrough_out = passthrough; g_strfreev (envp); } +static gchar *tmux_allow_passthrough_original; +static gboolean tmux_allow_passthrough_is_changed; + +static gboolean +apply_passthrough_workarounds_tmux (void) +{ + gboolean result = FALSE; + gchar *standard_output = NULL; + gchar *standard_error = NULL; + gchar **strings; + gchar *mode = NULL; + gint wait_status = -1; + + /* allow-passthrough can be either unset, "on" or "all". Both "on" and "all" + * are fine, so don't mess with it if we don't have to. + * + * Also note that we may be in a remote session inside tmux. */ + + if (!g_spawn_command_line_sync ("tmux show allow-passthrough", + &standard_output, &standard_error, + &wait_status, NULL)) + goto out; + + strings = g_strsplit_set (standard_output, " ", -1); + if (strings [0] && strings [1]) + mode = g_ascii_strdown (strings [1], -1); + g_strfreev (strings); + + g_free (standard_output); + standard_output = NULL; + g_free (standard_error); + standard_error = NULL; + + if (!mode || (strcmp (mode, "on") && strcmp (mode, "all"))) + { + result = g_spawn_command_line_sync ("tmux set-option allow-passthrough on", + &standard_output, &standard_error, + &wait_status, NULL); + if (result) + { + tmux_allow_passthrough_original = mode; + tmux_allow_passthrough_is_changed = TRUE; + } + } + else + { + g_free (mode); + } + +out: + g_free (standard_output); + g_free (standard_error); + return result; +} + +static gboolean +retire_passthrough_workarounds_tmux (void) +{ + gboolean result = FALSE; + gchar *standard_output = NULL; + gchar *standard_error = NULL; + gchar **strings; + gchar *cmd; + gint wait_status = -1; + + if (!tmux_allow_passthrough_is_changed) + return TRUE; + + if (tmux_allow_passthrough_original) + { + cmd = g_strdup_printf ("tmux set-option allow-passthrough %s", + tmux_allow_passthrough_original); + } + else + { + cmd = g_strdup ("tmux set-option -u allow-passthrough"); + } + + result = g_spawn_command_line_sync (cmd, &standard_output, &standard_error, + &wait_status, NULL); + if (result) + { + g_free (tmux_allow_passthrough_original); + tmux_allow_passthrough_original = NULL; + tmux_allow_passthrough_is_changed = FALSE; + } + + g_free (standard_output); + g_free (standard_error); + return result; +} + static gboolean parse_options (int *argc, char **argv []) { @@ -1526,6 +1679,7 @@ parse_options (int *argc, char **argv []) { "margin-bottom", '\0', 0, G_OPTION_ARG_INT, &options.margin_bottom, "Bottom margin", NULL }, { "margin-right", '\0', 0, G_OPTION_ARG_INT, &options.margin_right, "Right margin", NULL }, { "optimize", 'O', 0, G_OPTION_ARG_INT, &options.optimization_level, "Optimization", NULL }, + { "passthrough", '\0', 0, G_OPTION_ARG_CALLBACK, parse_passthrough_arg, "Passthrough", NULL }, { "polite", '\0', 0, G_OPTION_ARG_CALLBACK, parse_polite_arg, "Polite", NULL }, { "preprocess", 'p', 0, G_OPTION_ARG_CALLBACK, parse_preprocess_arg, "Preprocessing", NULL }, { "relative", '\0', 0, G_OPTION_ARG_CALLBACK, parse_relative_arg, "Relative", NULL }, @@ -1545,6 +1699,7 @@ parse_options (int *argc, char **argv []) }; ChafaCanvasMode canvas_mode; ChafaPixelMode pixel_mode; + ChafaPassthrough passthrough; memset (&options, 0, sizeof (options)); @@ -1572,7 +1727,8 @@ parse_options (int *argc, char **argv []) options.fill_symbol_map = chafa_symbol_map_new (); options.is_interactive = isatty (STDIN_FILENO) && isatty (STDOUT_FILENO); - detect_terminal (&options.term_info, &canvas_mode, &pixel_mode); + detect_terminal (&options.term_info, &canvas_mode, &pixel_mode, &passthrough); + options.mode = CHAFA_CANVAS_MODE_MAX; /* Unset */ options.pixel_mode = pixel_mode; options.dither_mode = CHAFA_DITHER_MODE_NONE; @@ -1779,6 +1935,17 @@ parse_options (int *argc, char **argv []) options.scale = 1.0; } + /* Apply detected passthrough if auto */ + if (!options.passthrough_set) + options.passthrough = passthrough; + + /* Apply tmux workarounds only if both detected and desired, and a + * graphics protocol is desired */ + if (passthrough == CHAFA_PASSTHROUGH_TMUX + && options.passthrough == CHAFA_PASSTHROUGH_TMUX + && options.pixel_mode != CHAFA_PIXEL_MODE_SYMBOLS) + apply_passthrough_workarounds_tmux (); + if (options.work_factor < 1 || options.work_factor > 9) { g_printerr ("%s: Work factor must be in the range [1-9].\n", options.executable_name); @@ -2157,10 +2324,13 @@ static GString ** build_strings (ChafaPixelType pixel_type, const guint8 *pixels, gint src_width, gint src_height, gint src_rowstride, gint dest_width, gint dest_height, - gboolean is_animation) + gboolean is_animation, + gint placement_id) { ChafaCanvasConfig *config; ChafaCanvas *canvas; + ChafaFrame *frame; + ChafaImage *image; GString **gsa; config = chafa_canvas_config_new (); @@ -2177,6 +2347,7 @@ build_strings (ChafaPixelType pixel_type, const guint8 *pixels, chafa_canvas_config_set_bg_color (config, options.bg_color); chafa_canvas_config_set_preprocessing_enabled (config, options.preprocess); chafa_canvas_config_set_fg_only_enabled (config, options.fg_only); + chafa_canvas_config_set_passthrough (config, options.passthrough); if (is_animation && options.pixel_mode == CHAFA_PIXEL_MODE_KITTY @@ -2198,9 +2369,16 @@ build_strings (ChafaPixelType pixel_type, const guint8 *pixels, chafa_canvas_config_set_optimizations (config, options.optimizations); canvas = chafa_canvas_new (config); - chafa_canvas_draw_all_pixels (canvas, pixel_type, pixels, src_width, src_height, src_rowstride); + frame = chafa_frame_new_borrow ((gpointer) pixels, pixel_type, + src_width, src_height, src_rowstride); + image = chafa_image_new (); + chafa_image_set_frame (image, frame); + chafa_canvas_set_image (canvas, image, placement_id); + chafa_canvas_print_rows (canvas, options.term_info, &gsa, NULL); + chafa_image_unref (image); + chafa_frame_unref (frame); chafa_canvas_unref (canvas); chafa_canvas_config_unref (config); return gsa; @@ -2252,6 +2430,8 @@ run_generic (const gchar *filename, gboolean is_first_file, gboolean is_first_fr gint loop_n = 0; MediaLoader *media_loader; GString **gsa; + gint placement_id = -1; + gint frame_count = 0; RunResult result = FILE_FAILED; GError *error = NULL; @@ -2271,6 +2451,15 @@ run_generic (const gchar *filename, gboolean is_first_file, gboolean is_first_fr if (interrupted_by_user) goto out; + if (options.pixel_mode == CHAFA_PIXEL_MODE_KITTY + && options.passthrough != CHAFA_PASSTHROUGH_NONE) + { + if (!placement_counter) + placement_counter = placement_counter_new (); + + placement_id = placement_counter_get_next_id (placement_counter); + } + is_animation = options.animate ? media_loader_get_is_animation (media_loader) : FALSE; result = is_animation ? FILE_WAS_ANIMATION : FILE_WAS_STILL; @@ -2339,7 +2528,8 @@ run_generic (const gchar *filename, gboolean is_first_file, gboolean is_first_fr gsa = build_strings (pixel_type, pixels, src_width, src_height, src_rowstride, dest_width, dest_height, - is_animation); + is_animation, + placement_id >= 0 ? placement_id + ((frame_count++) % 2) : -1); if (!write_image_prologue (is_first_file, is_first_frame, is_animation, dest_height) || !write_image (gsa, dest_width) @@ -2379,6 +2569,12 @@ run_generic (const gchar *filename, gboolean is_first_file, gboolean is_first_fr && !options.watch && anim_elapsed_s < anim_duration_s); out: + /* We need two IDs per animation in order to do flicker-free flips. If the + * final frame got the higher ID, increment the global counter so the next + * image doesn't clobber it. */ + if (placement_id >= 0 && !(frame_count % 2)) + placement_id = placement_counter_get_next_id (placement_counter); + if (media_loader) media_loader_destroy (media_loader); g_timer_destroy (timer); @@ -2515,6 +2711,11 @@ main (int argc, char *argv []) ? run_watch (options.args->data) : run_all (options.args); + retire_passthrough_workarounds_tmux (); + + if (placement_counter) + placement_counter_destroy (placement_counter); + if (options.symbol_map) chafa_symbol_map_unref (options.symbol_map); if (options.fill_symbol_map) diff --git a/tools/chafa/placement-counter.c b/tools/chafa/placement-counter.c new file mode 100644 index 00000000..7814d2a2 --- /dev/null +++ b/tools/chafa/placement-counter.c @@ -0,0 +1,100 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2018-2023 Hans Petter Jansson + * + * This file is part of Chafa, a program that turns images into character art. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Chafa. If not, see . */ + +#include "config.h" +#include +#include + +#include +#include "placement-counter.h" + +#define BUF_SIZE 256 + +struct PlacementCounter +{ + guint id; +}; + +static void +save_id (PlacementCounter *counter) +{ + gchar *path = g_build_path (G_DIR_SEPARATOR_S, g_get_user_cache_dir (), "chafa", "placement-id", NULL); + gchar buf [BUF_SIZE]; + + snprintf (buf, BUF_SIZE, "%u\n", counter->id); + g_file_set_contents (path, buf, -1, NULL); + + g_free (path); +} + +static void +restore_id (PlacementCounter *counter) +{ + gchar *path = g_build_path (G_DIR_SEPARATOR_S, g_get_user_cache_dir (), "chafa", "placement-id", NULL); + gchar *buf = NULL; + gsize length; + + g_file_get_contents (path, &buf, &length, NULL); + + if (buf && length > 0) + { + counter->id = strtoul (buf, NULL, 10); + if (counter->id < 1) + counter->id = 1; + } + + g_free (buf); + g_free (path); +} + +static void +ensure_id_storage (void) +{ + gchar *path = g_build_path (G_DIR_SEPARATOR_S, g_get_user_cache_dir (), "chafa", NULL); + g_mkdir_with_parents (path, 0750); + g_free (path); +} + +PlacementCounter * +placement_counter_new (void) +{ + PlacementCounter *counter; + + counter = g_new0 (PlacementCounter, 1); + counter->id = 0; + + ensure_id_storage (); + restore_id (counter); + + return counter; +} + +void +placement_counter_destroy (PlacementCounter *counter) +{ + save_id (counter); + g_free (counter); +} + +guint +placement_counter_get_next_id (PlacementCounter *counter) +{ + counter->id = 1 + (counter->id % 65535); + return counter->id; +} diff --git a/tools/chafa/placement-counter.h b/tools/chafa/placement-counter.h new file mode 100644 index 00000000..a315be6a --- /dev/null +++ b/tools/chafa/placement-counter.h @@ -0,0 +1,36 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Copyright (C) 2018-2023 Hans Petter Jansson + * + * This file is part of Chafa, a program that turns images into character art. + * + * Chafa is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chafa is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Chafa. If not, see . */ + +#ifndef __PLACEMENT_COUNTER_H__ +#define __PLACEMENT_COUNTER_H__ + +#include + +G_BEGIN_DECLS + +typedef struct PlacementCounter PlacementCounter; + +PlacementCounter *placement_counter_new (void); +void placement_counter_destroy (PlacementCounter *counter); + +guint placement_counter_get_next_id (PlacementCounter *counter); + +G_END_DECLS + +#endif /* __PLACEMENT_COUNTER_H__ */