xref: /qemu/ui/curses.c (revision 8ac65578)
1 /*
2  * QEMU curses/ncurses display driver
3  *
4  * Copyright (c) 2005 Andrzej Zaborowski  <balrog@zabor.org>
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a copy
7  * of this software and associated documentation files (the "Software"), to deal
8  * in the Software without restriction, including without limitation the rights
9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10  * copies of the Software, and to permit persons to whom the Software is
11  * furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in
14  * all copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22  * THE SOFTWARE.
23  */
24 #include "qemu/osdep.h"
25 
26 #ifndef _WIN32
27 #include <sys/ioctl.h>
28 #include <termios.h>
29 #endif
30 #include <locale.h>
31 #include <wchar.h>
32 #include <langinfo.h>
33 #include <iconv.h>
34 
35 #include "qapi/error.h"
36 #include "qemu-common.h"
37 #include "ui/console.h"
38 #include "ui/input.h"
39 #include "sysemu/sysemu.h"
40 
41 /* KEY_EVENT is defined in wincon.h and in curses.h. Avoid redefinition. */
42 #undef KEY_EVENT
43 #include <curses.h>
44 #undef KEY_EVENT
45 
46 #define FONT_HEIGHT 16
47 #define FONT_WIDTH 8
48 
49 enum maybe_keycode {
50     CURSES_KEYCODE,
51     CURSES_CHAR,
52     CURSES_CHAR_OR_KEYCODE,
53 };
54 
55 static DisplayChangeListener *dcl;
56 static console_ch_t screen[160 * 100];
57 static WINDOW *screenpad = NULL;
58 static int width, height, gwidth, gheight, invalidate;
59 static int px, py, sminx, sminy, smaxx, smaxy;
60 
61 static const char *font_charset = "CP437";
62 static cchar_t vga_to_curses[256];
63 
64 static void curses_update(DisplayChangeListener *dcl,
65                           int x, int y, int w, int h)
66 {
67     console_ch_t *line;
68     cchar_t curses_line[width];
69 
70     line = screen + y * width;
71     for (h += y; y < h; y ++, line += width) {
72         for (x = 0; x < width; x++) {
73             chtype ch = line[x] & 0xff;
74             chtype at = line[x] & ~0xff;
75             if (vga_to_curses[ch].chars[0]) {
76                 curses_line[x] = vga_to_curses[ch];
77             } else {
78                 curses_line[x] = (cchar_t) {
79                     .chars[0] = ch,
80                 };
81             }
82             curses_line[x].attr |= at;
83         }
84         mvwadd_wchnstr(screenpad, y, 0, curses_line, width);
85     }
86 
87     pnoutrefresh(screenpad, py, px, sminy, sminx, smaxy - 1, smaxx - 1);
88     refresh();
89 }
90 
91 static void curses_calc_pad(void)
92 {
93     if (qemu_console_is_fixedsize(NULL)) {
94         width = gwidth;
95         height = gheight;
96     } else {
97         width = COLS;
98         height = LINES;
99     }
100 
101     if (screenpad)
102         delwin(screenpad);
103 
104     clear();
105     refresh();
106 
107     screenpad = newpad(height, width);
108 
109     if (width > COLS) {
110         px = (width - COLS) / 2;
111         sminx = 0;
112         smaxx = COLS;
113     } else {
114         px = 0;
115         sminx = (COLS - width) / 2;
116         smaxx = sminx + width;
117     }
118 
119     if (height > LINES) {
120         py = (height - LINES) / 2;
121         sminy = 0;
122         smaxy = LINES;
123     } else {
124         py = 0;
125         sminy = (LINES - height) / 2;
126         smaxy = sminy + height;
127     }
128 }
129 
130 static void curses_resize(DisplayChangeListener *dcl,
131                           int width, int height)
132 {
133     if (width == gwidth && height == gheight) {
134         return;
135     }
136 
137     gwidth = width;
138     gheight = height;
139 
140     curses_calc_pad();
141 }
142 
143 #if !defined(_WIN32) && defined(SIGWINCH) && defined(KEY_RESIZE)
144 static volatile sig_atomic_t got_sigwinch;
145 static void curses_winch_check(void)
146 {
147     struct winsize {
148         unsigned short ws_row;
149         unsigned short ws_col;
150         unsigned short ws_xpixel;   /* unused */
151         unsigned short ws_ypixel;   /* unused */
152     } ws;
153 
154     if (!got_sigwinch) {
155         return;
156     }
157     got_sigwinch = false;
158 
159     if (ioctl(1, TIOCGWINSZ, &ws) == -1) {
160         return;
161     }
162 
163     resize_term(ws.ws_row, ws.ws_col);
164     invalidate = 1;
165 }
166 
167 static void curses_winch_handler(int signum)
168 {
169     got_sigwinch = true;
170 }
171 
172 static void curses_winch_init(void)
173 {
174     struct sigaction old, winch = {
175         .sa_handler  = curses_winch_handler,
176     };
177     sigaction(SIGWINCH, &winch, &old);
178 }
179 #else
180 static void curses_winch_check(void) {}
181 static void curses_winch_init(void) {}
182 #endif
183 
184 static void curses_cursor_position(DisplayChangeListener *dcl,
185                                    int x, int y)
186 {
187     if (x >= 0) {
188         x = sminx + x - px;
189         y = sminy + y - py;
190 
191         if (x >= 0 && y >= 0 && x < COLS && y < LINES) {
192             move(y, x);
193             curs_set(1);
194             /* it seems that curs_set(1) must always be called before
195              * curs_set(2) for the latter to have effect */
196             if (!qemu_console_is_graphic(NULL)) {
197                 curs_set(2);
198             }
199             return;
200         }
201     }
202 
203     curs_set(0);
204 }
205 
206 /* generic keyboard conversion */
207 
208 #include "curses_keys.h"
209 
210 static kbd_layout_t *kbd_layout = NULL;
211 
212 static wint_t console_getch(enum maybe_keycode *maybe_keycode)
213 {
214     wint_t ret;
215     switch (get_wch(&ret)) {
216     case KEY_CODE_YES:
217         *maybe_keycode = CURSES_KEYCODE;
218         break;
219     case OK:
220         *maybe_keycode = CURSES_CHAR;
221         break;
222     case ERR:
223         ret = -1;
224         break;
225     }
226     return ret;
227 }
228 
229 static int curses2foo(const int _curses2foo[], const int _curseskey2foo[],
230                       int chr, enum maybe_keycode maybe_keycode)
231 {
232     int ret = -1;
233     if (maybe_keycode == CURSES_CHAR) {
234         if (chr < CURSES_CHARS) {
235             ret = _curses2foo[chr];
236         }
237     } else {
238         if (chr < CURSES_KEYS) {
239             ret = _curseskey2foo[chr];
240         }
241         if (ret == -1 && maybe_keycode == CURSES_CHAR_OR_KEYCODE &&
242             chr < CURSES_CHARS) {
243             ret = _curses2foo[chr];
244         }
245     }
246     return ret;
247 }
248 
249 #define curses2keycode(chr, maybe_keycode) \
250     curses2foo(_curses2keycode, _curseskey2keycode, chr, maybe_keycode)
251 #define curses2keysym(chr, maybe_keycode) \
252     curses2foo(_curses2keysym, _curseskey2keysym, chr, maybe_keycode)
253 #define curses2qemu(chr, maybe_keycode) \
254     curses2foo(_curses2qemu, _curseskey2qemu, chr, maybe_keycode)
255 
256 static void curses_refresh(DisplayChangeListener *dcl)
257 {
258     int chr, keysym, keycode, keycode_alt;
259     enum maybe_keycode maybe_keycode;
260 
261     curses_winch_check();
262 
263     if (invalidate) {
264         clear();
265         refresh();
266         curses_calc_pad();
267         graphic_hw_invalidate(NULL);
268         invalidate = 0;
269     }
270 
271     graphic_hw_text_update(NULL, screen);
272 
273     while (1) {
274         /* while there are any pending key strokes to process */
275         chr = console_getch(&maybe_keycode);
276 
277         if (chr == -1)
278             break;
279 
280 #ifdef KEY_RESIZE
281         /* this shouldn't occur when we use a custom SIGWINCH handler */
282         if (maybe_keycode != CURSES_CHAR && chr == KEY_RESIZE) {
283             clear();
284             refresh();
285             curses_calc_pad();
286             curses_update(dcl, 0, 0, width, height);
287             continue;
288         }
289 #endif
290 
291         keycode = curses2keycode(chr, maybe_keycode);
292         keycode_alt = 0;
293 
294         /* alt or esc key */
295         if (keycode == 1) {
296             enum maybe_keycode next_maybe_keycode;
297             int nextchr = console_getch(&next_maybe_keycode);
298 
299             if (nextchr != -1) {
300                 chr = nextchr;
301                 maybe_keycode = next_maybe_keycode;
302                 keycode_alt = ALT;
303                 keycode = curses2keycode(chr, maybe_keycode);
304 
305                 if (keycode != -1) {
306                     keycode |= ALT;
307 
308                     /* process keys reserved for qemu */
309                     if (keycode >= QEMU_KEY_CONSOLE0 &&
310                             keycode < QEMU_KEY_CONSOLE0 + 9) {
311                         erase();
312                         wnoutrefresh(stdscr);
313                         console_select(keycode - QEMU_KEY_CONSOLE0);
314 
315                         invalidate = 1;
316                         continue;
317                     }
318                 }
319             }
320         }
321 
322         if (kbd_layout) {
323             keysym = curses2keysym(chr, maybe_keycode);
324 
325             if (keysym == -1) {
326                 if (chr < ' ') {
327                     keysym = chr + '@';
328                     if (keysym >= 'A' && keysym <= 'Z')
329                         keysym += 'a' - 'A';
330                     keysym |= KEYSYM_CNTRL;
331                 } else
332                     keysym = chr;
333             }
334 
335             keycode = keysym2scancode(kbd_layout, keysym & KEYSYM_MASK,
336                                       NULL, false);
337             if (keycode == 0)
338                 continue;
339 
340             keycode |= (keysym & ~KEYSYM_MASK) >> 16;
341             keycode |= keycode_alt;
342         }
343 
344         if (keycode == -1)
345             continue;
346 
347         if (qemu_console_is_graphic(NULL)) {
348             /* since terminals don't know about key press and release
349              * events, we need to emit both for each key received */
350             if (keycode & SHIFT) {
351                 qemu_input_event_send_key_number(NULL, SHIFT_CODE, true);
352                 qemu_input_event_send_key_delay(0);
353             }
354             if (keycode & CNTRL) {
355                 qemu_input_event_send_key_number(NULL, CNTRL_CODE, true);
356                 qemu_input_event_send_key_delay(0);
357             }
358             if (keycode & ALT) {
359                 qemu_input_event_send_key_number(NULL, ALT_CODE, true);
360                 qemu_input_event_send_key_delay(0);
361             }
362             if (keycode & ALTGR) {
363                 qemu_input_event_send_key_number(NULL, GREY | ALT_CODE, true);
364                 qemu_input_event_send_key_delay(0);
365             }
366 
367             qemu_input_event_send_key_number(NULL, keycode & KEY_MASK, true);
368             qemu_input_event_send_key_delay(0);
369             qemu_input_event_send_key_number(NULL, keycode & KEY_MASK, false);
370             qemu_input_event_send_key_delay(0);
371 
372             if (keycode & ALTGR) {
373                 qemu_input_event_send_key_number(NULL, GREY | ALT_CODE, false);
374                 qemu_input_event_send_key_delay(0);
375             }
376             if (keycode & ALT) {
377                 qemu_input_event_send_key_number(NULL, ALT_CODE, false);
378                 qemu_input_event_send_key_delay(0);
379             }
380             if (keycode & CNTRL) {
381                 qemu_input_event_send_key_number(NULL, CNTRL_CODE, false);
382                 qemu_input_event_send_key_delay(0);
383             }
384             if (keycode & SHIFT) {
385                 qemu_input_event_send_key_number(NULL, SHIFT_CODE, false);
386                 qemu_input_event_send_key_delay(0);
387             }
388         } else {
389             keysym = curses2qemu(chr, maybe_keycode);
390             if (keysym == -1)
391                 keysym = chr;
392 
393             kbd_put_keysym(keysym);
394         }
395     }
396 }
397 
398 static void curses_atexit(void)
399 {
400     endwin();
401 }
402 
403 /* Setup wchar glyph for one UCS-2 char */
404 static void convert_ucs(int glyph, uint16_t ch, iconv_t conv)
405 {
406     wchar_t wch;
407     char *pch, *pwch;
408     size_t sch, swch;
409 
410     pch = (char *) &ch;
411     pwch = (char *) &wch;
412     sch = sizeof(ch);
413     swch = sizeof(wch);
414 
415     if (iconv(conv, &pch, &sch, &pwch, &swch) == (size_t) -1) {
416         fprintf(stderr, "Could not convert 0x%04x from UCS-2 to WCHAR_T: %s\n",
417                         ch, strerror(errno));
418     } else {
419         vga_to_curses[glyph].chars[0] = wch;
420     }
421 }
422 
423 /* Setup wchar glyph for one font character */
424 static void convert_font(unsigned char ch, iconv_t conv)
425 {
426     wchar_t wch;
427     char *pch, *pwch;
428     size_t sch, swch;
429 
430     pch = (char *) &ch;
431     pwch = (char *) &wch;
432     sch = sizeof(ch);
433     swch = sizeof(wch);
434 
435     if (iconv(conv, &pch, &sch, &pwch, &swch) == (size_t) -1) {
436         fprintf(stderr, "Could not convert 0x%02x from %s to WCHAR_T: %s\n",
437                         ch, font_charset, strerror(errno));
438     } else {
439         vga_to_curses[ch].chars[0] = wch;
440     }
441 }
442 
443 /* Convert one wchar to UCS-2 */
444 static uint16_t get_ucs(wchar_t wch, iconv_t conv)
445 {
446     uint16_t ch;
447     char *pch, *pwch;
448     size_t sch, swch;
449 
450     pch = (char *) &ch;
451     pwch = (char *) &wch;
452     sch = sizeof(ch);
453     swch = sizeof(wch);
454 
455     if (iconv(conv, &pwch, &swch, &pch, &sch) == (size_t) -1) {
456         fprintf(stderr, "Could not convert 0x%02lx from WCHAR_T to UCS-2: %s\n",
457                 (unsigned long)wch, strerror(errno));
458         return 0xFFFD;
459     }
460 
461     return ch;
462 }
463 
464 /*
465  * Setup mapping for vga to curses line graphics.
466  */
467 static void font_setup(void)
468 {
469     /*
470      * Control characters are normally non-printable, but VGA does have
471      * well-known glyphs for them.
472      */
473     static uint16_t control_characters[0x20] = {
474       0x0020,
475       0x263a,
476       0x263b,
477       0x2665,
478       0x2666,
479       0x2663,
480       0x2660,
481       0x2022,
482       0x25d8,
483       0x25cb,
484       0x25d9,
485       0x2642,
486       0x2640,
487       0x266a,
488       0x266b,
489       0x263c,
490       0x25ba,
491       0x25c4,
492       0x2195,
493       0x203c,
494       0x00b6,
495       0x00a7,
496       0x25ac,
497       0x21a8,
498       0x2191,
499       0x2193,
500       0x2192,
501       0x2190,
502       0x221f,
503       0x2194,
504       0x25b2,
505       0x25bc
506     };
507 
508     iconv_t ucs_to_wchar_conv;
509     iconv_t wchar_to_ucs_conv;
510     iconv_t font_conv;
511     int i;
512 
513     ucs_to_wchar_conv = iconv_open("WCHAR_T", "UCS-2");
514     if (ucs_to_wchar_conv == (iconv_t) -1) {
515         fprintf(stderr, "Could not convert font glyphs from UCS-2: '%s'\n",
516                         strerror(errno));
517         exit(1);
518     }
519 
520     wchar_to_ucs_conv = iconv_open("UCS-2", "WCHAR_T");
521     if (wchar_to_ucs_conv == (iconv_t) -1) {
522         iconv_close(ucs_to_wchar_conv);
523         fprintf(stderr, "Could not convert font glyphs to UCS-2: '%s'\n",
524                         strerror(errno));
525         exit(1);
526     }
527 
528     font_conv = iconv_open("WCHAR_T", font_charset);
529     if (font_conv == (iconv_t) -1) {
530         iconv_close(ucs_to_wchar_conv);
531         iconv_close(wchar_to_ucs_conv);
532         fprintf(stderr, "Could not convert font glyphs from %s: '%s'\n",
533                         font_charset, strerror(errno));
534         exit(1);
535     }
536 
537     /* Control characters */
538     for (i = 0; i <= 0x1F; i++) {
539         convert_ucs(i, control_characters[i], ucs_to_wchar_conv);
540     }
541 
542     for (i = 0x20; i <= 0xFF; i++) {
543         convert_font(i, font_conv);
544     }
545 
546     /* DEL */
547     convert_ucs(0x7F, 0x2302, ucs_to_wchar_conv);
548 
549     if (strcmp(nl_langinfo(CODESET), "UTF-8")) {
550         /* Non-Unicode capable, use termcap equivalents for those available */
551         for (i = 0; i <= 0xFF; i++) {
552             switch (get_ucs(vga_to_curses[i].chars[0], wchar_to_ucs_conv)) {
553             case 0x00a3:
554                 vga_to_curses[i] = *WACS_STERLING;
555                 break;
556             case 0x2591:
557                 vga_to_curses[i] = *WACS_BOARD;
558                 break;
559             case 0x2592:
560                 vga_to_curses[i] = *WACS_CKBOARD;
561                 break;
562             case 0x2502:
563                 vga_to_curses[i] = *WACS_VLINE;
564                 break;
565             case 0x2524:
566                 vga_to_curses[i] = *WACS_RTEE;
567                 break;
568             case 0x2510:
569                 vga_to_curses[i] = *WACS_URCORNER;
570                 break;
571             case 0x2514:
572                 vga_to_curses[i] = *WACS_LLCORNER;
573                 break;
574             case 0x2534:
575                 vga_to_curses[i] = *WACS_BTEE;
576                 break;
577             case 0x252c:
578                 vga_to_curses[i] = *WACS_TTEE;
579                 break;
580             case 0x251c:
581                 vga_to_curses[i] = *WACS_LTEE;
582                 break;
583             case 0x2500:
584                 vga_to_curses[i] = *WACS_HLINE;
585                 break;
586             case 0x253c:
587                 vga_to_curses[i] = *WACS_PLUS;
588                 break;
589             case 0x256c:
590                 vga_to_curses[i] = *WACS_LANTERN;
591                 break;
592             case 0x256a:
593                 vga_to_curses[i] = *WACS_NEQUAL;
594                 break;
595             case 0x2518:
596                 vga_to_curses[i] = *WACS_LRCORNER;
597                 break;
598             case 0x250c:
599                 vga_to_curses[i] = *WACS_ULCORNER;
600                 break;
601             case 0x2588:
602                 vga_to_curses[i] = *WACS_BLOCK;
603                 break;
604             case 0x03c0:
605                 vga_to_curses[i] = *WACS_PI;
606                 break;
607             case 0x00b1:
608                 vga_to_curses[i] = *WACS_PLMINUS;
609                 break;
610             case 0x2265:
611                 vga_to_curses[i] = *WACS_GEQUAL;
612                 break;
613             case 0x2264:
614                 vga_to_curses[i] = *WACS_LEQUAL;
615                 break;
616             case 0x00b0:
617                 vga_to_curses[i] = *WACS_DEGREE;
618                 break;
619             case 0x25a0:
620                 vga_to_curses[i] = *WACS_BULLET;
621                 break;
622             case 0x2666:
623                 vga_to_curses[i] = *WACS_DIAMOND;
624                 break;
625             case 0x2192:
626                 vga_to_curses[i] = *WACS_RARROW;
627                 break;
628             case 0x2190:
629                 vga_to_curses[i] = *WACS_LARROW;
630                 break;
631             case 0x2191:
632                 vga_to_curses[i] = *WACS_UARROW;
633                 break;
634             case 0x2193:
635                 vga_to_curses[i] = *WACS_DARROW;
636                 break;
637             case 0x23ba:
638                 vga_to_curses[i] = *WACS_S1;
639                 break;
640             case 0x23bb:
641                 vga_to_curses[i] = *WACS_S3;
642                 break;
643             case 0x23bc:
644                 vga_to_curses[i] = *WACS_S7;
645                 break;
646             case 0x23bd:
647                 vga_to_curses[i] = *WACS_S9;
648                 break;
649             }
650         }
651     }
652     iconv_close(ucs_to_wchar_conv);
653     iconv_close(wchar_to_ucs_conv);
654     iconv_close(font_conv);
655 }
656 
657 static void curses_setup(void)
658 {
659     int i, colour_default[8] = {
660         [QEMU_COLOR_BLACK]   = COLOR_BLACK,
661         [QEMU_COLOR_BLUE]    = COLOR_BLUE,
662         [QEMU_COLOR_GREEN]   = COLOR_GREEN,
663         [QEMU_COLOR_CYAN]    = COLOR_CYAN,
664         [QEMU_COLOR_RED]     = COLOR_RED,
665         [QEMU_COLOR_MAGENTA] = COLOR_MAGENTA,
666         [QEMU_COLOR_YELLOW]  = COLOR_YELLOW,
667         [QEMU_COLOR_WHITE]   = COLOR_WHITE,
668     };
669 
670     /* input as raw as possible, let everything be interpreted
671      * by the guest system */
672     initscr(); noecho(); intrflush(stdscr, FALSE);
673     nodelay(stdscr, TRUE); nonl(); keypad(stdscr, TRUE);
674     start_color(); raw(); scrollok(stdscr, FALSE);
675     set_escdelay(25);
676 
677     /* Make color pair to match color format (3bits bg:3bits fg) */
678     for (i = 0; i < 64; i++) {
679         init_pair(i, colour_default[i & 7], colour_default[i >> 3]);
680     }
681     /* Set default color for more than 64 for safety. */
682     for (i = 64; i < COLOR_PAIRS; i++) {
683         init_pair(i, COLOR_WHITE, COLOR_BLACK);
684     }
685 
686     font_setup();
687 }
688 
689 static void curses_keyboard_setup(void)
690 {
691 #if defined(__APPLE__)
692     /* always use generic keymaps */
693     if (!keyboard_layout)
694         keyboard_layout = "en-us";
695 #endif
696     if(keyboard_layout) {
697         kbd_layout = init_keyboard_layout(name2keysym, keyboard_layout,
698                                           &error_fatal);
699     }
700 }
701 
702 static const DisplayChangeListenerOps dcl_ops = {
703     .dpy_name        = "curses",
704     .dpy_text_update = curses_update,
705     .dpy_text_resize = curses_resize,
706     .dpy_refresh     = curses_refresh,
707     .dpy_text_cursor = curses_cursor_position,
708 };
709 
710 static void curses_display_init(DisplayState *ds, DisplayOptions *opts)
711 {
712 #ifndef _WIN32
713     if (!isatty(1)) {
714         fprintf(stderr, "We need a terminal output\n");
715         exit(1);
716     }
717 #endif
718 
719     setlocale(LC_CTYPE, "");
720     if (opts->u.curses.charset) {
721         font_charset = opts->u.curses.charset;
722     }
723     curses_setup();
724     curses_keyboard_setup();
725     atexit(curses_atexit);
726 
727     curses_winch_init();
728 
729     dcl = g_new0(DisplayChangeListener, 1);
730     dcl->ops = &dcl_ops;
731     register_displaychangelistener(dcl);
732 
733     invalidate = 1;
734 }
735 
736 static QemuDisplay qemu_display_curses = {
737     .type       = DISPLAY_TYPE_CURSES,
738     .init       = curses_display_init,
739 };
740 
741 static void register_curses(void)
742 {
743     qemu_display_register(&qemu_display_curses);
744 }
745 
746 type_init(register_curses);
747