From d63eeada73bacce2374c6b1f1523e0551dbbd5b6 Mon Sep 17 00:00:00 2001 From: Sergei Grechanik Date: Sat, 29 Oct 2022 19:48:06 -0700 Subject: [PATCH] Image placement using Unicode placeholders This commit introduces the Unicode placeholder image placement method. In particular: - Virtual placements can be created by passing `U=1` in a put command. - Images with virtual placements can be displayed using the placeholder character `U+10EEEE` with diacritics indicating rows and columns. - The image ID is indicated by the foreground color of the placeholder. Additionally, the most significant byte of the ID can be specified via the third diacritic. - Underline color can be optionally used to specify the placement ID. - A bug was fixed, which caused incomplete image removal when it was overwritten by another image with the same ID. --- .gitattributes | 1 + docs/changelog.rst | 2 + docs/graphics-protocol.rst | 81 +++++++++ gen-apc-parsers.py | 1 + gen-wcwidth.py | 47 +++++ kitty/data-types.h | 4 + kitty/fast_data_types.pyi | 1 + kitty/fonts.c | 1 + kitty/graphics.c | 228 +++++++++++++++++++++++- kitty/graphics.h | 12 +- kitty/history.c | 5 + kitty/line-buf.c | 5 + kitty/lineops.h | 2 + kitty/parse-graphics-command.h | 14 +- kitty/rowcolumn-diacritics.c | 187 ++++++++++++++++++++ kitty/screen.c | 163 +++++++++++++++++ kitty/unicode-data.h | 3 + kitty_tests/graphics.py | 236 ++++++++++++++++++++++++- kitty_tests/parser.py | 2 +- rowcolumn-diacritics.txt | 310 +++++++++++++++++++++++++++++++++ 20 files changed, 1288 insertions(+), 17 deletions(-) create mode 100644 kitty/rowcolumn-diacritics.c create mode 100644 rowcolumn-diacritics.txt diff --git a/.gitattributes b/.gitattributes index 676400be0..a1b6b1d99 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,7 @@ kitty/emoji.h linguist-generated=true kitty/charsets.c linguist-generated=true kitty/key_encoding.py linguist-generated=true kitty/unicode-data.c linguist-generated=true +kitty/rowcolumn-diacritics.c linguist-generated=true kitty/rgb.py linguist-generated=true kitty/srgb_gamma.c linguist-generated=true kitty/gl-wrapper.* linguist-generated=true diff --git a/docs/changelog.rst b/docs/changelog.rst index 463149449..f717aaef2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -146,6 +146,8 @@ Detailed list of changes - macOS: Export kitty selected text to the system for use with services that accept it (patch by Sertaç Ö. Yıldız) +- Image placement using Unicode placeholders (:pull:`5664`) + 0.26.5 [2022-11-07] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/graphics-protocol.rst b/docs/graphics-protocol.rst index 363e31f56..2979501a7 100644 --- a/docs/graphics-protocol.rst +++ b/docs/graphics-protocol.rst @@ -472,6 +472,87 @@ z-index and the same id, then the behavior is undefined. Support for the C=1 cursor movement policy +Unicode placeholders +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 0.27.0 + Unicode placeholders + +You can also use a special Unicode character ``U+10EEEE`` as a placeholder for +an image. This approach is less flexible, but it works for any application that +supports Unicode and foreground colors (tmux, vim, etc). To use it, you need to +create a virtual image placement by specifying ``U=1`` and the desirable number +of lines and columns:: + + _Ga=p,U=1,i=,c=,r=\ + +The image will be fit to the specified rectangle, its aspect ratio preserved (in +the future more scaling modes may be added to the protocol). Now you can display +the image using the placeholder character, encoding the image ID in its +foreground color. The row and column values are specified with diacritics listed +in ``rowcolumn-diacritics.txt``. For example, here is how you can print a 2x2 +placeholder for image ID 42:: + + printf "\e[38;5;42m\U10EEEE\U0305\U0305\U10EEEE\U0305\U030D\n" + printf "\e[38;5;42m\U10EEEE\U030D\U0305\U10EEEE\U030D\U030D\n" + +By using only the foreground color you are limited to 8-bit IDs in 256 color +mode and to 24-bit IDs in true color mode. If you need more bits for the image +ID, you can specify the most significant byte via the third diacritic. For +example, this is the placeholder for the image ID ``738197504 = 42 + 2 << 24``:: + + printf "\e[38;5;42m\U10EEEE\U0305\U0305\U030E\U10EEEE\U0305\U030D\U030E\n" + printf "\e[38;5;42m\U10EEEE\U030D\U0305\U030E\U10EEEE\U030D\U030D\U030E\n" + +You can also specify a placement ID using the underline color (if it's omitted +or zero, the terminal may choose any virtual placement of the given image). The +background color is interpreted as the background color, visible if the image is +transparent. Other text attributes are reserved for future use. + +Row, column and most significant byte diacritics may also be omitted, in which +case the placeholder cell will inherit the missing values from the placeholder +cell to the left: + +- If no diacritics are present, and the previous placeholder cell has the same + foreground and underline colors, then the row of the current cell will be the + row of the cell to the left, the column will be the column of the cell to the + left plus one, and the most significant image ID byte will be the most + significant image ID byte of the cell to the left. +- If only the row diacritic is present, and the previous placeholder cell has + the same row and the same foreground and underline colors, then the column of + the current cell will be the column of the cell to the left plus one, and the + most significant image ID byte will be the most significant image ID byte of + the cell to the left. +- If only the row and column diacritics are present, and the previous + placeholder cell has the same row, the same foreground and underline colors, + and its column is one less than the current column, then the most significant + image ID byte of the current cell will be the most significant image ID byte + of the cell to the left. + +These rules are applied left-to-right, which allows specifying only row +diacritics of the first column, i.e. here is a 2 rows by 3 columns placeholder:: + + printf "\e[38;5;42m\U10EEEE\U0305\U10EEEE\U10EEEE\n" + printf "\e[38;5;42m\U10EEEE\U030D\U10EEEE\U10EEEE\n" + +This will not work for horizontal scrolling and overlapping images since the two +given rules will fail to guess the missing information. In such cases, the +terminal may apply other heuristics (but it doesn't have to). + +It is important to distinguish between virtual image placements and real images +displayed on top of placeholders. Virtual placements are invisible and only play +the role of prototypes for real images. Virtual placements can be deleted by a +deletion command only when the `d` key is equal to ``i``, ``I``, ``n`` or ``N``. +The key values ``a``, ``c``, ``p``, ``q``, ``x``, ``y``, ``z`` and their capital +variants never affect virtual placements because they do not have a physical +location on the screen. + +Real images displayed on top of placeholders are not considered placements from +the protocol perspective. They cannot be manipulated using graphics commands, +instead they should be moved, deleted, or modified by manipulating the +underlying placeholder as normal text. + + Deleting images --------------------- diff --git a/gen-apc-parsers.py b/gen-apc-parsers.py index 241209a62..bf98c53a9 100755 --- a/gen-apc-parsers.py +++ b/gen-apc-parsers.py @@ -274,6 +274,7 @@ def graphics_parser() -> None: 'Y': ('cell_y_offset', 'uint'), 'z': ('z_index', 'int'), 'C': ('cursor_movement', 'uint'), + 'U': ('unicode_placement', 'uint'), } text = generate('parse_graphics_code', 'screen_handle_graphics_command', 'graphics_command', keymap, 'GraphicsCommand') write_header(text, 'kitty/parse-graphics-command.h') diff --git a/gen-wcwidth.py b/gen-wcwidth.py index 9096c0b87..83f6fbc86 100755 --- a/gen-wcwidth.py +++ b/gen-wcwidth.py @@ -529,6 +529,52 @@ def add_all(p: Callable[..., None], for_go: bool = False) -> None: subprocess.check_call(['gofmt', '-w', '-s', gof.name]) +def gen_rowcolumn_diacritics() -> None: + # codes of all row/column diacritics + codes = [] + with open("./rowcolumn-diacritics.txt", "r") as file: + for line in file.readlines(): + if line.startswith('#'): + continue + code = int(line.split(";")[0], 16) + codes.append(code) + + with create_header('kitty/rowcolumn-diacritics.c') as p: + p('#include "unicode-data.h"') + p('int diacritic_to_num(char_type code) {') + p('\tswitch (code) {') + + range_start_num = 1 + range_start = 0 + range_end = 0 + + def print_range() -> None: + if range_start >= range_end: + return + write_case((range_start, range_end), p) + p('\t\treturn code - ' + hex(range_start) + ' + ' + + str(range_start_num) + ';') + + for code in codes: + if range_end == code: + range_end += 1 + else: + print_range() + range_start_num += range_end - range_start + range_start = code + range_end = code + 1 + print_range() + + p('\t}') + p('\treturn 0;') + p('}') + + with open('kittens/icat/rowcolumn_diacritics.py', 'w') as f: + f.write('# generated by gen-wcwidth.py, do not edit\n\n') + strings_utf8 = ', '.join(repr(chr(c).encode('utf-8')) for c in codes) + f.write("rowcolumn_diacritics_utf8 = ({}) # noqa\n".format(strings_utf8)) + + parse_ucd() parse_prop_list() parse_emoji() @@ -537,3 +583,4 @@ def add_all(p: Callable[..., None], for_go: bool = False) -> None: gen_wcwidth() gen_emoji() gen_names() +gen_rowcolumn_diacritics() diff --git a/kitty/data-types.h b/kitty/data-types.h index fb9b2e5f2..d25dfbe5f 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -81,6 +81,9 @@ typedef struct ImageAnchorPosition { #define DECORATION_FG_CODE 58 #define CHAR_IS_BLANK(ch) ((ch) == 32 || (ch) == 0) +// PUA character used as an image placeholder. +#define IMAGE_PLACEHOLDER_CHAR 0x10EEEE + #define FG 1 #define BG 2 @@ -192,6 +195,7 @@ typedef union LineAttrs { struct { uint8_t is_continued : 1; uint8_t has_dirty_text : 1; + uint8_t has_image_placeholders : 1; PromptKind prompt_kind : 2; }; uint8_t val; diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 60d8d0850..fc159d0bf 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -21,6 +21,7 @@ from kitty.options.types import Options from kitty.types import SignalInfo # Constants {{{ +IMAGE_PLACEHOLDER_CHAR: int GLFW_PRIMARY_SELECTION: int GLFW_CLIPBOARD: int CLD_KILLED: int diff --git a/kitty/fonts.c b/kitty/fonts.c index 866f718b4..7771fd7df 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -535,6 +535,7 @@ START_ALLOW_CASE_RANGE case 0: case ' ': case '\t': + case IMAGE_PLACEHOLDER_CHAR: return BLANK_FONT; case 0x2500 ... 0x2573: case 0x2574 ... 0x259f: diff --git a/kitty/graphics.c b/kitty/graphics.c index 26fd84d51..389364be8 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -149,6 +149,7 @@ img_by_client_number(GraphicsManager *self, uint32_t number) { static void remove_image(GraphicsManager *self, size_t idx) { + assert(idx < self->image_count); free_image(self, self->images + idx); remove_i_from_array(self->images, idx, self->image_count); self->layers_dirty = true; @@ -555,7 +556,7 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_ img->root_frame_data_loaded = false; img->is_drawn = false; img->current_frame_shown_at = 0; - free_refs_data(img); + free_image(self, img); *is_dirty = true; self->layers_dirty = true; } else { @@ -651,12 +652,12 @@ static void update_dest_rect(ImageRef *ref, uint32_t num_cols, uint32_t num_rows, CellPixelSize cell) { uint32_t t; if (num_cols == 0) { - t = ref->src_width + ref->cell_x_offset; + t = (uint32_t)(ref->src_width + ref->cell_x_offset); num_cols = t / cell.width; if (t > num_cols * cell.width) num_cols += 1; } if (num_rows == 0) { - t = ref->src_height + ref->cell_y_offset; + t = (uint32_t)(ref->src_height + ref->cell_y_offset); num_rows = t / cell.height; if (t > num_rows * cell.height) num_rows += 1; } @@ -664,6 +665,176 @@ update_dest_rect(ImageRef *ref, uint32_t num_cols, uint32_t num_rows, CellPixelS ref->effective_num_cols = num_cols; } +// Create a real image ref for a virtual image ref (placement) positioned in the +// given cells. This is used for images positioned using Unicode placeholders. +// +// The image is resized to fit a box of cells with dimensions +// `image_ref->columns` by `image_ref->rows`. The parameters `img_col`, +// `img_row, `columns`, `rows` describe a part of this box that we want to +// display. +// +// Parameters: +// - `self` - the graphics manager +// - `screen_row` - the starting row of the screen +// - `screen_col` - the starting column of the screen +// - `image_id` - the id of the image +// - `placement_id` - the id of the placement (0 to find it automatically), it +// must be a virtual placement +// - `img_col` - the column of the image box we want to start with (base 0) +// - `img_row` - the row of the image box we want to start with (base 0) +// - `columns` - the number of columns we want to display +// - `rows` - the number of rows we want to display +// - `cell` - the size of a screen cell +Image *grman_put_cell_image(GraphicsManager *self, uint32_t screen_row, + uint32_t screen_col, uint32_t image_id, + uint32_t placement_id, uint32_t img_col, + uint32_t img_row, uint32_t columns, uint32_t rows, + CellPixelSize cell) { + Image *img = img_by_client_id(self, image_id); + if (img == NULL) return NULL; + + ImageRef *virt_img_ref = NULL; + if (placement_id) { + // Find the placement by the id. It must be a virtual placement. + for (size_t i = 0; i < img->refcnt; i++) { + if (img->refs[i].is_virtual_ref && + img->refs[i].client_id == placement_id) { + virt_img_ref = img->refs + i; + break; + } + } + } else { + // Find the first virtual image placement. + for (size_t i = 0; i < img->refcnt; i++) { + if (img->refs[i].is_virtual_ref) { + virt_img_ref = img->refs + i; + break; + } + } + } + + if (!virt_img_ref) return NULL; + + // Create the ref structure on stack first. We will not create a real + // reference if the image is completely out of bounds. + ImageRef ref = {0}; + ref.is_cell_image = true; + + uint32_t img_rows = virt_img_ref->num_rows; + uint32_t img_columns = virt_img_ref->num_cols; + // If the number of columns or rows for the image is not set, compute them + // in such a way that the image is as close as possible to its natural size. + if (img_columns == 0) + img_columns = (img->width + cell.width - 1) / cell.width; + if (img_rows == 0) img_rows = (img->height + cell.height - 1) / cell.height; + + ref.start_row = screen_row; + ref.start_column = screen_col; + ref.num_cols = columns; + ref.num_rows = rows; + + // The image is fit to the destination box of size + // (cell.width * img_columns) by (cell.height * img_rows) + // The conversion from source (image) coordinates to destination (box) + // coordinates is done by the following formula: + // x_dst = x_src * x_scale + x_offset + // y_dst = y_src * y_scale + y_offset + float x_offset, y_offset, x_scale, y_scale; + + // Fit the image to the box while preserving aspect ratio + if (img->width * img_rows * cell.height > + img->height * img_columns * cell.width) { + // Fit to width and center vertically. + x_offset = 0; + x_scale = (float)(img_columns * cell.width) / img->width; + y_scale = x_scale; + y_offset = (img_rows * cell.height - img->height * y_scale) / 2; + } else { + // Fit to height and center horizontally. + y_offset = 0; + y_scale = (float)(img_rows * cell.height) / img->height; + x_scale = y_scale; + x_offset = (img_columns * cell.width - img->width * x_scale) / 2; + } + + // Now we can compute source (image) coordinates from destination (box) + // coordinates by formula: + // x_src = (x_dst - x_offset) / x_scale + // y_src = (y_dst - y_offset) / y_scale + + // Destination (box) coordinates of the rectangle we want to display. + uint32_t x_dst = img_col * cell.width; + uint32_t y_dst = img_row * cell.height; + uint32_t w_dst = columns * cell.width; + uint32_t h_dst = rows * cell.height; + + // Compute the source coordinates of the rectangle. + ref.src_x = (x_dst - x_offset) / x_scale; + ref.src_y = (y_dst - y_offset) / y_scale; + ref.src_width = w_dst / x_scale; + ref.src_height = h_dst / y_scale; + + // If the top left corner is out of bounds of the source image, we can + // adjust cell offsets and the starting row/column. And if the rectangle is + // completely out of bounds, we can avoid creating a real reference. This + // is just an optimization, the image will be displayed correctly even if we + // do not do this. + if (ref.src_x < 0) { + ref.src_width += ref.src_x; + ref.cell_x_offset = (uint32_t)(-ref.src_x * x_scale); + ref.src_x = 0; + uint32_t col_offset = ref.cell_x_offset / cell.width; + ref.cell_x_offset %= cell.width; + ref.start_column += col_offset; + if (ref.num_cols <= col_offset) + return img; + ref.num_cols -= col_offset; + } + if (ref.src_y < 0) { + ref.src_height += ref.src_y; + ref.cell_y_offset = (uint32_t)(-ref.src_y * y_scale); + ref.src_y = 0; + uint32_t row_offset = ref.cell_y_offset / cell.height; + ref.cell_y_offset %= cell.height; + ref.start_row += row_offset; + if (ref.num_rows <= row_offset) + return img; + ref.num_rows -= row_offset; + } + + // For the bottom right corner we can remove only completely empty rows and + // columns. + if (ref.src_x + ref.src_width > img->width) { + float redundant_w = ref.src_x + ref.src_width - img->width; + uint32_t redundant_cols = (uint32_t)(redundant_w * x_scale) / cell.width; + if (ref.num_cols <= redundant_cols) + return img; + ref.src_width -= redundant_cols * cell.width / x_scale; + ref.num_cols -= redundant_cols; + } + if (ref.src_y + ref.src_height > img->height) { + float redundant_h = ref.src_y + ref.src_height - img->height; + uint32_t redundant_rows = (uint32_t)(redundant_h * y_scale) / cell.height; + if (ref.num_rows <= redundant_rows) + return img; + ref.src_height -= redundant_rows * cell.height / y_scale; + ref.num_rows -= redundant_rows; + } + + // The cursor will be drawn on top of the image. + ref.z_index = -1; + + // Create a real ref. + ensure_space_for(img, refs, ImageRef, img->refcnt + 1, refcap, 16, true); + self->layers_dirty = true; + ImageRef *real_ref = img->refs + img->refcnt++; + *real_ref = ref; + img->atime = monotonic(); + + update_src_rect(real_ref, img); + update_dest_rect(real_ref, ref.num_cols, ref.num_rows, cell); + return img; +} static uint32_t handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, bool *is_dirty, Image *img, CellPixelSize cell) { @@ -691,8 +862,8 @@ handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, b } img->atime = monotonic(); ref->src_x = g->x_offset; ref->src_y = g->y_offset; ref->src_width = g->width ? g->width : img->width; ref->src_height = g->height ? g->height : img->height; - ref->src_width = MIN(ref->src_width, img->width - (img->width > ref->src_x ? ref->src_x : img->width)); - ref->src_height = MIN(ref->src_height, img->height - (img->height > ref->src_y ? ref->src_y : img->height)); + ref->src_width = MIN(ref->src_width, img->width - ((float)img->width > ref->src_x ? ref->src_x : (float)img->width)); + ref->src_height = MIN(ref->src_height, img->height - ((float)img->height > ref->src_y ? ref->src_y : (float)img->height)); ref->z_index = g->z_index; ref->start_row = c->y; ref->start_column = c->x; ref->cell_x_offset = MIN(g->cell_x_offset, cell.width - 1); @@ -701,8 +872,12 @@ handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, b if (img->client_id) ref->client_id = g->placement_id; update_src_rect(ref, img); update_dest_rect(ref, g->num_cells, g->num_lines, cell); + if (g->unicode_placement) { + ref->is_virtual_ref = true; + ref->start_row = ref->start_column = 0; + } // Move the cursor, the screen will take care of ensuring it is in bounds - if (g->cursor_movement != 1) { + if (g->cursor_movement != 1 && !g->unicode_placement) { c->x += ref->effective_num_cols; c->y += ref->effective_num_rows - 1; } return img->client_id; @@ -759,6 +934,7 @@ grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scree img->is_drawn = false; for (j = 0; j < img->refcnt; j++) { ref = img->refs + j; + if (ref->is_virtual_ref) continue; r.top = y0 - ref->start_row * dy - dy * (float)ref->cell_y_offset / (float)cell.height; if (ref->num_rows > 0) r.bottom = y0 - (ref->start_row + (int32_t)ref->num_rows) * dy; else r.bottom = r.top - screen_height * (float)ref->src_height / screen_height_px; @@ -1414,6 +1590,7 @@ modify_refs(GraphicsManager *self, const void* data, bool (*filter_func)(ImageRe static bool scroll_filter_func(ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) { + if (ref->is_virtual_ref) return false; ScrollData *d = (ScrollData*)data; ref->start_row += d->amt; return ref->start_row + (int32_t)ref->effective_num_rows <= d->limit; @@ -1431,6 +1608,7 @@ ref_outside_region(const ImageRef *ref, index_type margin_top, index_type margin static bool scroll_filter_margins_func(ImageRef* ref, Image* img, const void* data, CellPixelSize cell) { + if (ref->is_virtual_ref) return false; ScrollData *d = (ScrollData*)data; if (ref_within_region(ref, d->margin_top, d->margin_bottom)) { ref->start_row += d->amt; @@ -1468,13 +1646,44 @@ grman_scroll_images(GraphicsManager *self, const ScrollData *data, CellPixelSize } } +static bool +cell_image_row_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) { + if (ref->is_virtual_ref || !ref->is_cell_image) + return false; + int32_t top = *(int32_t *)data; + int32_t bottom = *((int32_t *)data + 1); + return ref_within_region(ref, top, bottom); +} + +static bool +cell_image_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data UNUSED, CellPixelSize cell UNUSED) { + return !ref->is_virtual_ref && ref->is_cell_image; +} + +// Remove cell images within the given region. +void +grman_remove_cell_images(GraphicsManager *self, int32_t top, int32_t bottom) { + CellPixelSize dummy = {0}; + int32_t data[] = {top, bottom}; + filter_refs(self, data, false, cell_image_row_filter_func, dummy, false); +} + +void +grman_remove_all_cell_images(GraphicsManager *self) { + CellPixelSize dummy = {0}; + filter_refs(self, NULL, false, cell_image_filter_func, dummy, false); +} + + static bool clear_filter_func(const ImageRef *ref, Image UNUSED *img, const void UNUSED *data, CellPixelSize cell UNUSED) { + if (ref->is_virtual_ref || ref->is_cell_image) return false; return ref->start_row + (int32_t)ref->effective_num_rows > 0; } static bool clear_all_filter_func(const ImageRef *ref UNUSED, Image UNUSED *img, const void UNUSED *data, CellPixelSize cell UNUSED) { + if (ref->is_virtual_ref) return false; return true; } @@ -1500,18 +1709,21 @@ number_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelS static bool x_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) { + if (ref->is_virtual_ref || ref->is_cell_image) return false; const GraphicsCommand *g = data; return ref->start_column <= (int32_t)g->x_offset - 1 && ((int32_t)g->x_offset - 1) < ((int32_t)(ref->start_column + ref->effective_num_cols)); } static bool y_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) { + if (ref->is_virtual_ref || ref->is_cell_image) return false; const GraphicsCommand *g = data; return ref->start_row <= (int32_t)g->y_offset - 1 && ((int32_t)g->y_offset - 1) < ((int32_t)(ref->start_row + ref->effective_num_rows)); } static bool z_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixelSize cell UNUSED) { + if (ref->is_virtual_ref || ref->is_cell_image) return false; const GraphicsCommand *g = data; return ref->z_index == g->z_index; } @@ -1519,11 +1731,13 @@ z_filter_func(const ImageRef *ref, Image UNUSED *img, const void *data, CellPixe static bool point_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell) { + if (ref->is_virtual_ref || ref->is_cell_image) return false; return x_filter_func(ref, img, data, cell) && y_filter_func(ref, img, data, cell); } static bool point3d_filter_func(const ImageRef *ref, Image *img, const void *data, CellPixelSize cell) { + if (ref->is_virtual_ref || ref->is_cell_image) return false; return z_filter_func(ref, img, data, cell) && point_filter_func(ref, img, data, cell); } @@ -1584,6 +1798,7 @@ grman_rescale(GraphicsManager *self, CellPixelSize cell) { img = self->images + i; for (size_t j = img->refcnt; j-- > 0;) { ref = img->refs + j; + if (ref->is_virtual_ref || ref->is_cell_image) continue; ref->cell_x_offset = MIN(ref->cell_x_offset, cell.width - 1); ref->cell_y_offset = MIN(ref->cell_y_offset, cell.height - 1); update_dest_rect(ref, ref->num_cols, ref->num_rows, cell); @@ -1851,6 +2066,7 @@ init_graphics(PyObject *module) { if (PyType_Ready(&GraphicsManager_Type) < 0) return false; if (PyModule_AddObject(module, "GraphicsManager", (PyObject *)&GraphicsManager_Type) != 0) return false; if (PyModule_AddFunctions(module, module_methods) != 0) return false; + if (PyModule_AddIntMacro(module, IMAGE_PLACEHOLDER_CHAR) != 0) return false; Py_INCREF(&GraphicsManager_Type); return true; } diff --git a/kitty/graphics.h b/kitty/graphics.h index 760f8f272..b990178b3 100644 --- a/kitty/graphics.h +++ b/kitty/graphics.h @@ -21,6 +21,7 @@ typedef struct { union { uint32_t num_cells, other_frame_number; }; union { int32_t z_index, gap; }; size_t payload_sz; + bool unicode_placement; } GraphicsCommand; typedef struct { @@ -28,12 +29,18 @@ typedef struct { } ImageRect; typedef struct { - uint32_t src_width, src_height, src_x, src_y; + float src_width, src_height, src_x, src_y; uint32_t cell_x_offset, cell_y_offset, num_cols, num_rows, effective_num_rows, effective_num_cols; int32_t z_index; int32_t start_row, start_column; uint32_t client_id; ImageRect src_rect; + // Indicates whether this reference represents a cell image that should be + // removed when the corresponding cells are modified. + bool is_cell_image; + // Virtual refs are not displayed but they can be used as prototypes for + // refs placed using unicode placeholders. + bool is_virtual_ref; } ImageRef; typedef struct { @@ -154,10 +161,13 @@ gl_pos_y(const unsigned int px_from_top_margin, const unsigned int viewport_size GraphicsManager* grman_alloc(void); void grman_clear(GraphicsManager*, bool, CellPixelSize fg); const char* grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, Cursor *c, bool *is_dirty, CellPixelSize fg); +Image* grman_put_cell_image(GraphicsManager *self, uint32_t row, uint32_t col, uint32_t image_id, uint32_t placement_id, uint32_t x, uint32_t y, uint32_t w, uint32_t h, CellPixelSize cell); bool grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float screen_left, float screen_top, float dx, float dy, unsigned int num_cols, unsigned int num_rows, CellPixelSize); void grman_scroll_images(GraphicsManager *self, const ScrollData*, CellPixelSize fg); void grman_resize(GraphicsManager*, index_type, index_type, index_type, index_type); void grman_rescale(GraphicsManager *self, CellPixelSize fg); +void grman_remove_cell_images(GraphicsManager *self, int32_t top, int32_t bottom); +void grman_remove_all_cell_images(GraphicsManager *self); void gpu_data_for_image(ImageRenderData *ans, float left, float top, float right, float bottom); void gpu_data_for_centered_image(ImageRenderData *ans, unsigned int screen_width_px, unsigned int screen_height_px, unsigned int width, unsigned int height); bool png_path_to_bitmap(const char *path, uint8_t** data, unsigned int* width, unsigned int* height, size_t* sz); diff --git a/kitty/history.c b/kitty/history.c index 028334ee3..957c8c3c0 100644 --- a/kitty/history.c +++ b/kitty/history.c @@ -201,6 +201,11 @@ historybuf_mark_line_dirty(HistoryBuf *self, index_type y) { attrptr(self, index_of(self, y))->has_dirty_text = true; } +void +historybuf_set_line_has_image_placeholders(HistoryBuf *self, index_type y, bool val) { + attrptr(self, index_of(self, y))->has_image_placeholders = val; +} + void historybuf_clear(HistoryBuf *self) { pagerhist_clear(self); diff --git a/kitty/line-buf.c b/kitty/line-buf.c index 5cbb82c8e..659114ce8 100644 --- a/kitty/line-buf.c +++ b/kitty/line-buf.c @@ -52,6 +52,11 @@ linebuf_mark_line_clean(LineBuf *self, index_type y) { self->line_attrs[y].has_dirty_text = false; } +void +linebuf_set_line_has_image_placeholders(LineBuf *self, index_type y, bool val) { + self->line_attrs[y].has_image_placeholders = val; +} + void linebuf_clear_attrs_and_dirty(LineBuf *self, index_type y) { self->line_attrs[y].val = 0; diff --git a/kitty/lineops.h b/kitty/lineops.h index cbba20d74..22dc9e8b5 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -112,6 +112,7 @@ void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, H void linebuf_mark_line_dirty(LineBuf *self, index_type y); void linebuf_clear_attrs_and_dirty(LineBuf *self, index_type y); void linebuf_mark_line_clean(LineBuf *self, index_type y); +void linebuf_set_line_has_image_placeholders(LineBuf *self, index_type y, bool val); unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y); void linebuf_set_last_char_as_continuation(LineBuf *self, index_type y, bool continued); bool linebuf_line_ends_with_continuation(LineBuf *self, index_type y); @@ -124,6 +125,7 @@ bool history_buf_endswith_wrap(HistoryBuf *self); CPUCell* historybuf_cpu_cells(HistoryBuf *self, index_type num); void historybuf_mark_line_clean(HistoryBuf *self, index_type y); void historybuf_mark_line_dirty(HistoryBuf *self, index_type y); +void historybuf_set_line_has_image_placeholders(HistoryBuf *self, index_type y, bool val); void historybuf_refresh_sprite_positions(HistoryBuf *self); void historybuf_clear(HistoryBuf *self); void mark_text_in_line(PyObject *marker, Line *line); diff --git a/kitty/parse-graphics-command.h b/kitty/parse-graphics-command.h index 025055ea8..d28a41fd8 100644 --- a/kitty/parse-graphics-command.h +++ b/kitty/parse-graphics-command.h @@ -39,7 +39,8 @@ static inline void parse_graphics_code(Screen *screen, cell_x_offset = 'X', cell_y_offset = 'Y', z_index = 'z', - cursor_movement = 'C' + cursor_movement = 'C', + unicode_placement = 'U' }; enum KEYS key = 'a'; @@ -124,6 +125,9 @@ static inline void parse_graphics_code(Screen *screen, case cursor_movement: value_state = UINT; break; + case unicode_placement: + value_state = UINT; + break; default: REPORT_ERROR("Malformed GraphicsCommand control block, invalid key " "character: 0x%x", @@ -268,6 +272,7 @@ static inline void parse_graphics_code(Screen *screen, U(cell_x_offset); U(cell_y_offset); U(cursor_movement); + U(unicode_placement); default: break; } @@ -327,7 +332,7 @@ static inline void parse_graphics_code(Screen *screen, REPORT_VA_COMMAND( "s {sc sc sc sc sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI sI " - "si sI} y#", + "sI si sI} y#", "graphics_command", "action", g.action, "delete_action", g.delete_action, "transmission_type", g.transmission_type, "compressed", g.compressed, "format", (unsigned int)g.format, "more", (unsigned int)g.more, "id", @@ -341,8 +346,9 @@ static inline void parse_graphics_code(Screen *screen, "num_cells", (unsigned int)g.num_cells, "num_lines", (unsigned int)g.num_lines, "cell_x_offset", (unsigned int)g.cell_x_offset, "cell_y_offset", (unsigned int)g.cell_y_offset, "cursor_movement", - (unsigned int)g.cursor_movement, "z_index", (int)g.z_index, "payload_sz", - g.payload_sz, payload, g.payload_sz); + (unsigned int)g.cursor_movement, "unicode_placement", + (unsigned int)g.unicode_placement, "z_index", (int)g.z_index, + "payload_sz", g.payload_sz, payload, g.payload_sz); screen_handle_graphics_command(screen, &g, payload); } diff --git a/kitty/rowcolumn-diacritics.c b/kitty/rowcolumn-diacritics.c new file mode 100644 index 000000000..b97518b59 --- /dev/null +++ b/kitty/rowcolumn-diacritics.c @@ -0,0 +1,187 @@ +// Unicode data, built from the Unicode Standard 15.0.0 +// Code generated by gen-wcwidth.py, DO NOT EDIT. + +#include "data-types.h" + +START_ALLOW_CASE_RANGE + +#include "unicode-data.h" +int diacritic_to_num(char_type code) { + switch (code) { + case 0x305 ... 0x306: + return code - 0x305 + 1; + case 0x30d ... 0x30f: + return code - 0x30d + 2; + case 0x310 ... 0x311: + return code - 0x310 + 4; + case 0x312 ... 0x313: + return code - 0x312 + 5; + case 0x33d ... 0x340: + return code - 0x33d + 6; + case 0x346 ... 0x347: + return code - 0x346 + 9; + case 0x34a ... 0x34d: + return code - 0x34a + 10; + case 0x350 ... 0x353: + return code - 0x350 + 13; + case 0x357 ... 0x358: + return code - 0x357 + 16; + case 0x35b ... 0x35c: + return code - 0x35b + 17; + case 0x363 ... 0x370: + return code - 0x363 + 18; + case 0x483 ... 0x488: + return code - 0x483 + 31; + case 0x592 ... 0x596: + return code - 0x592 + 36; + case 0x597 ... 0x59a: + return code - 0x597 + 40; + case 0x59c ... 0x5a2: + return code - 0x59c + 43; + case 0x5a8 ... 0x5aa: + return code - 0x5a8 + 49; + case 0x5ab ... 0x5ad: + return code - 0x5ab + 51; + case 0x5af ... 0x5b0: + return code - 0x5af + 53; + case 0x5c4 ... 0x5c5: + return code - 0x5c4 + 54; + case 0x610 ... 0x618: + return code - 0x610 + 55; + case 0x657 ... 0x65c: + return code - 0x657 + 63; + case 0x65d ... 0x65f: + return code - 0x65d + 68; + case 0x6d6 ... 0x6dd: + return code - 0x6d6 + 70; + case 0x6df ... 0x6e3: + return code - 0x6df + 77; + case 0x6e4 ... 0x6e5: + return code - 0x6e4 + 81; + case 0x6e7 ... 0x6e9: + return code - 0x6e7 + 82; + case 0x6eb ... 0x6ed: + return code - 0x6eb + 84; + case 0x730 ... 0x731: + return code - 0x730 + 86; + case 0x732 ... 0x734: + return code - 0x732 + 87; + case 0x735 ... 0x737: + return code - 0x735 + 89; + case 0x73a ... 0x73b: + return code - 0x73a + 91; + case 0x73d ... 0x73e: + return code - 0x73d + 92; + case 0x73f ... 0x742: + return code - 0x73f + 93; + case 0x743 ... 0x744: + return code - 0x743 + 96; + case 0x745 ... 0x746: + return code - 0x745 + 97; + case 0x747 ... 0x748: + return code - 0x747 + 98; + case 0x749 ... 0x74b: + return code - 0x749 + 99; + case 0x7eb ... 0x7f2: + return code - 0x7eb + 101; + case 0x7f3 ... 0x7f4: + return code - 0x7f3 + 108; + case 0x816 ... 0x81a: + return code - 0x816 + 109; + case 0x81b ... 0x824: + return code - 0x81b + 113; + case 0x825 ... 0x828: + return code - 0x825 + 122; + case 0x829 ... 0x82e: + return code - 0x829 + 125; + case 0x951 ... 0x952: + return code - 0x951 + 130; + case 0x953 ... 0x955: + return code - 0x953 + 131; + case 0xf82 ... 0xf84: + return code - 0xf82 + 133; + case 0xf86 ... 0xf88: + return code - 0xf86 + 135; + case 0x135d ... 0x1360: + return code - 0x135d + 137; + case 0x17dd ... 0x17de: + return code - 0x17dd + 140; + case 0x193a ... 0x193b: + return code - 0x193a + 141; + case 0x1a17 ... 0x1a18: + return code - 0x1a17 + 142; + case 0x1a75 ... 0x1a7d: + return code - 0x1a75 + 143; + case 0x1b6b ... 0x1b6c: + return code - 0x1b6b + 151; + case 0x1b6d ... 0x1b74: + return code - 0x1b6d + 152; + case 0x1cd0 ... 0x1cd3: + return code - 0x1cd0 + 159; + case 0x1cda ... 0x1cdc: + return code - 0x1cda + 162; + case 0x1ce0 ... 0x1ce1: + return code - 0x1ce0 + 164; + case 0x1dc0 ... 0x1dc2: + return code - 0x1dc0 + 165; + case 0x1dc3 ... 0x1dca: + return code - 0x1dc3 + 167; + case 0x1dcb ... 0x1dcd: + return code - 0x1dcb + 174; + case 0x1dd1 ... 0x1de7: + return code - 0x1dd1 + 176; + case 0x1dfe ... 0x1dff: + return code - 0x1dfe + 198; + case 0x20d0 ... 0x20d2: + return code - 0x20d0 + 199; + case 0x20d4 ... 0x20d8: + return code - 0x20d4 + 201; + case 0x20db ... 0x20dd: + return code - 0x20db + 205; + case 0x20e1 ... 0x20e2: + return code - 0x20e1 + 207; + case 0x20e7 ... 0x20e8: + return code - 0x20e7 + 208; + case 0x20e9 ... 0x20ea: + return code - 0x20e9 + 209; + case 0x20f0 ... 0x20f1: + return code - 0x20f0 + 210; + case 0x2cef ... 0x2cf2: + return code - 0x2cef + 211; + case 0x2de0 ... 0x2e00: + return code - 0x2de0 + 214; + case 0xa66f ... 0xa670: + return code - 0xa66f + 246; + case 0xa67c ... 0xa67e: + return code - 0xa67c + 247; + case 0xa6f0 ... 0xa6f2: + return code - 0xa6f0 + 249; + case 0xa8e0 ... 0xa8f2: + return code - 0xa8e0 + 251; + case 0xaab0 ... 0xaab1: + return code - 0xaab0 + 269; + case 0xaab2 ... 0xaab4: + return code - 0xaab2 + 270; + case 0xaab7 ... 0xaab9: + return code - 0xaab7 + 272; + case 0xaabe ... 0xaac0: + return code - 0xaabe + 274; + case 0xaac1 ... 0xaac2: + return code - 0xaac1 + 276; + case 0xfe20 ... 0xfe27: + return code - 0xfe20 + 277; + case 0x10a0f ... 0x10a10: + return code - 0x10a0f + 284; + case 0x10a38 ... 0x10a39: + return code - 0x10a38 + 285; + case 0x1d185 ... 0x1d18a: + return code - 0x1d185 + 286; + case 0x1d1aa ... 0x1d1ae: + return code - 0x1d1aa + 291; + case 0x1d242 ... 0x1d245: + return code - 0x1d242 + 295; + } + return 0; +} + +END_ALLOW_CASE_RANGE diff --git a/kitty/screen.c b/kitty/screen.c index 842396e31..f00e68499 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -374,6 +374,7 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) { if (is_main) setup_cursor(cursor); /* printf("old_cursor: (%u, %u) new_cursor: (%u, %u) beyond_content: %d\n", self->cursor->x, self->cursor->y, cursor.after.x, cursor.after.y, cursor.is_beyond_content); */ setup_cursor(main_saved_cursor); + grman_remove_all_cell_images(self->main_grman); grman_resize(self->main_grman, self->lines, lines, self->columns, columns); // Resize alt linebuf @@ -382,6 +383,7 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) { Py_CLEAR(self->alt_linebuf); self->alt_linebuf = n; if (!is_main) setup_cursor(cursor); setup_cursor(alt_saved_cursor); + grman_remove_all_cell_images(self->alt_grman); grman_resize(self->alt_grman, self->lines, lines, self->columns, columns); #undef setup_cursor @@ -432,6 +434,8 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) { void screen_rescale_images(Screen *self) { + grman_remove_all_cell_images(self->main_grman); + grman_remove_all_cell_images(self->alt_grman); grman_rescale(self->main_grman, self->cell_size); grman_rescale(self->alt_grman, self->cell_size); } @@ -708,6 +712,9 @@ draw_codepoint(Screen *self, char_type och, bool from_input_stream) { line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, self->active_hyperlink_id); self->cursor->x++; } + if (UNLIKELY(ch == IMAGE_PLACEHOLDER_CHAR)) { + linebuf_set_line_has_image_placeholders(self->linebuf, self->cursor->y, true); + } self->is_dirty = true; if (selection_has_screen_line(&self->selections, self->cursor->y)) clear_selection(&self->selections); linebuf_mark_line_dirty(self->linebuf, self->cursor->y); @@ -1621,6 +1628,25 @@ screen_fake_move_cursor_to_position(Screen *self, index_type start_x, index_type // Editing {{{ +// Remove all cell images from a portion of the screen and mark lines that +// contain image placeholders as dirty to make sure they are redrawn. This is +// needed when we perform commands that may move some lines without marking them +// as dirty (like screen_insert_lines) and at the same time don't move image +// references (i.e. unlike screen_scroll, which moves everything). +static void +screen_dirty_line_graphics(Screen *self, unsigned int top, unsigned int bottom) { + bool need_to_remove = false; + for (unsigned int y = top; y <= bottom; y++) { + if (self->linebuf->line_attrs[y].has_image_placeholders) { + need_to_remove = true; + linebuf_mark_line_dirty(self->linebuf, y); + self->is_dirty = true; + } + } + if (need_to_remove) + grman_remove_cell_images(self->grman, top, bottom); +} + void screen_erase_in_line(Screen *self, unsigned int how, bool private) { /*Erases a line in a specific way. @@ -1651,6 +1677,7 @@ screen_erase_in_line(Screen *self, unsigned int how, bool private) { break; } if (n > 0) { + screen_dirty_line_graphics(self, self->cursor->y, self->cursor->y); linebuf_init_line(self->linebuf, self->cursor->y); if (private) { line_clear_text(self->linebuf->line, s, n, BLANK_CHAR); @@ -1701,6 +1728,7 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) { return; } if (b > a) { + if (how != 3) screen_dirty_line_graphics(self, a, b); for (unsigned int i=a; i < b; i++) { linebuf_init_line(self->linebuf, i); if (private) { @@ -1728,6 +1756,7 @@ screen_insert_lines(Screen *self, unsigned int count) { unsigned int top = self->margin_top, bottom = self->margin_bottom; if (count == 0) count = 1; if (top <= self->cursor->y && self->cursor->y <= bottom) { + screen_dirty_line_graphics(self, top, bottom); linebuf_insert_lines(self->linebuf, count, self->cursor->y, bottom); self->is_dirty = true; clear_selection(&self->selections); @@ -1751,6 +1780,7 @@ screen_delete_lines(Screen *self, unsigned int count) { unsigned int top = self->margin_top, bottom = self->margin_bottom; if (count == 0) count = 1; if (top <= self->cursor->y && self->cursor->y <= bottom) { + screen_dirty_line_graphics(self, top, bottom); linebuf_delete_lines(self->linebuf, count, self->cursor->y, bottom); self->is_dirty = true; clear_selection(&self->selections); @@ -2277,6 +2307,134 @@ screen_has_marker(Screen *self) { return self->marker != NULL; } +static uint32_t diacritic_to_rowcolumn(combining_type m) { + char_type c = codepoint_for_mark(m); + return diacritic_to_num(c); +} + +static uint32_t color_to_id(color_type c) { + // Just take 24 most significant bits of the color. This works both for + // 24-bit and 8-bit colors. + return (c >> 8) & 0xffffff; +} + +// Scan the line and create cell images in place of unicode placeholders +// reserved for image placement. +static void +screen_render_line_graphics(Screen *self, Line *line, int32_t row) { + // If there are no image placeholders now, no need to rescan the line. + if (!line->attrs.has_image_placeholders) + return; + // Remove existing images. + grman_remove_cell_images(self->grman, row, row); + // The placeholders might be erased. We will update the attribute. + line->attrs.has_image_placeholders = false; + index_type i; + uint32_t run_length = 0; + uint32_t prev_img_id_lower24bits = 0; + uint32_t prev_placement_id = 0; + // Note that the following values are 1-based, zero means unknown or incorrect. + uint32_t prev_img_id_higher8bits = 0; + uint32_t prev_img_row = 0; + uint32_t prev_img_col = 0; + for (i = 0; i < line->xnum; i++) { + CPUCell *cpu_cell = line->cpu_cells + i; + GPUCell *gpu_cell = line->gpu_cells + i; + uint32_t cur_img_id_lower24bits = 0; + uint32_t cur_placement_id = 0; + uint32_t cur_img_id_higher8bits = 0; + uint32_t cur_img_row = 0; + uint32_t cur_img_col = 0; + if (cpu_cell->ch == IMAGE_PLACEHOLDER_CHAR) { + line->attrs.has_image_placeholders = true; + // The lower 24 bits of the image id are encoded in the foreground + // color, and the placement id is (optionally) in the underline color. + cur_img_id_lower24bits = color_to_id(gpu_cell->fg); + cur_placement_id = color_to_id(gpu_cell->decoration_fg); + // If the char has diacritics, use them as row and column indices. + if (cpu_cell->cc_idx[0]) + cur_img_row = diacritic_to_rowcolumn(cpu_cell->cc_idx[0]); + if (cpu_cell->cc_idx[1]) + cur_img_col = diacritic_to_rowcolumn(cpu_cell->cc_idx[1]); + // The third diacritic is used to encode the higher 8 bits of the + // image id (optional). + if (cpu_cell->cc_idx[2]) + cur_img_id_higher8bits = diacritic_to_rowcolumn(cpu_cell->cc_idx[2]); + } + // The current run is continued if the lower 24 bits of the image id and + // the placement id are the same as in the previous cell and everything + // else is unknown or compatible with the previous cell. + if (run_length > 0 && cur_img_id_lower24bits == prev_img_id_lower24bits && + cur_placement_id == prev_placement_id && + (!cur_img_row || cur_img_row == prev_img_row) && + (!cur_img_col || cur_img_col == prev_img_col + 1) && + (!cur_img_id_higher8bits || cur_img_id_higher8bits == prev_img_id_higher8bits)) { + // This cell continues the current run. + run_length++; + // If some values are unknown, infer them from the previous cell. + cur_img_row = MAX(prev_img_row, 1u); + cur_img_col = prev_img_col + 1; + cur_img_id_higher8bits = MAX(prev_img_id_higher8bits, 1u); + } else { + // This cell breaks the current run. Render the current run if it + // has a non-zero length. + if (run_length > 0) { + uint32_t img_id = prev_img_id_lower24bits | (prev_img_id_higher8bits - 1) << 24; + grman_put_cell_image( + self->grman, row, i - run_length, img_id, + prev_placement_id, prev_img_col - run_length, + prev_img_row - 1, run_length, 1, self->cell_size); + } + // Start a new run. + if (cpu_cell->ch == IMAGE_PLACEHOLDER_CHAR) { + run_length = 1; + if (!cur_img_col) cur_img_col = 1; + if (!cur_img_row) cur_img_row = 1; + if (!cur_img_id_higher8bits) cur_img_id_higher8bits = 1; + } + } + prev_img_id_lower24bits = cur_img_id_lower24bits; + prev_img_id_higher8bits = cur_img_id_higher8bits; + prev_placement_id = cur_placement_id; + prev_img_row = cur_img_row; + prev_img_col = cur_img_col; + } + if (run_length > 0) { + // Render the last run. + uint32_t img_id = prev_img_id_lower24bits | (prev_img_id_higher8bits - 1) << 24; + grman_put_cell_image(self->grman, row, i - run_length, img_id, + prev_placement_id, prev_img_col - run_length, + prev_img_row - 1, run_length, 1, self->cell_size); + } +} + +// This functions is similar to screen_update_cell_data, but it only updates +// line graphics (cell images) and then marks lines as clean. It's used +// exclusively for testing unicode placeholders. +static void +screen_update_only_line_graphics_data(Screen *self) { + unsigned int history_line_added_count = self->history_line_added_count; + index_type lnum; + if (self->scrolled_by) self->scrolled_by = MIN(self->scrolled_by + history_line_added_count, self->historybuf->count); + screen_reset_dirty(self); + self->scroll_changed = false; + for (index_type y = 0; y < MIN(self->lines, self->scrolled_by); y++) { + lnum = self->scrolled_by - 1 - y; + historybuf_init_line(self->historybuf, lnum, self->historybuf->line); + if (self->historybuf->line->attrs.has_dirty_text) { + screen_render_line_graphics(self, self->historybuf->line, y - self->scrolled_by); + historybuf_mark_line_clean(self->historybuf, lnum); + } + } + for (index_type y = self->scrolled_by; y < self->lines; y++) { + lnum = y - self->scrolled_by; + linebuf_init_line(self->linebuf, lnum); + if (self->linebuf->line->attrs.has_dirty_text) { + screen_render_line_graphics(self, self->linebuf->line, y - self->scrolled_by); + linebuf_mark_line_clean(self->linebuf, lnum); + } + } +} void screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_data, bool cursor_has_moved) { @@ -2291,6 +2449,7 @@ screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_dat historybuf_init_line(self->historybuf, lnum, self->historybuf->line); if (self->historybuf->line->attrs.has_dirty_text) { render_line(fonts_data, self->historybuf->line, lnum, self->cursor, self->disable_ligatures); + screen_render_line_graphics(self, self->historybuf->line, y - self->scrolled_by); if (screen_has_marker(self)) mark_text_in_line(self->marker, self->historybuf->line); historybuf_mark_line_clean(self->historybuf, lnum); } @@ -2302,6 +2461,7 @@ screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_dat if (self->linebuf->line->attrs.has_dirty_text || (cursor_has_moved && (self->cursor->y == lnum || self->last_rendered.cursor_y == lnum))) { render_line(fonts_data, self->linebuf->line, lnum, self->cursor, self->disable_ligatures); + screen_render_line_graphics(self, self->linebuf->line, y - self->scrolled_by); if (self->linebuf->line->attrs.has_dirty_text && screen_has_marker(self)) mark_text_in_line(self->marker, self->linebuf->line); linebuf_mark_line_clean(self->linebuf, lnum); @@ -4059,6 +4219,8 @@ line_edge_colors(Screen *self, PyObject *a UNUSED) { return Py_BuildValue("kk", (unsigned long)left, (unsigned long)right); } +WRAP0(update_only_line_graphics_data) + #define MND(name, args) {#name, (PyCFunction)name, args, #name}, #define MODEFUNC(name) MND(name, METH_NOARGS) MND(set_##name, METH_O) @@ -4144,6 +4306,7 @@ static PyMethodDef methods[] = { MND(set_marker, METH_VARARGS) MND(marked_cells, METH_NOARGS) MND(scroll_to_next_mark, METH_VARARGS) + MND(update_only_line_graphics_data, METH_NOARGS) {"select_graphic_rendition", (PyCFunction)_select_graphic_rendition, METH_VARARGS, ""}, {NULL} /* Sentinel */ diff --git a/kitty/unicode-data.h b/kitty/unicode-data.h index 62e8cadd0..22cb74623 100644 --- a/kitty/unicode-data.h +++ b/kitty/unicode-data.h @@ -5,6 +5,9 @@ static const combining_type VS15 = 1364, VS16 = 1365; // END_KNOWN_MARKS +// Converts row/column diacritics to numbers. +int diacritic_to_num(char_type ch); + bool is_combining_char(char_type ch); bool is_ignored_char(char_type ch); bool is_word_char(char_type ch); diff --git a/kitty_tests/graphics.py b/kitty_tests/graphics.py index 52b41f893..d0d96eadf 100644 --- a/kitty_tests/graphics.py +++ b/kitty_tests/graphics.py @@ -114,21 +114,21 @@ def sl(payload, **kw): return s, g, pl, sl -def put_helpers(self, cw, ch): +def put_helpers(self, cw, ch, cols=10, lines=5): iid = 0 def create_screen(): - s = self.create_screen(10, 5, cell_width=cw, cell_height=ch) + s = self.create_screen(cols, lines, cell_width=cw, cell_height=ch) return s, 2 / s.columns, 2 / s.lines def put_cmd( z=0, num_cols=0, num_lines=0, x_off=0, y_off=0, width=0, height=0, cell_x_off=0, cell_y_off=0, placement_id=0, - cursor_movement=0 + cursor_movement=0, unicode_placeholder=0 ): - return 'z=%d,c=%d,r=%d,x=%d,y=%d,w=%d,h=%d,X=%d,Y=%d,p=%d,C=%d' % ( + return 'z=%d,c=%d,r=%d,x=%d,y=%d,w=%d,h=%d,X=%d,Y=%d,p=%d,C=%d,U=%d' % ( z, num_cols, num_lines, x_off, y_off, width, height, cell_x_off, - cell_y_off, placement_id, cursor_movement + cell_y_off, placement_id, cursor_movement, unicode_placeholder ) def put_image(screen, w, h, **kw): @@ -515,6 +515,232 @@ def test_image_put(self): s.reset() self.assertEqual(s.grman.disk_cache.total_size, 0) + def test_unicode_placeholders(self): + # This test tests basic image placement using using unicode placeholders + cw, ch = 10, 20 + s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch) + # Upload two images. + put_image(s, 20, 20, num_cols=4, num_lines=2, unicode_placeholder=1, id=42) + put_image(s, 10, 20, num_cols=4, num_lines=2, unicode_placeholder=1, id=(42<<16) + (43<<8) + 44) + # The references are virtual, so no visible refs yet. + s.update_only_line_graphics_data() + refs = layers(s) + self.ae(len(refs), 0) + # A reminder of row/column diacritics meaning (assuming 0-based): + # \u0305 -> 0 + # \u030D -> 1 + # \u030E -> 2 + # \u0310 -> 3 + # Now print the placeholders for the first image. + # Encode the id as an 8-bit color. + s.apply_sgr("38;5;42") + # These two characters will become one 2x1 ref. + s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030D") + # These two characters will be two separate refs (not contiguous). + s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030E") + s.cursor_back(4) + s.update_only_line_graphics_data() + refs = layers(s) + self.ae(len(refs), 3) + self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 0.5, 'bottom': 0.5}) + self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 0.25, 'bottom': 0.5}) + self.ae(refs[2]['src_rect'], {'left': 0.5, 'top': 0.0, 'right': 0.75, 'bottom': 0.5}) + # Erase the line. + s.erase_in_line(2) + # There must be 0 refs after the line is erased. + s.update_only_line_graphics_data() + refs = layers(s) + self.ae(len(refs), 0) + # Now test encoding IDs with the 24-bit color. + # The first image, 1x1 + s.apply_sgr("38;2;0;0;42") + s.draw("\U0010EEEE\u0305\u0305") + # The second image, 2x1 + s.apply_sgr("38;2;42;43;44") + s.draw("\U0010EEEE\u0305\u030D\U0010EEEE\u0305\u030E") + s.cursor_back(2) + s.update_only_line_graphics_data() + refs = layers(s) + self.ae(len(refs), 2) + self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 0.25, 'bottom': 0.5}) + # The second ref spans the whole widths of the second image because it's + # fit to height and centered in a 4x2 box (specified in put_image). + self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.5}) + # Erase the line. + s.erase_in_line(2) + # Now test implicit column numbers. + # We will mix implicit and explicit column/row specifications, but they + # will be combine into just two references. + s.apply_sgr("38;5;42") + # full row 0 of the first image + s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\U0010EEEE\U0010EEEE\u0305") + # full row 1 of the first image + s.draw("\U0010EEEE\u030D\U0010EEEE\U0010EEEE\U0010EEEE\u030D\u0310") + s.cursor_back(8) + s.update_only_line_graphics_data() + refs = layers(s) + self.ae(len(refs), 2) + self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.5}) + self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.5, 'right': 1.0, 'bottom': 1.0}) + + def test_unicode_placeholders_3rd_combining_char(self): + # This test tests that we can use the 3rd diacritic for the most + # significant byte + cw, ch = 10, 20 + s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch) + # Upload two images. + put_image(s, 20, 20, num_cols=4, num_lines=2, unicode_placeholder=1, id=42) + put_image(s, 20, 10, num_cols=4, num_lines=1, unicode_placeholder=1, id=(42 << 24) + 43) + # This one will have id=43, which does not exist. + s.apply_sgr("38;2;0;0;43") + s.draw("\U0010EEEE\u0305\U0010EEEE\U0010EEEE\U0010EEEE") + s.cursor_back(4) + s.update_only_line_graphics_data() + refs = layers(s) + self.ae(len(refs), 0) + s.erase_in_line(2) + # This one will have id=42. We explicitly specify that the most + # significant byte is 0 (third \u305). Specifying the zero byte like + # this is not necessary but is correct. + s.apply_sgr("38;2;0;0;42") + s.draw("\U0010EEEE\u0305\u0305\u0305\U0010EEEE\u0305\u030D\u0305") + # This is the second image. + # \u059C -> 42 + s.apply_sgr("38;2;0;0;43") + s.draw("\U0010EEEE\u0305\u0305\u059C\U0010EEEE\u0305\u030D\u059C") + # Check that we can continue by using implicit row/column specification. + s.draw("\U0010EEEE\u0305\U0010EEEE") + s.cursor_back(6) + s.update_only_line_graphics_data() + refs = layers(s) + self.ae(len(refs), 2) + self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 0.5, 'bottom': 0.5}) + self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.0}) + s.erase_in_line(2) + # Now test the 8-bit color mode. Using the third diacritic, we can + # specify 16 bits: the most significant byte and the least significant + # byte. + s.apply_sgr("38;5;42") + s.draw("\U0010EEEE\u0305\u0305\u0305\U0010EEEE") + s.apply_sgr("38;5;43") + s.draw("\U0010EEEE\u0305\u0305\u059C\U0010EEEE\U0010EEEE\u0305\U0010EEEE") + s.cursor_back(6) + s.update_only_line_graphics_data() + refs = layers(s) + self.ae(len(refs), 2) + self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 0.5, 'bottom': 0.5}) + self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.0}) + + def test_unicode_placeholders_multiple_placements(self): + # Here we test placement specification via underline color. + cw, ch = 10, 20 + s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch) + put_image(s, 20, 20, num_cols=1, num_lines=1, placement_id=1, unicode_placeholder=1, id=42) + put_ref(s, id=42, num_cols=2, num_lines=1, placement_id=22, unicode_placeholder=1) + put_ref(s, id=42, num_cols=4, num_lines=2, placement_id=44, unicode_placeholder=1) + # The references are virtual, so no visible refs yet. + s.update_only_line_graphics_data() + refs = layers(s) + self.ae(len(refs), 0) + # Draw the first row of each placement. + s.apply_sgr("38;5;42") + s.apply_sgr("58;5;1") + s.draw("\U0010EEEE\u0305") + s.apply_sgr("58;5;22") + s.draw("\U0010EEEE\u0305\U0010EEEE\u0305") + s.apply_sgr("58;5;44") + s.draw("\U0010EEEE\u0305\U0010EEEE\u0305\U0010EEEE\u0305\U0010EEEE\u0305") + s.update_only_line_graphics_data() + refs = layers(s) + self.ae(len(refs), 3) + self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.5}) + self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.0}) + self.ae(refs[2]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.5}) + + def test_unicode_placeholders_scroll(self): + # Here we test scrolling of a region. We'll draw an image spanning 8 + # rows and then scroll only the middle part of this image. Each + # reference corresponds to one row. + cw, ch = 5, 10 + s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch, lines=8) + put_image(s, 5, 80, num_cols=1, num_lines=8, unicode_placeholder=1, id=42) + s.apply_sgr("38;5;42") + s.cursor_position(1, 0) + s.draw("\U0010EEEE\u0305\n") + s.cursor_position(2, 0) + s.draw("\U0010EEEE\u030D\n") + s.cursor_position(3, 0) + s.draw("\U0010EEEE\u030E\n") + s.cursor_position(4, 0) + s.draw("\U0010EEEE\u0310\n") + s.cursor_position(5, 0) + s.draw("\U0010EEEE\u0312\n") + s.cursor_position(6, 0) + s.draw("\U0010EEEE\u033D\n") + s.cursor_position(7, 0) + s.draw("\U0010EEEE\u033E\n") + s.cursor_position(8, 0) + s.draw("\U0010EEEE\u033F") + # Each line will contain a part of the image. + s.update_only_line_graphics_data() + refs = layers(s) + refs = sorted(refs, key=lambda r: r['src_rect']['top']) + self.ae(len(refs), 8) + for i in range(8): + self.ae(refs[i]['src_rect'], {'left': 0.0, 'top': 0.125*i, 'right': 1.0, 'bottom': 0.125*(i + 1)}) + self.ae(refs[i]['dest_rect']['top'], 1 - 0.25*i) + # Now set margins to lines 3 and 6. + s.set_margins(3, 6) # 1-based indexing + # Scroll two lines down (i.e. move lines 3..6 up). + # Lines 3 and 4 will be erased. + s.cursor_position(6, 0) + s.index() + s.index() + s.update_only_line_graphics_data() + refs = layers(s) + refs = sorted(refs, key=lambda r: r['src_rect']['top']) + self.ae(len(refs), 6) + # Lines 1 and 2 are outside of the region, not scrolled. + self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.125}) + self.ae(refs[0]['dest_rect']['top'], 1.0) + self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.125*1, 'right': 1.0, 'bottom': 0.125*2}) + self.ae(refs[1]['dest_rect']['top'], 1.0 - 0.25*1) + # Lines 3 and 4 are erased. + # Lines 5 and 6 are now higher. + self.ae(refs[2]['src_rect'], {'left': 0.0, 'top': 0.125*4, 'right': 1.0, 'bottom': 0.125*5}) + self.ae(refs[2]['dest_rect']['top'], 1.0 - 0.25*2) + self.ae(refs[3]['src_rect'], {'left': 0.0, 'top': 0.125*5, 'right': 1.0, 'bottom': 0.125*6}) + self.ae(refs[3]['dest_rect']['top'], 1.0 - 0.25*3) + # Lines 7 and 8 are outside of the region. + self.ae(refs[4]['src_rect'], {'left': 0.0, 'top': 0.125*6, 'right': 1.0, 'bottom': 0.125*7}) + self.ae(refs[4]['dest_rect']['top'], 1.0 - 0.25*6) + self.ae(refs[5]['src_rect'], {'left': 0.0, 'top': 0.125*7, 'right': 1.0, 'bottom': 0.125*8}) + self.ae(refs[5]['dest_rect']['top'], 1.0 - 0.25*7) + # Now scroll three lines up (i.e. move lines 5..6 down). + # Line 6 will be erased. + s.cursor_position(3, 0) + s.reverse_index() + s.reverse_index() + s.reverse_index() + s.update_only_line_graphics_data() + refs = layers(s) + refs = sorted(refs, key=lambda r: r['src_rect']['top']) + self.ae(len(refs), 5) + # Lines 1 and 2 are outside of the region, not scrolled. + self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.125}) + self.ae(refs[0]['dest_rect']['top'], 1.0) + self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.125*1, 'right': 1.0, 'bottom': 0.125*2}) + self.ae(refs[1]['dest_rect']['top'], 1.0 - 0.25*1) + # Lines 3, 4 and 6 are erased. + # Line 5 is now lower. + self.ae(refs[2]['src_rect'], {'left': 0.0, 'top': 0.125*4, 'right': 1.0, 'bottom': 0.125*5}) + self.ae(refs[2]['dest_rect']['top'], 1.0 - 0.25*5) + # Lines 7 and 8 are outside of the region. + self.ae(refs[3]['src_rect'], {'left': 0.0, 'top': 0.125*6, 'right': 1.0, 'bottom': 0.125*7}) + self.ae(refs[3]['dest_rect']['top'], 1.0 - 0.25*6) + self.ae(refs[4]['src_rect'], {'left': 0.0, 'top': 0.125*7, 'right': 1.0, 'bottom': 0.125*8}) + self.ae(refs[4]['dest_rect']['top'], 1.0 - 0.25*7) + def test_gr_scroll(self): cw, ch = 10, 20 s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch) diff --git a/kitty_tests/parser.py b/kitty_tests/parser.py index 6a5130b0f..c6d3486d9 100644 --- a/kitty_tests/parser.py +++ b/kitty_tests/parser.py @@ -410,7 +410,7 @@ def c(**k): for f in 'action delete_action transmission_type compressed'.split(): k.setdefault(f, b'\0') for f in ('format more id data_sz data_offset width height x_offset y_offset data_height data_width cursor_movement' - ' num_cells num_lines cell_x_offset cell_y_offset z_index placement_id image_number quiet').split(): + ' num_cells num_lines cell_x_offset cell_y_offset z_index placement_id image_number quiet unicode_placement').split(): k.setdefault(f, 0) p = k.pop('payload', '').encode('utf-8') k['payload_sz'] = len(p) diff --git a/rowcolumn-diacritics.txt b/rowcolumn-diacritics.txt new file mode 100644 index 000000000..9d8091e37 --- /dev/null +++ b/rowcolumn-diacritics.txt @@ -0,0 +1,310 @@ +# This file lists the diacritics used to indicate row/column numbers for +# Unicode terminal image placeholders. It is derived from UnicodeData.txt for +# Unicode 6.0.0 (chosen somewhat arbitrarily: it's old enough, but still +# contains more than 255 suitable combining chars) using the following +# command: +# +# cat UnicodeData.txt | grep "Mn;230;NSM;;" | grep -v "0300\|0301\|0302\|0303\|0304\|0306\|0307\|0308\|0309\|030A\|030B\|030C\|030F\|0311\|0313\|0314\|0342\|0653\|0654" +# +# That is, we use combining chars of the same combining class 230 (above the +# base character) that do not have decomposition mappings, and we also remove +# some characters that may be fused with other characters during normalization, +# like 0041 0300 -> 00C0 which is À (A with grave). +# +0305;COMBINING OVERLINE;Mn;230;NSM;;;;;N;NON-SPACING OVERSCORE;;;; +030D;COMBINING VERTICAL LINE ABOVE;Mn;230;NSM;;;;;N;NON-SPACING VERTICAL LINE ABOVE;;;; +030E;COMBINING DOUBLE VERTICAL LINE ABOVE;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE VERTICAL LINE ABOVE;;;; +0310;COMBINING CANDRABINDU;Mn;230;NSM;;;;;N;NON-SPACING CANDRABINDU;;;; +0312;COMBINING TURNED COMMA ABOVE;Mn;230;NSM;;;;;N;NON-SPACING TURNED COMMA ABOVE;;;; +033D;COMBINING X ABOVE;Mn;230;NSM;;;;;N;NON-SPACING X ABOVE;;;; +033E;COMBINING VERTICAL TILDE;Mn;230;NSM;;;;;N;NON-SPACING VERTICAL TILDE;;;; +033F;COMBINING DOUBLE OVERLINE;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE OVERSCORE;;;; +0346;COMBINING BRIDGE ABOVE;Mn;230;NSM;;;;;N;;;;; +034A;COMBINING NOT TILDE ABOVE;Mn;230;NSM;;;;;N;;;;; +034B;COMBINING HOMOTHETIC ABOVE;Mn;230;NSM;;;;;N;;;;; +034C;COMBINING ALMOST EQUAL TO ABOVE;Mn;230;NSM;;;;;N;;;;; +0350;COMBINING RIGHT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; +0351;COMBINING LEFT HALF RING ABOVE;Mn;230;NSM;;;;;N;;;;; +0352;COMBINING FERMATA;Mn;230;NSM;;;;;N;;;;; +0357;COMBINING RIGHT HALF RING ABOVE;Mn;230;NSM;;;;;N;;;;; +035B;COMBINING ZIGZAG ABOVE;Mn;230;NSM;;;;;N;;;;; +0363;COMBINING LATIN SMALL LETTER A;Mn;230;NSM;;;;;N;;;;; +0364;COMBINING LATIN SMALL LETTER E;Mn;230;NSM;;;;;N;;;;; +0365;COMBINING LATIN SMALL LETTER I;Mn;230;NSM;;;;;N;;;;; +0366;COMBINING LATIN SMALL LETTER O;Mn;230;NSM;;;;;N;;;;; +0367;COMBINING LATIN SMALL LETTER U;Mn;230;NSM;;;;;N;;;;; +0368;COMBINING LATIN SMALL LETTER C;Mn;230;NSM;;;;;N;;;;; +0369;COMBINING LATIN SMALL LETTER D;Mn;230;NSM;;;;;N;;;;; +036A;COMBINING LATIN SMALL LETTER H;Mn;230;NSM;;;;;N;;;;; +036B;COMBINING LATIN SMALL LETTER M;Mn;230;NSM;;;;;N;;;;; +036C;COMBINING LATIN SMALL LETTER R;Mn;230;NSM;;;;;N;;;;; +036D;COMBINING LATIN SMALL LETTER T;Mn;230;NSM;;;;;N;;;;; +036E;COMBINING LATIN SMALL LETTER V;Mn;230;NSM;;;;;N;;;;; +036F;COMBINING LATIN SMALL LETTER X;Mn;230;NSM;;;;;N;;;;; +0483;COMBINING CYRILLIC TITLO;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING TITLO;;;; +0484;COMBINING CYRILLIC PALATALIZATION;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING PALATALIZATION;;;; +0485;COMBINING CYRILLIC DASIA PNEUMATA;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING DASIA PNEUMATA;;;; +0486;COMBINING CYRILLIC PSILI PNEUMATA;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING PSILI PNEUMATA;;;; +0487;COMBINING CYRILLIC POKRYTIE;Mn;230;NSM;;;;;N;;;;; +0592;HEBREW ACCENT SEGOL;Mn;230;NSM;;;;;N;;;;; +0593;HEBREW ACCENT SHALSHELET;Mn;230;NSM;;;;;N;;;;; +0594;HEBREW ACCENT ZAQEF QATAN;Mn;230;NSM;;;;;N;;;;; +0595;HEBREW ACCENT ZAQEF GADOL;Mn;230;NSM;;;;;N;;;;; +0597;HEBREW ACCENT REVIA;Mn;230;NSM;;;;;N;;;;; +0598;HEBREW ACCENT ZARQA;Mn;230;NSM;;;;;N;;;;; +0599;HEBREW ACCENT PASHTA;Mn;230;NSM;;;;;N;;;;; +059C;HEBREW ACCENT GERESH;Mn;230;NSM;;;;;N;;;;; +059D;HEBREW ACCENT GERESH MUQDAM;Mn;230;NSM;;;;;N;;;;; +059E;HEBREW ACCENT GERSHAYIM;Mn;230;NSM;;;;;N;;;;; +059F;HEBREW ACCENT QARNEY PARA;Mn;230;NSM;;;;;N;;;;; +05A0;HEBREW ACCENT TELISHA GEDOLA;Mn;230;NSM;;;;;N;;;;; +05A1;HEBREW ACCENT PAZER;Mn;230;NSM;;;;;N;;;;; +05A8;HEBREW ACCENT QADMA;Mn;230;NSM;;;;;N;;;;; +05A9;HEBREW ACCENT TELISHA QETANA;Mn;230;NSM;;;;;N;;;;; +05AB;HEBREW ACCENT OLE;Mn;230;NSM;;;;;N;;;;; +05AC;HEBREW ACCENT ILUY;Mn;230;NSM;;;;;N;;;;; +05AF;HEBREW MARK MASORA CIRCLE;Mn;230;NSM;;;;;N;;;;; +05C4;HEBREW MARK UPPER DOT;Mn;230;NSM;;;;;N;;;;; +0610;ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM;Mn;230;NSM;;;;;N;;;;; +0611;ARABIC SIGN ALAYHE ASSALLAM;Mn;230;NSM;;;;;N;;;;; +0612;ARABIC SIGN RAHMATULLAH ALAYHE;Mn;230;NSM;;;;;N;;;;; +0613;ARABIC SIGN RADI ALLAHOU ANHU;Mn;230;NSM;;;;;N;;;;; +0614;ARABIC SIGN TAKHALLUS;Mn;230;NSM;;;;;N;;;;; +0615;ARABIC SMALL HIGH TAH;Mn;230;NSM;;;;;N;;;;; +0616;ARABIC SMALL HIGH LIGATURE ALEF WITH LAM WITH YEH;Mn;230;NSM;;;;;N;;;;; +0617;ARABIC SMALL HIGH ZAIN;Mn;230;NSM;;;;;N;;;;; +0657;ARABIC INVERTED DAMMA;Mn;230;NSM;;;;;N;;;;; +0658;ARABIC MARK NOON GHUNNA;Mn;230;NSM;;;;;N;;;;; +0659;ARABIC ZWARAKAY;Mn;230;NSM;;;;;N;;;;; +065A;ARABIC VOWEL SIGN SMALL V ABOVE;Mn;230;NSM;;;;;N;;;;; +065B;ARABIC VOWEL SIGN INVERTED SMALL V ABOVE;Mn;230;NSM;;;;;N;;;;; +065D;ARABIC REVERSED DAMMA;Mn;230;NSM;;;;;N;;;;; +065E;ARABIC FATHA WITH TWO DOTS;Mn;230;NSM;;;;;N;;;;; +06D6;ARABIC SMALL HIGH LIGATURE SAD WITH LAM WITH ALEF MAKSURA;Mn;230;NSM;;;;;N;;;;; +06D7;ARABIC SMALL HIGH LIGATURE QAF WITH LAM WITH ALEF MAKSURA;Mn;230;NSM;;;;;N;;;;; +06D8;ARABIC SMALL HIGH MEEM INITIAL FORM;Mn;230;NSM;;;;;N;;;;; +06D9;ARABIC SMALL HIGH LAM ALEF;Mn;230;NSM;;;;;N;;;;; +06DA;ARABIC SMALL HIGH JEEM;Mn;230;NSM;;;;;N;;;;; +06DB;ARABIC SMALL HIGH THREE DOTS;Mn;230;NSM;;;;;N;;;;; +06DC;ARABIC SMALL HIGH SEEN;Mn;230;NSM;;;;;N;;;;; +06DF;ARABIC SMALL HIGH ROUNDED ZERO;Mn;230;NSM;;;;;N;;;;; +06E0;ARABIC SMALL HIGH UPRIGHT RECTANGULAR ZERO;Mn;230;NSM;;;;;N;;;;; +06E1;ARABIC SMALL HIGH DOTLESS HEAD OF KHAH;Mn;230;NSM;;;;;N;;;;; +06E2;ARABIC SMALL HIGH MEEM ISOLATED FORM;Mn;230;NSM;;;;;N;;;;; +06E4;ARABIC SMALL HIGH MADDA;Mn;230;NSM;;;;;N;;;;; +06E7;ARABIC SMALL HIGH YEH;Mn;230;NSM;;;;;N;;;;; +06E8;ARABIC SMALL HIGH NOON;Mn;230;NSM;;;;;N;;;;; +06EB;ARABIC EMPTY CENTRE HIGH STOP;Mn;230;NSM;;;;;N;;;;; +06EC;ARABIC ROUNDED HIGH STOP WITH FILLED CENTRE;Mn;230;NSM;;;;;N;;;;; +0730;SYRIAC PTHAHA ABOVE;Mn;230;NSM;;;;;N;;;;; +0732;SYRIAC PTHAHA DOTTED;Mn;230;NSM;;;;;N;;;;; +0733;SYRIAC ZQAPHA ABOVE;Mn;230;NSM;;;;;N;;;;; +0735;SYRIAC ZQAPHA DOTTED;Mn;230;NSM;;;;;N;;;;; +0736;SYRIAC RBASA ABOVE;Mn;230;NSM;;;;;N;;;;; +073A;SYRIAC HBASA ABOVE;Mn;230;NSM;;;;;N;;;;; +073D;SYRIAC ESASA ABOVE;Mn;230;NSM;;;;;N;;;;; +073F;SYRIAC RWAHA;Mn;230;NSM;;;;;N;;;;; +0740;SYRIAC FEMININE DOT;Mn;230;NSM;;;;;N;;;;; +0741;SYRIAC QUSHSHAYA;Mn;230;NSM;;;;;N;;;;; +0743;SYRIAC TWO VERTICAL DOTS ABOVE;Mn;230;NSM;;;;;N;;;;; +0745;SYRIAC THREE DOTS ABOVE;Mn;230;NSM;;;;;N;;;;; +0747;SYRIAC OBLIQUE LINE ABOVE;Mn;230;NSM;;;;;N;;;;; +0749;SYRIAC MUSIC;Mn;230;NSM;;;;;N;;;;; +074A;SYRIAC BARREKH;Mn;230;NSM;;;;;N;;;;; +07EB;NKO COMBINING SHORT HIGH TONE;Mn;230;NSM;;;;;N;;;;; +07EC;NKO COMBINING SHORT LOW TONE;Mn;230;NSM;;;;;N;;;;; +07ED;NKO COMBINING SHORT RISING TONE;Mn;230;NSM;;;;;N;;;;; +07EE;NKO COMBINING LONG DESCENDING TONE;Mn;230;NSM;;;;;N;;;;; +07EF;NKO COMBINING LONG HIGH TONE;Mn;230;NSM;;;;;N;;;;; +07F0;NKO COMBINING LONG LOW TONE;Mn;230;NSM;;;;;N;;;;; +07F1;NKO COMBINING LONG RISING TONE;Mn;230;NSM;;;;;N;;;;; +07F3;NKO COMBINING DOUBLE DOT ABOVE;Mn;230;NSM;;;;;N;;;;; +0816;SAMARITAN MARK IN;Mn;230;NSM;;;;;N;;;;; +0817;SAMARITAN MARK IN-ALAF;Mn;230;NSM;;;;;N;;;;; +0818;SAMARITAN MARK OCCLUSION;Mn;230;NSM;;;;;N;;;;; +0819;SAMARITAN MARK DAGESH;Mn;230;NSM;;;;;N;;;;; +081B;SAMARITAN MARK EPENTHETIC YUT;Mn;230;NSM;;;;;N;;;;; +081C;SAMARITAN VOWEL SIGN LONG E;Mn;230;NSM;;;;;N;;;;; +081D;SAMARITAN VOWEL SIGN E;Mn;230;NSM;;;;;N;;;;; +081E;SAMARITAN VOWEL SIGN OVERLONG AA;Mn;230;NSM;;;;;N;;;;; +081F;SAMARITAN VOWEL SIGN LONG AA;Mn;230;NSM;;;;;N;;;;; +0820;SAMARITAN VOWEL SIGN AA;Mn;230;NSM;;;;;N;;;;; +0821;SAMARITAN VOWEL SIGN OVERLONG A;Mn;230;NSM;;;;;N;;;;; +0822;SAMARITAN VOWEL SIGN LONG A;Mn;230;NSM;;;;;N;;;;; +0823;SAMARITAN VOWEL SIGN A;Mn;230;NSM;;;;;N;;;;; +0825;SAMARITAN VOWEL SIGN SHORT A;Mn;230;NSM;;;;;N;;;;; +0826;SAMARITAN VOWEL SIGN LONG U;Mn;230;NSM;;;;;N;;;;; +0827;SAMARITAN VOWEL SIGN U;Mn;230;NSM;;;;;N;;;;; +0829;SAMARITAN VOWEL SIGN LONG I;Mn;230;NSM;;;;;N;;;;; +082A;SAMARITAN VOWEL SIGN I;Mn;230;NSM;;;;;N;;;;; +082B;SAMARITAN VOWEL SIGN O;Mn;230;NSM;;;;;N;;;;; +082C;SAMARITAN VOWEL SIGN SUKUN;Mn;230;NSM;;;;;N;;;;; +082D;SAMARITAN MARK NEQUDAA;Mn;230;NSM;;;;;N;;;;; +0951;DEVANAGARI STRESS SIGN UDATTA;Mn;230;NSM;;;;;N;;;;; +0953;DEVANAGARI GRAVE ACCENT;Mn;230;NSM;;;;;N;;;;; +0954;DEVANAGARI ACUTE ACCENT;Mn;230;NSM;;;;;N;;;;; +0F82;TIBETAN SIGN NYI ZLA NAA DA;Mn;230;NSM;;;;;N;TIBETAN CANDRABINDU WITH ORNAMENT;;;; +0F83;TIBETAN SIGN SNA LDAN;Mn;230;NSM;;;;;N;TIBETAN CANDRABINDU;;;; +0F86;TIBETAN SIGN LCI RTAGS;Mn;230;NSM;;;;;N;;;;; +0F87;TIBETAN SIGN YANG RTAGS;Mn;230;NSM;;;;;N;;;;; +135D;ETHIOPIC COMBINING GEMINATION AND VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;; +135E;ETHIOPIC COMBINING VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;; +135F;ETHIOPIC COMBINING GEMINATION MARK;Mn;230;NSM;;;;;N;;;;; +17DD;KHMER SIGN ATTHACAN;Mn;230;NSM;;;;;N;;;;; +193A;LIMBU SIGN KEMPHRENG;Mn;230;NSM;;;;;N;;;;; +1A17;BUGINESE VOWEL SIGN I;Mn;230;NSM;;;;;N;;;;; +1A75;TAI THAM SIGN TONE-1;Mn;230;NSM;;;;;N;;;;; +1A76;TAI THAM SIGN TONE-2;Mn;230;NSM;;;;;N;;;;; +1A77;TAI THAM SIGN KHUEN TONE-3;Mn;230;NSM;;;;;N;;;;; +1A78;TAI THAM SIGN KHUEN TONE-4;Mn;230;NSM;;;;;N;;;;; +1A79;TAI THAM SIGN KHUEN TONE-5;Mn;230;NSM;;;;;N;;;;; +1A7A;TAI THAM SIGN RA HAAM;Mn;230;NSM;;;;;N;;;;; +1A7B;TAI THAM SIGN MAI SAM;Mn;230;NSM;;;;;N;;;;; +1A7C;TAI THAM SIGN KHUEN-LUE KARAN;Mn;230;NSM;;;;;N;;;;; +1B6B;BALINESE MUSICAL SYMBOL COMBINING TEGEH;Mn;230;NSM;;;;;N;;;;; +1B6D;BALINESE MUSICAL SYMBOL COMBINING KEMPUL;Mn;230;NSM;;;;;N;;;;; +1B6E;BALINESE MUSICAL SYMBOL COMBINING KEMPLI;Mn;230;NSM;;;;;N;;;;; +1B6F;BALINESE MUSICAL SYMBOL COMBINING JEGOGAN;Mn;230;NSM;;;;;N;;;;; +1B70;BALINESE MUSICAL SYMBOL COMBINING KEMPUL WITH JEGOGAN;Mn;230;NSM;;;;;N;;;;; +1B71;BALINESE MUSICAL SYMBOL COMBINING KEMPLI WITH JEGOGAN;Mn;230;NSM;;;;;N;;;;; +1B72;BALINESE MUSICAL SYMBOL COMBINING BENDE;Mn;230;NSM;;;;;N;;;;; +1B73;BALINESE MUSICAL SYMBOL COMBINING GONG;Mn;230;NSM;;;;;N;;;;; +1CD0;VEDIC TONE KARSHANA;Mn;230;NSM;;;;;N;;;;; +1CD1;VEDIC TONE SHARA;Mn;230;NSM;;;;;N;;;;; +1CD2;VEDIC TONE PRENKHA;Mn;230;NSM;;;;;N;;;;; +1CDA;VEDIC TONE DOUBLE SVARITA;Mn;230;NSM;;;;;N;;;;; +1CDB;VEDIC TONE TRIPLE SVARITA;Mn;230;NSM;;;;;N;;;;; +1CE0;VEDIC TONE RIGVEDIC KASHMIRI INDEPENDENT SVARITA;Mn;230;NSM;;;;;N;;;;; +1DC0;COMBINING DOTTED GRAVE ACCENT;Mn;230;NSM;;;;;N;;;;; +1DC1;COMBINING DOTTED ACUTE ACCENT;Mn;230;NSM;;;;;N;;;;; +1DC3;COMBINING SUSPENSION MARK;Mn;230;NSM;;;;;N;;;;; +1DC4;COMBINING MACRON-ACUTE;Mn;230;NSM;;;;;N;;;;; +1DC5;COMBINING GRAVE-MACRON;Mn;230;NSM;;;;;N;;;;; +1DC6;COMBINING MACRON-GRAVE;Mn;230;NSM;;;;;N;;;;; +1DC7;COMBINING ACUTE-MACRON;Mn;230;NSM;;;;;N;;;;; +1DC8;COMBINING GRAVE-ACUTE-GRAVE;Mn;230;NSM;;;;;N;;;;; +1DC9;COMBINING ACUTE-GRAVE-ACUTE;Mn;230;NSM;;;;;N;;;;; +1DCB;COMBINING BREVE-MACRON;Mn;230;NSM;;;;;N;;;;; +1DCC;COMBINING MACRON-BREVE;Mn;230;NSM;;;;;N;;;;; +1DD1;COMBINING UR ABOVE;Mn;230;NSM;;;;;N;;;;; +1DD2;COMBINING US ABOVE;Mn;230;NSM;;;;;N;;;;; +1DD3;COMBINING LATIN SMALL LETTER FLATTENED OPEN A ABOVE;Mn;230;NSM;;;;;N;;;;; +1DD4;COMBINING LATIN SMALL LETTER AE;Mn;230;NSM;;;;;N;;;;; +1DD5;COMBINING LATIN SMALL LETTER AO;Mn;230;NSM;;;;;N;;;;; +1DD6;COMBINING LATIN SMALL LETTER AV;Mn;230;NSM;;;;;N;;;;; +1DD7;COMBINING LATIN SMALL LETTER C CEDILLA;Mn;230;NSM;;;;;N;;;;; +1DD8;COMBINING LATIN SMALL LETTER INSULAR D;Mn;230;NSM;;;;;N;;;;; +1DD9;COMBINING LATIN SMALL LETTER ETH;Mn;230;NSM;;;;;N;;;;; +1DDA;COMBINING LATIN SMALL LETTER G;Mn;230;NSM;;;;;N;;;;; +1DDB;COMBINING LATIN LETTER SMALL CAPITAL G;Mn;230;NSM;;;;;N;;;;; +1DDC;COMBINING LATIN SMALL LETTER K;Mn;230;NSM;;;;;N;;;;; +1DDD;COMBINING LATIN SMALL LETTER L;Mn;230;NSM;;;;;N;;;;; +1DDE;COMBINING LATIN LETTER SMALL CAPITAL L;Mn;230;NSM;;;;;N;;;;; +1DDF;COMBINING LATIN LETTER SMALL CAPITAL M;Mn;230;NSM;;;;;N;;;;; +1DE0;COMBINING LATIN SMALL LETTER N;Mn;230;NSM;;;;;N;;;;; +1DE1;COMBINING LATIN LETTER SMALL CAPITAL N;Mn;230;NSM;;;;;N;;;;; +1DE2;COMBINING LATIN LETTER SMALL CAPITAL R;Mn;230;NSM;;;;;N;;;;; +1DE3;COMBINING LATIN SMALL LETTER R ROTUNDA;Mn;230;NSM;;;;;N;;;;; +1DE4;COMBINING LATIN SMALL LETTER S;Mn;230;NSM;;;;;N;;;;; +1DE5;COMBINING LATIN SMALL LETTER LONG S;Mn;230;NSM;;;;;N;;;;; +1DE6;COMBINING LATIN SMALL LETTER Z;Mn;230;NSM;;;;;N;;;;; +1DFE;COMBINING LEFT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; +20D0;COMBINING LEFT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT HARPOON ABOVE;;;; +20D1;COMBINING RIGHT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RIGHT HARPOON ABOVE;;;; +20D4;COMBINING ANTICLOCKWISE ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING ANTICLOCKWISE ARROW ABOVE;;;; +20D5;COMBINING CLOCKWISE ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING CLOCKWISE ARROW ABOVE;;;; +20D6;COMBINING LEFT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT ARROW ABOVE;;;; +20D7;COMBINING RIGHT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RIGHT ARROW ABOVE;;;; +20DB;COMBINING THREE DOTS ABOVE;Mn;230;NSM;;;;;N;NON-SPACING THREE DOTS ABOVE;;;; +20DC;COMBINING FOUR DOTS ABOVE;Mn;230;NSM;;;;;N;NON-SPACING FOUR DOTS ABOVE;;;; +20E1;COMBINING LEFT RIGHT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT RIGHT ARROW ABOVE;;;; +20E7;COMBINING ANNUITY SYMBOL;Mn;230;NSM;;;;;N;;;;; +20E9;COMBINING WIDE BRIDGE ABOVE;Mn;230;NSM;;;;;N;;;;; +20F0;COMBINING ASTERISK ABOVE;Mn;230;NSM;;;;;N;;;;; +2CEF;COPTIC COMBINING NI ABOVE;Mn;230;NSM;;;;;N;;;;; +2CF0;COPTIC COMBINING SPIRITUS ASPER;Mn;230;NSM;;;;;N;;;;; +2CF1;COPTIC COMBINING SPIRITUS LENIS;Mn;230;NSM;;;;;N;;;;; +2DE0;COMBINING CYRILLIC LETTER BE;Mn;230;NSM;;;;;N;;;;; +2DE1;COMBINING CYRILLIC LETTER VE;Mn;230;NSM;;;;;N;;;;; +2DE2;COMBINING CYRILLIC LETTER GHE;Mn;230;NSM;;;;;N;;;;; +2DE3;COMBINING CYRILLIC LETTER DE;Mn;230;NSM;;;;;N;;;;; +2DE4;COMBINING CYRILLIC LETTER ZHE;Mn;230;NSM;;;;;N;;;;; +2DE5;COMBINING CYRILLIC LETTER ZE;Mn;230;NSM;;;;;N;;;;; +2DE6;COMBINING CYRILLIC LETTER KA;Mn;230;NSM;;;;;N;;;;; +2DE7;COMBINING CYRILLIC LETTER EL;Mn;230;NSM;;;;;N;;;;; +2DE8;COMBINING CYRILLIC LETTER EM;Mn;230;NSM;;;;;N;;;;; +2DE9;COMBINING CYRILLIC LETTER EN;Mn;230;NSM;;;;;N;;;;; +2DEA;COMBINING CYRILLIC LETTER O;Mn;230;NSM;;;;;N;;;;; +2DEB;COMBINING CYRILLIC LETTER PE;Mn;230;NSM;;;;;N;;;;; +2DEC;COMBINING CYRILLIC LETTER ER;Mn;230;NSM;;;;;N;;;;; +2DED;COMBINING CYRILLIC LETTER ES;Mn;230;NSM;;;;;N;;;;; +2DEE;COMBINING CYRILLIC LETTER TE;Mn;230;NSM;;;;;N;;;;; +2DEF;COMBINING CYRILLIC LETTER HA;Mn;230;NSM;;;;;N;;;;; +2DF0;COMBINING CYRILLIC LETTER TSE;Mn;230;NSM;;;;;N;;;;; +2DF1;COMBINING CYRILLIC LETTER CHE;Mn;230;NSM;;;;;N;;;;; +2DF2;COMBINING CYRILLIC LETTER SHA;Mn;230;NSM;;;;;N;;;;; +2DF3;COMBINING CYRILLIC LETTER SHCHA;Mn;230;NSM;;;;;N;;;;; +2DF4;COMBINING CYRILLIC LETTER FITA;Mn;230;NSM;;;;;N;;;;; +2DF5;COMBINING CYRILLIC LETTER ES-TE;Mn;230;NSM;;;;;N;;;;; +2DF6;COMBINING CYRILLIC LETTER A;Mn;230;NSM;;;;;N;;;;; +2DF7;COMBINING CYRILLIC LETTER IE;Mn;230;NSM;;;;;N;;;;; +2DF8;COMBINING CYRILLIC LETTER DJERV;Mn;230;NSM;;;;;N;;;;; +2DF9;COMBINING CYRILLIC LETTER MONOGRAPH UK;Mn;230;NSM;;;;;N;;;;; +2DFA;COMBINING CYRILLIC LETTER YAT;Mn;230;NSM;;;;;N;;;;; +2DFB;COMBINING CYRILLIC LETTER YU;Mn;230;NSM;;;;;N;;;;; +2DFC;COMBINING CYRILLIC LETTER IOTIFIED A;Mn;230;NSM;;;;;N;;;;; +2DFD;COMBINING CYRILLIC LETTER LITTLE YUS;Mn;230;NSM;;;;;N;;;;; +2DFE;COMBINING CYRILLIC LETTER BIG YUS;Mn;230;NSM;;;;;N;;;;; +2DFF;COMBINING CYRILLIC LETTER IOTIFIED BIG YUS;Mn;230;NSM;;;;;N;;;;; +A66F;COMBINING CYRILLIC VZMET;Mn;230;NSM;;;;;N;;;;; +A67C;COMBINING CYRILLIC KAVYKA;Mn;230;NSM;;;;;N;;;;; +A67D;COMBINING CYRILLIC PAYEROK;Mn;230;NSM;;;;;N;;;;; +A6F0;BAMUM COMBINING MARK KOQNDON;Mn;230;NSM;;;;;N;;;;; +A6F1;BAMUM COMBINING MARK TUKWENTIS;Mn;230;NSM;;;;;N;;;;; +A8E0;COMBINING DEVANAGARI DIGIT ZERO;Mn;230;NSM;;;;;N;;;;; +A8E1;COMBINING DEVANAGARI DIGIT ONE;Mn;230;NSM;;;;;N;;;;; +A8E2;COMBINING DEVANAGARI DIGIT TWO;Mn;230;NSM;;;;;N;;;;; +A8E3;COMBINING DEVANAGARI DIGIT THREE;Mn;230;NSM;;;;;N;;;;; +A8E4;COMBINING DEVANAGARI DIGIT FOUR;Mn;230;NSM;;;;;N;;;;; +A8E5;COMBINING DEVANAGARI DIGIT FIVE;Mn;230;NSM;;;;;N;;;;; +A8E6;COMBINING DEVANAGARI DIGIT SIX;Mn;230;NSM;;;;;N;;;;; +A8E7;COMBINING DEVANAGARI DIGIT SEVEN;Mn;230;NSM;;;;;N;;;;; +A8E8;COMBINING DEVANAGARI DIGIT EIGHT;Mn;230;NSM;;;;;N;;;;; +A8E9;COMBINING DEVANAGARI DIGIT NINE;Mn;230;NSM;;;;;N;;;;; +A8EA;COMBINING DEVANAGARI LETTER A;Mn;230;NSM;;;;;N;;;;; +A8EB;COMBINING DEVANAGARI LETTER U;Mn;230;NSM;;;;;N;;;;; +A8EC;COMBINING DEVANAGARI LETTER KA;Mn;230;NSM;;;;;N;;;;; +A8ED;COMBINING DEVANAGARI LETTER NA;Mn;230;NSM;;;;;N;;;;; +A8EE;COMBINING DEVANAGARI LETTER PA;Mn;230;NSM;;;;;N;;;;; +A8EF;COMBINING DEVANAGARI LETTER RA;Mn;230;NSM;;;;;N;;;;; +A8F0;COMBINING DEVANAGARI LETTER VI;Mn;230;NSM;;;;;N;;;;; +A8F1;COMBINING DEVANAGARI SIGN AVAGRAHA;Mn;230;NSM;;;;;N;;;;; +AAB0;TAI VIET MAI KANG;Mn;230;NSM;;;;;N;;;;; +AAB2;TAI VIET VOWEL I;Mn;230;NSM;;;;;N;;;;; +AAB3;TAI VIET VOWEL UE;Mn;230;NSM;;;;;N;;;;; +AAB7;TAI VIET MAI KHIT;Mn;230;NSM;;;;;N;;;;; +AAB8;TAI VIET VOWEL IA;Mn;230;NSM;;;;;N;;;;; +AABE;TAI VIET VOWEL AM;Mn;230;NSM;;;;;N;;;;; +AABF;TAI VIET TONE MAI EK;Mn;230;NSM;;;;;N;;;;; +AAC1;TAI VIET TONE MAI THO;Mn;230;NSM;;;;;N;;;;; +FE20;COMBINING LIGATURE LEFT HALF;Mn;230;NSM;;;;;N;;;;; +FE21;COMBINING LIGATURE RIGHT HALF;Mn;230;NSM;;;;;N;;;;; +FE22;COMBINING DOUBLE TILDE LEFT HALF;Mn;230;NSM;;;;;N;;;;; +FE23;COMBINING DOUBLE TILDE RIGHT HALF;Mn;230;NSM;;;;;N;;;;; +FE24;COMBINING MACRON LEFT HALF;Mn;230;NSM;;;;;N;;;;; +FE25;COMBINING MACRON RIGHT HALF;Mn;230;NSM;;;;;N;;;;; +FE26;COMBINING CONJOINING MACRON;Mn;230;NSM;;;;;N;;;;; +10A0F;KHAROSHTHI SIGN VISARGA;Mn;230;NSM;;;;;N;;;;; +10A38;KHAROSHTHI SIGN BAR ABOVE;Mn;230;NSM;;;;;N;;;;; +1D185;MUSICAL SYMBOL COMBINING DOIT;Mn;230;NSM;;;;;N;;;;; +1D186;MUSICAL SYMBOL COMBINING RIP;Mn;230;NSM;;;;;N;;;;; +1D187;MUSICAL SYMBOL COMBINING FLIP;Mn;230;NSM;;;;;N;;;;; +1D188;MUSICAL SYMBOL COMBINING SMEAR;Mn;230;NSM;;;;;N;;;;; +1D189;MUSICAL SYMBOL COMBINING BEND;Mn;230;NSM;;;;;N;;;;; +1D1AA;MUSICAL SYMBOL COMBINING DOWN BOW;Mn;230;NSM;;;;;N;;;;; +1D1AB;MUSICAL SYMBOL COMBINING UP BOW;Mn;230;NSM;;;;;N;;;;; +1D1AC;MUSICAL SYMBOL COMBINING HARMONIC;Mn;230;NSM;;;;;N;;;;; +1D1AD;MUSICAL SYMBOL COMBINING SNAP PIZZICATO;Mn;230;NSM;;;;;N;;;;; +1D242;COMBINING GREEK MUSICAL TRISEME;Mn;230;NSM;;;;;N;;;;; +1D243;COMBINING GREEK MUSICAL TETRASEME;Mn;230;NSM;;;;;N;;;;; +1D244;COMBINING GREEK MUSICAL PENTASEME;Mn;230;NSM;;;;;N;;;;;