1/* Terminal hooks for GNU Emacs on the Microsoft W32 API. 2 Copyright (C) 1992, 1999, 2001, 2002, 2003, 2004, 3 2005, 2006, 2007 Free Software Foundation, Inc. 4 5This file is part of GNU Emacs. 6 7GNU Emacs is free software; you can redistribute it and/or modify 8it under the terms of the GNU General Public License as published by 9the Free Software Foundation; either version 2, or (at your option) 10any later version. 11 12GNU Emacs is distributed in the hope that it will be useful, 13but WITHOUT ANY WARRANTY; without even the implied warranty of 14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15GNU General Public License for more details. 16 17You should have received a copy of the GNU General Public License 18along with GNU Emacs; see the file COPYING. If not, write to 19the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 20Boston, MA 02110-1301, USA. 21 22 Tim Fleehart (apollo@online.com) 1-17-92 23 Geoff Voelker (voelker@cs.washington.edu) 9-12-93 24*/ 25 26 27#include <config.h> 28 29#include <stdlib.h> 30#include <stdio.h> 31#include <windows.h> 32#include <string.h> 33 34#include "lisp.h" 35#include "charset.h" 36#include "coding.h" 37#include "disptab.h" 38#include "termhooks.h" 39#include "dispextern.h" 40/* Disable features in frame.h that require a Window System. */ 41#undef HAVE_WINDOW_SYSTEM 42#include "frame.h" 43#include "w32inevt.h" 44 45/* from window.c */ 46extern Lisp_Object Frecenter (); 47 48/* from keyboard.c */ 49extern int detect_input_pending (); 50 51/* from sysdep.c */ 52extern int read_input_pending (); 53 54extern struct frame * updating_frame; 55extern int meta_key; 56 57static void w32con_move_cursor (int row, int col); 58static void w32con_clear_to_end (void); 59static void w32con_clear_frame (void); 60static void w32con_clear_end_of_line (int); 61static void w32con_ins_del_lines (int vpos, int n); 62static void w32con_insert_glyphs (struct glyph *start, int len); 63static void w32con_write_glyphs (struct glyph *string, int len); 64static void w32con_delete_glyphs (int n); 65void w32_sys_ring_bell (void); 66static void w32con_reset_terminal_modes (void); 67static void w32con_set_terminal_modes (void); 68static void w32con_set_terminal_window (int size); 69static void w32con_update_begin (struct frame * f); 70static void w32con_update_end (struct frame * f); 71static WORD w32_face_attributes (struct frame *f, int face_id); 72 73static COORD cursor_coords; 74static HANDLE prev_screen, cur_screen; 75static WORD char_attr_normal; 76static DWORD prev_console_mode; 77 78#ifndef USE_SEPARATE_SCREEN 79static CONSOLE_CURSOR_INFO prev_console_cursor; 80#endif 81 82/* Determine whether to make frame dimensions match the screen buffer, 83 or the current window size. The former is desirable when running 84 over telnet, while the latter is more useful when working directly at 85 the console with a large scroll-back buffer. */ 86int w32_use_full_screen_buffer; 87HANDLE keyboard_handle; 88 89 90/* Setting this as the ctrl handler prevents emacs from being killed when 91 someone hits ^C in a 'suspended' session (child shell). 92 Also ignore Ctrl-Break signals. */ 93 94BOOL 95ctrl_c_handler (unsigned long type) 96{ 97 /* Only ignore "interrupt" events when running interactively. */ 98 return (!noninteractive 99 && (type == CTRL_C_EVENT || type == CTRL_BREAK_EVENT)); 100} 101 102/* If we're updating a frame, use it as the current frame 103 Otherwise, use the selected frame. */ 104#define PICK_FRAME() (updating_frame ? updating_frame : SELECTED_FRAME ()) 105 106/* Move the cursor to (row, col). */ 107static void 108w32con_move_cursor (int row, int col) 109{ 110 cursor_coords.X = col; 111 cursor_coords.Y = row; 112 113 if (updating_frame == (struct frame *) NULL) 114 { 115 SetConsoleCursorPosition (cur_screen, cursor_coords); 116 } 117} 118 119/* Clear from cursor to end of screen. */ 120static void 121w32con_clear_to_end (void) 122{ 123 struct frame * f = PICK_FRAME (); 124 125 w32con_clear_end_of_line (FRAME_COLS (f) - 1); 126 w32con_ins_del_lines (cursor_coords.Y, FRAME_LINES (f) - cursor_coords.Y - 1); 127} 128 129/* Clear the frame. */ 130static void 131w32con_clear_frame (void) 132{ 133 struct frame * f = PICK_FRAME (); 134 COORD dest; 135 int n; 136 DWORD r; 137 CONSOLE_SCREEN_BUFFER_INFO info; 138 139 GetConsoleScreenBufferInfo (GetStdHandle (STD_OUTPUT_HANDLE), &info); 140 141 /* Remember that the screen buffer might be wider than the window. */ 142 n = FRAME_LINES (f) * info.dwSize.X; 143 dest.X = dest.Y = 0; 144 145 FillConsoleOutputAttribute (cur_screen, char_attr_normal, n, dest, &r); 146 FillConsoleOutputCharacter (cur_screen, ' ', n, dest, &r); 147 148 w32con_move_cursor (0, 0); 149} 150 151 152static struct glyph glyph_base[256]; 153static BOOL ceol_initialized = FALSE; 154 155/* Clear from Cursor to end (what's "standout marker"?). */ 156static void 157w32con_clear_end_of_line (int end) 158{ 159 if (!ceol_initialized) 160 { 161 int i; 162 for (i = 0; i < 256; i++) 163 { 164 memcpy (&glyph_base[i], &space_glyph, sizeof (struct glyph)); 165 } 166 ceol_initialized = TRUE; 167 } 168 w32con_write_glyphs (glyph_base, end - cursor_coords.X); /* fencepost ? */ 169} 170 171/* Insert n lines at vpos. if n is negative delete -n lines. */ 172static void 173w32con_ins_del_lines (int vpos, int n) 174{ 175 int i, nb; 176 SMALL_RECT scroll; 177 COORD dest; 178 CHAR_INFO fill; 179 struct frame * f = PICK_FRAME (); 180 181 if (n < 0) 182 { 183 scroll.Top = vpos - n; 184 scroll.Bottom = FRAME_LINES (f); 185 dest.Y = vpos; 186 } 187 else 188 { 189 scroll.Top = vpos; 190 scroll.Bottom = FRAME_LINES (f) - n; 191 dest.Y = vpos + n; 192 } 193 scroll.Left = 0; 194 scroll.Right = FRAME_COLS (f); 195 196 dest.X = 0; 197 198 fill.Char.AsciiChar = 0x20; 199 fill.Attributes = char_attr_normal; 200 201 ScrollConsoleScreenBuffer (cur_screen, &scroll, NULL, dest, &fill); 202 203 /* Here we have to deal with a w32 console flake: If the scroll 204 region looks like abc and we scroll c to a and fill with d we get 205 cbd... if we scroll block c one line at a time to a, we get cdd... 206 Emacs expects cdd consistently... So we have to deal with that 207 here... (this also occurs scrolling the same way in the other 208 direction. */ 209 210 if (n > 0) 211 { 212 if (scroll.Bottom < dest.Y) 213 { 214 for (i = scroll.Bottom; i < dest.Y; i++) 215 { 216 w32con_move_cursor (i, 0); 217 w32con_clear_end_of_line (FRAME_COLS (f)); 218 } 219 } 220 } 221 else 222 { 223 nb = dest.Y + (scroll.Bottom - scroll.Top) + 1; 224 225 if (nb < scroll.Top) 226 { 227 for (i = nb; i < scroll.Top; i++) 228 { 229 w32con_move_cursor (i, 0); 230 w32con_clear_end_of_line (FRAME_COLS (f)); 231 } 232 } 233 } 234 235 cursor_coords.X = 0; 236 cursor_coords.Y = vpos; 237} 238 239#undef LEFT 240#undef RIGHT 241#define LEFT 1 242#define RIGHT 0 243 244static void 245scroll_line (int dist, int direction) 246{ 247 /* The idea here is to implement a horizontal scroll in one line to 248 implement delete and half of insert. */ 249 SMALL_RECT scroll; 250 COORD dest; 251 CHAR_INFO fill; 252 struct frame * f = PICK_FRAME (); 253 254 scroll.Top = cursor_coords.Y; 255 scroll.Bottom = cursor_coords.Y; 256 257 if (direction == LEFT) 258 { 259 scroll.Left = cursor_coords.X + dist; 260 scroll.Right = FRAME_COLS (f) - 1; 261 } 262 else 263 { 264 scroll.Left = cursor_coords.X; 265 scroll.Right = FRAME_COLS (f) - dist - 1; 266 } 267 268 dest.X = cursor_coords.X; 269 dest.Y = cursor_coords.Y; 270 271 fill.Char.AsciiChar = 0x20; 272 fill.Attributes = char_attr_normal; 273 274 ScrollConsoleScreenBuffer (cur_screen, &scroll, NULL, dest, &fill); 275} 276 277 278/* If start is zero insert blanks instead of a string at start ?. */ 279static void 280w32con_insert_glyphs (register struct glyph *start, register int len) 281{ 282 scroll_line (len, RIGHT); 283 284 /* Move len chars to the right starting at cursor_coords, fill with blanks */ 285 if (start) 286 { 287 /* Print the first len characters of start, cursor_coords.X adjusted 288 by write_glyphs. */ 289 290 w32con_write_glyphs (start, len); 291 } 292 else 293 { 294 w32con_clear_end_of_line (cursor_coords.X + len); 295 } 296} 297 298extern unsigned char *encode_terminal_code P_ ((struct glyph *, int, 299 struct coding_system *)); 300 301static void 302w32con_write_glyphs (register struct glyph *string, register int len) 303{ 304 int produced, consumed; 305 DWORD r; 306 struct frame * f = PICK_FRAME (); 307 WORD char_attr; 308 unsigned char *conversion_buffer; 309 struct coding_system *coding; 310 311 if (len <= 0) 312 return; 313 314 /* If terminal_coding does any conversion, use it, otherwise use 315 safe_terminal_coding. We can't use CODING_REQUIRE_ENCODING here 316 because it always return 1 if the member src_multibyte is 1. */ 317 coding = (terminal_coding.common_flags & CODING_REQUIRE_ENCODING_MASK 318 ? &terminal_coding : &safe_terminal_coding); 319 /* The mode bit CODING_MODE_LAST_BLOCK should be set to 1 only at 320 the tail. */ 321 terminal_coding.mode &= ~CODING_MODE_LAST_BLOCK; 322 323 while (len > 0) 324 { 325 /* Identify a run of glyphs with the same face. */ 326 int face_id = string->face_id; 327 int n; 328 329 for (n = 1; n < len; ++n) 330 if (string[n].face_id != face_id) 331 break; 332 333 /* Turn appearance modes of the face of the run on. */ 334 char_attr = w32_face_attributes (f, face_id); 335 336 if (n == len) 337 /* This is the last run. */ 338 coding->mode |= CODING_MODE_LAST_BLOCK; 339 conversion_buffer = encode_terminal_code (string, n, coding); 340 if (coding->produced > 0) 341 { 342 /* Set the attribute for these characters. */ 343 if (!FillConsoleOutputAttribute (cur_screen, char_attr, 344 coding->produced, cursor_coords, 345 &r)) 346 { 347 printf ("Failed writing console attributes: %d\n", 348 GetLastError ()); 349 fflush (stdout); 350 } 351 352 /* Write the characters. */ 353 if (!WriteConsoleOutputCharacter (cur_screen, conversion_buffer, 354 coding->produced, cursor_coords, 355 &r)) 356 { 357 printf ("Failed writing console characters: %d\n", 358 GetLastError ()); 359 fflush (stdout); 360 } 361 362 cursor_coords.X += coding->produced; 363 w32con_move_cursor (cursor_coords.Y, cursor_coords.X); 364 } 365 len -= n; 366 string += n; 367 } 368} 369 370 371static void 372w32con_delete_glyphs (int n) 373{ 374 /* delete chars means scroll chars from cursor_coords.X + n to 375 cursor_coords.X, anything beyond the edge of the screen should 376 come out empty... */ 377 378 scroll_line (n, LEFT); 379} 380 381static unsigned int sound_type = 0xFFFFFFFF; 382#define MB_EMACS_SILENT (0xFFFFFFFF - 1) 383 384void 385w32_sys_ring_bell (void) 386{ 387 if (sound_type == 0xFFFFFFFF) 388 { 389 Beep (666, 100); 390 } 391 else if (sound_type == MB_EMACS_SILENT) 392 { 393 /* Do nothing. */ 394 } 395 else 396 MessageBeep (sound_type); 397} 398 399DEFUN ("set-message-beep", Fset_message_beep, Sset_message_beep, 1, 1, 0, 400 doc: /* Set the sound generated when the bell is rung. 401SOUND is 'asterisk, 'exclamation, 'hand, 'question, 'ok, or 'silent 402to use the corresponding system sound for the bell. The 'silent sound 403prevents Emacs from making any sound at all. 404SOUND is nil to use the normal beep. */) 405 (sound) 406 Lisp_Object sound; 407{ 408 CHECK_SYMBOL (sound); 409 410 if (NILP (sound)) 411 sound_type = 0xFFFFFFFF; 412 else if (EQ (sound, intern ("asterisk"))) 413 sound_type = MB_ICONASTERISK; 414 else if (EQ (sound, intern ("exclamation"))) 415 sound_type = MB_ICONEXCLAMATION; 416 else if (EQ (sound, intern ("hand"))) 417 sound_type = MB_ICONHAND; 418 else if (EQ (sound, intern ("question"))) 419 sound_type = MB_ICONQUESTION; 420 else if (EQ (sound, intern ("ok"))) 421 sound_type = MB_OK; 422 else if (EQ (sound, intern ("silent"))) 423 sound_type = MB_EMACS_SILENT; 424 else 425 sound_type = 0xFFFFFFFF; 426 427 return sound; 428} 429 430static void 431w32con_reset_terminal_modes (void) 432{ 433#ifdef USE_SEPARATE_SCREEN 434 SetConsoleActiveScreenBuffer (prev_screen); 435#else 436 SetConsoleCursorInfo (prev_screen, &prev_console_cursor); 437#endif 438 SetConsoleMode (keyboard_handle, prev_console_mode); 439} 440 441static void 442w32con_set_terminal_modes (void) 443{ 444 CONSOLE_CURSOR_INFO cci; 445 446 /* make cursor big and visible (100 on Win95 makes it disappear) */ 447 cci.dwSize = 99; 448 cci.bVisible = TRUE; 449 (void) SetConsoleCursorInfo (cur_screen, &cci); 450 451 SetConsoleActiveScreenBuffer (cur_screen); 452 453 SetConsoleMode (keyboard_handle, ENABLE_MOUSE_INPUT | ENABLE_WINDOW_INPUT); 454 455 /* Initialize input mode: interrupt_input off, no flow control, allow 456 8 bit character input, standard quit char. */ 457 Fset_input_mode (Qnil, Qnil, make_number (2), Qnil); 458} 459 460/* hmmm... perhaps these let us bracket screen changes so that we can flush 461 clumps rather than one-character-at-a-time... 462 463 we'll start with not moving the cursor while an update is in progress. */ 464static void 465w32con_update_begin (struct frame * f) 466{ 467} 468 469static void 470w32con_update_end (struct frame * f) 471{ 472 SetConsoleCursorPosition (cur_screen, cursor_coords); 473} 474 475static void 476w32con_set_terminal_window (int size) 477{ 478} 479 480/*********************************************************************** 481 Faces 482 ***********************************************************************/ 483 484 485/* Turn appearances of face FACE_ID on tty frame F on. */ 486 487static WORD 488w32_face_attributes (f, face_id) 489 struct frame *f; 490 int face_id; 491{ 492 WORD char_attr; 493 struct face *face = FACE_FROM_ID (f, face_id); 494 495 xassert (face != NULL); 496 497 char_attr = char_attr_normal; 498 499 if (face->foreground != FACE_TTY_DEFAULT_FG_COLOR 500 && face->foreground != FACE_TTY_DEFAULT_COLOR) 501 char_attr = (char_attr & 0xfff0) + (face->foreground % 16); 502 503 if (face->background != FACE_TTY_DEFAULT_BG_COLOR 504 && face->background != FACE_TTY_DEFAULT_COLOR) 505 char_attr = (char_attr & 0xff0f) + ((face->background % 16) << 4); 506 507 508 /* NTEMACS_TODO: Faces defined during startup get both foreground 509 and background of 0. Need a better way around this - for now detect 510 the problem and invert one of the faces to make the text readable. */ 511 if (((char_attr & 0x00f0) >> 4) == (char_attr & 0x000f)) 512 char_attr ^= 0x0007; 513 514 if (face->tty_reverse_p) 515 char_attr = (char_attr & 0xff00) + ((char_attr & 0x000f) << 4) 516 + ((char_attr & 0x00f0) >> 4); 517 518 return char_attr; 519} 520 521 522/* Emulation of some X window features from xfns.c and xfaces.c. */ 523 524extern char unspecified_fg[], unspecified_bg[]; 525 526 527/* Given a color index, return its standard name. */ 528Lisp_Object 529vga_stdcolor_name (int idx) 530{ 531 /* Standard VGA colors, in the order of their standard numbering 532 in the default VGA palette. */ 533 static char *vga_colors[16] = { 534 "black", "blue", "green", "cyan", "red", "magenta", "brown", 535 "lightgray", "darkgray", "lightblue", "lightgreen", "lightcyan", 536 "lightred", "lightmagenta", "yellow", "white" 537 }; 538 539 extern Lisp_Object Qunspecified; 540 541 if (idx >= 0 && idx < sizeof (vga_colors) / sizeof (vga_colors[0])) 542 return build_string (vga_colors[idx]); 543 else 544 return Qunspecified; /* meaning the default */ 545} 546 547typedef int (*term_hook) (); 548 549void 550initialize_w32_display (void) 551{ 552 CONSOLE_SCREEN_BUFFER_INFO info; 553 554 cursor_to_hook = w32con_move_cursor; 555 raw_cursor_to_hook = w32con_move_cursor; 556 clear_to_end_hook = w32con_clear_to_end; 557 clear_frame_hook = w32con_clear_frame; 558 clear_end_of_line_hook = w32con_clear_end_of_line; 559 ins_del_lines_hook = w32con_ins_del_lines; 560 insert_glyphs_hook = w32con_insert_glyphs; 561 write_glyphs_hook = w32con_write_glyphs; 562 delete_glyphs_hook = w32con_delete_glyphs; 563 ring_bell_hook = w32_sys_ring_bell; 564 reset_terminal_modes_hook = w32con_reset_terminal_modes; 565 set_terminal_modes_hook = w32con_set_terminal_modes; 566 set_terminal_window_hook = w32con_set_terminal_window; 567 update_begin_hook = w32con_update_begin; 568 update_end_hook = w32con_update_end; 569 570 read_socket_hook = w32_console_read_socket; 571 mouse_position_hook = w32_console_mouse_position; 572 573 /* Initialize interrupt_handle. */ 574 init_crit (); 575 576 /* Remember original console settings. */ 577 keyboard_handle = GetStdHandle (STD_INPUT_HANDLE); 578 GetConsoleMode (keyboard_handle, &prev_console_mode); 579 580 prev_screen = GetStdHandle (STD_OUTPUT_HANDLE); 581 582#ifdef USE_SEPARATE_SCREEN 583 cur_screen = CreateConsoleScreenBuffer (GENERIC_READ | GENERIC_WRITE, 584 0, NULL, 585 CONSOLE_TEXTMODE_BUFFER, 586 NULL); 587 588 if (cur_screen == INVALID_HANDLE_VALUE) 589 { 590 printf ("CreateConsoleScreenBuffer failed in ResetTerm\n"); 591 printf ("LastError = 0x%lx\n", GetLastError ()); 592 fflush (stdout); 593 exit (0); 594 } 595#else 596 cur_screen = prev_screen; 597 GetConsoleCursorInfo (prev_screen, &prev_console_cursor); 598#endif 599 600 /* Respect setting of LINES and COLUMNS environment variables. */ 601 { 602 char * lines = getenv("LINES"); 603 char * columns = getenv("COLUMNS"); 604 605 if (lines != NULL && columns != NULL) 606 { 607 SMALL_RECT new_win_dims; 608 COORD new_size; 609 610 new_size.X = atoi (columns); 611 new_size.Y = atoi (lines); 612 613 GetConsoleScreenBufferInfo (cur_screen, &info); 614 615 /* Shrink the window first, so the buffer dimensions can be 616 reduced if necessary. */ 617 new_win_dims.Top = 0; 618 new_win_dims.Left = 0; 619 new_win_dims.Bottom = min (new_size.Y, info.dwSize.Y) - 1; 620 new_win_dims.Right = min (new_size.X, info.dwSize.X) - 1; 621 SetConsoleWindowInfo (cur_screen, TRUE, &new_win_dims); 622 623 SetConsoleScreenBufferSize (cur_screen, new_size); 624 625 /* Set the window size to match the buffer dimension. */ 626 new_win_dims.Top = 0; 627 new_win_dims.Left = 0; 628 new_win_dims.Bottom = new_size.Y - 1; 629 new_win_dims.Right = new_size.X - 1; 630 SetConsoleWindowInfo (cur_screen, TRUE, &new_win_dims); 631 } 632 } 633 634 GetConsoleScreenBufferInfo (cur_screen, &info); 635 636 meta_key = 1; 637 char_attr_normal = info.wAttributes; 638 639 /* Determine if the info returned by GetConsoleScreenBufferInfo 640 is realistic. Old MS Telnet servers used to only fill out 641 the dwSize portion, even modern one fill the whole struct with 642 garbage when using non-MS telnet clients. */ 643 if ((w32_use_full_screen_buffer 644 && (info.dwSize.Y < 20 || info.dwSize.Y > 100 645 || info.dwSize.X < 40 || info.dwSize.X > 200)) 646 || (!w32_use_full_screen_buffer 647 && (info.srWindow.Bottom - info.srWindow.Top < 20 648 || info.srWindow.Bottom - info.srWindow.Top > 100 649 || info.srWindow.Right - info.srWindow.Left < 40 650 || info.srWindow.Right - info.srWindow.Left > 100))) 651 { 652 FRAME_LINES (SELECTED_FRAME ()) = 25; 653 SET_FRAME_COLS (SELECTED_FRAME (), 80); 654 } 655 656 else if (w32_use_full_screen_buffer) 657 { 658 FRAME_LINES (SELECTED_FRAME ()) = info.dwSize.Y; /* lines per page */ 659 SET_FRAME_COLS (SELECTED_FRAME (), info.dwSize.X); /* characters per line */ 660 } 661 else 662 { 663 /* Lines per page. Use buffer coords instead of buffer size. */ 664 FRAME_LINES (SELECTED_FRAME ()) = 1 + info.srWindow.Bottom - 665 info.srWindow.Top; 666 /* Characters per line. Use buffer coords instead of buffer size. */ 667 SET_FRAME_COLS (SELECTED_FRAME (), 1 + info.srWindow.Right - 668 info.srWindow.Left); 669 } 670 671 /* Setup w32_display_info structure for this frame. */ 672 673 w32_initialize_display_info (build_string ("Console")); 674 675} 676 677DEFUN ("set-screen-color", Fset_screen_color, Sset_screen_color, 2, 2, 0, 678 doc: /* Set screen colors. */) 679 (foreground, background) 680 Lisp_Object foreground; 681 Lisp_Object background; 682{ 683 char_attr_normal = XFASTINT (foreground) + (XFASTINT (background) << 4); 684 685 Frecenter (Qnil); 686 return Qt; 687} 688 689DEFUN ("set-cursor-size", Fset_cursor_size, Sset_cursor_size, 1, 1, 0, 690 doc: /* Set cursor size. */) 691 (size) 692 Lisp_Object size; 693{ 694 CONSOLE_CURSOR_INFO cci; 695 cci.dwSize = XFASTINT (size); 696 cci.bVisible = TRUE; 697 (void) SetConsoleCursorInfo (cur_screen, &cci); 698 699 return Qt; 700} 701 702void 703syms_of_ntterm () 704{ 705 DEFVAR_BOOL ("w32-use-full-screen-buffer", 706 &w32_use_full_screen_buffer, 707 doc: /* Non-nil means make terminal frames use the full screen buffer dimensions. 708This is desirable when running Emacs over telnet. 709A value of nil means use the current console window dimensions; this 710may be preferrable when working directly at the console with a large 711scroll-back buffer. */); 712 w32_use_full_screen_buffer = 0; 713 714 defsubr (&Sset_screen_color); 715 defsubr (&Sset_cursor_size); 716 defsubr (&Sset_message_beep); 717} 718 719/* arch-tag: a390a07f-f661-42bc-aeb4-e6d8bf860337 720 (do not change this comment) */ 721