This change adds the display(1) graphical user interface and desktop environment with basic windowing support and the graphical terminal(1) emulator along with integrations in chkblayout(1), chvideomode(1), sysinstall(8), sysupgrade(8), as well as the games and ports. Adopt the Aurora procedural wallpaper in display(1) and login(8). Remove the obsolete dispd. Juhani contributed keyboard and video mode APIs to the display protocol and other miscellaneous changes. dzwdz contributed the initial functioning window buttons, improved title bar, window tiling, and minor bug fixes Co-authored-by: Juhani Krekelä <juhani@krekelä.fi> Co-authored-by: dzwdz <kg67199@gmail.com>
1293 lines
29 KiB
C
1293 lines
29 KiB
C
/*
|
|
* Copyright (c) 2017, 2022 Jonas 'Sortie' Termansen.
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF7
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*
|
|
* terminal.c
|
|
* Terminal emulator.
|
|
*/
|
|
|
|
#include <sys/ioctl.h>
|
|
#include <sys/keycodes.h>
|
|
#include <sys/wait.h>
|
|
|
|
#include <assert.h>
|
|
#include <err.h>
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <poll.h>
|
|
#include <pthread.h>
|
|
#include <pty.h>
|
|
#include <pwd.h>
|
|
#include <signal.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
#include <wchar.h>
|
|
|
|
#if !defined(TTY_NAME_MAX)
|
|
#include <sortix/limits.h>
|
|
#endif
|
|
|
|
#include <display.h>
|
|
#include <framebuffer.h>
|
|
#include <pixel.h>
|
|
#include <vgafont.h>
|
|
|
|
#include "palette.h"
|
|
|
|
struct kbkey_sequence
|
|
{
|
|
const char* sequence;
|
|
int kbkey;
|
|
int flags;
|
|
};
|
|
|
|
#define MODIFIER_ALT (1 << 0)
|
|
#define MODIFIER_LSHIFT (1 << 1)
|
|
#define MODIFIER_RSHIFT (1 << 2)
|
|
#define MODIFIER_LCONTROL (1 << 3)
|
|
#define MODIFIER_RCONTROL (1 << 4)
|
|
|
|
#define SEQUENCE_1IFMOD (1 << 0)
|
|
#define SEQUENCE_OSHORT (1 << 1)
|
|
|
|
static const struct kbkey_sequence kbkey_sequences[] =
|
|
{
|
|
{ "\e[A", KBKEY_UP, SEQUENCE_1IFMOD },
|
|
{ "\e[B", KBKEY_DOWN, SEQUENCE_1IFMOD},
|
|
{ "\e[C", KBKEY_RIGHT, SEQUENCE_1IFMOD },
|
|
{ "\e[D", KBKEY_LEFT, SEQUENCE_1IFMOD },
|
|
{ "\e[F", KBKEY_END, SEQUENCE_1IFMOD },
|
|
{ "\e[H", KBKEY_HOME, SEQUENCE_1IFMOD },
|
|
{ "\e[2~", KBKEY_INSERT, 0 },
|
|
{ "\e[3~", KBKEY_DELETE, 0 },
|
|
{ "\e[5~", KBKEY_PGUP, 0 },
|
|
{ "\e[6~", KBKEY_PGDOWN, 0 },
|
|
{ "\e[1P", KBKEY_F1, SEQUENCE_OSHORT },
|
|
{ "\e[1Q", KBKEY_F2, SEQUENCE_OSHORT },
|
|
{ "\e[1R", KBKEY_F3, SEQUENCE_OSHORT },
|
|
{ "\e[1S", KBKEY_F4, SEQUENCE_OSHORT },
|
|
{ "\e[15~", KBKEY_F5, 0 },
|
|
{ "\e[17~", KBKEY_F6, 0 },
|
|
{ "\e[18~", KBKEY_F7, 0 },
|
|
{ "\e[19~", KBKEY_F8, 0 },
|
|
{ "\e[20~", KBKEY_F9, 0 },
|
|
{ "\e[21~", KBKEY_F10, 0 },
|
|
{ "\e[23~", KBKEY_F11, 0 },
|
|
{ "\e[24~", KBKEY_F12, 0 },
|
|
};
|
|
|
|
static inline const struct kbkey_sequence* lookup_keystroke_sequence(int kbkey)
|
|
{
|
|
size_t count = sizeof(kbkey_sequences) / sizeof(kbkey_sequences[0]);
|
|
for ( size_t i = 0; i < count; i++ )
|
|
if ( kbkey_sequences[i].kbkey == kbkey )
|
|
return &kbkey_sequences[i];
|
|
return NULL;
|
|
}
|
|
|
|
static uint32_t WINDOW_ID = 0;
|
|
static uint32_t WINDOW_WIDTH = 0;
|
|
static uint32_t WINDOW_HEIGHT = 0;
|
|
|
|
static bool need_redraw = true;
|
|
static bool need_show = true;
|
|
static bool need_exit = false;
|
|
static bool redraw_pipe_written = false;
|
|
static int redraw_pipe[2];
|
|
|
|
struct entry
|
|
{
|
|
uint32_t attr;
|
|
uint32_t fgcolor;
|
|
uint32_t bgcolor;
|
|
wchar_t wc;
|
|
};
|
|
|
|
#define ATTR_INVERSE (1 << 0)
|
|
#define ATTR_BOLD (1 << 1)
|
|
#define ATTR_UNDERLINE (1 << 2)
|
|
|
|
static struct entry* scrollback;
|
|
static size_t column = 0;
|
|
static size_t row = 0;
|
|
static size_t columns = 0;
|
|
static size_t rows = 0;
|
|
static int modifiers = 0;
|
|
static mbstate_t in_ps;
|
|
static mbstate_t out_ps;
|
|
static uint32_t default_fgcolor;
|
|
static uint32_t default_bgcolor;
|
|
static uint32_t current_fgcolor;
|
|
static uint32_t current_bgcolor;
|
|
static uint32_t attr;
|
|
static uint32_t next_attr;
|
|
static unsigned ansisavedposx;
|
|
static unsigned ansisavedposy;
|
|
static enum { NONE = 0, CSI, CHARSET, COMMAND, GREATERTHAN, } ansimode;
|
|
#define ANSI_NUM_PARAMS 16
|
|
static unsigned ansiusedparams;
|
|
static unsigned ansiparams[ANSI_NUM_PARAMS];
|
|
static bool ignore_sequence;
|
|
static bool draw_cursor = true;
|
|
|
|
static void scrollback_resize(size_t new_rows, size_t new_columns)
|
|
{
|
|
// TODO: Recover gracefully if the scrollback fails.
|
|
// TODO: Overflow.
|
|
struct entry* new_scrollback =
|
|
calloc(sizeof(struct entry), new_rows * new_columns);
|
|
if ( !new_scrollback )
|
|
err(1, "malloc");
|
|
size_t src_y_after_cursor = rows ? row + 1 : 0;
|
|
size_t src_y_count =
|
|
new_rows < src_y_after_cursor ? new_rows : src_y_after_cursor;
|
|
size_t src_y_from = src_y_after_cursor - src_y_count;
|
|
size_t new_row = row;
|
|
size_t new_column = column;
|
|
new_row += src_y_from;
|
|
for ( size_t dst_y = 0; dst_y < new_rows; dst_y++ )
|
|
{
|
|
size_t src_y = src_y_from + dst_y;
|
|
for ( size_t dst_x = 0; dst_x < new_columns; dst_x++ )
|
|
{
|
|
size_t src_x = dst_x;
|
|
struct entry tc;
|
|
if ( src_x < columns && src_y < rows )
|
|
tc = scrollback[src_y * columns + src_x];
|
|
else if ( columns && rows )
|
|
{
|
|
size_t templ_x = src_y < columns ? src_y : src_x - 1;
|
|
size_t templ_y = src_y < rows ? src_y : rows - 1;
|
|
tc = scrollback[templ_y * columns + templ_x];
|
|
tc.wc = 0;
|
|
tc.attr = 0;
|
|
}
|
|
else
|
|
tc = (struct entry) { 0 };
|
|
new_scrollback[dst_y * new_columns + dst_x] = tc;
|
|
if ( src_x == column && src_y == row )
|
|
{
|
|
new_row = dst_y;
|
|
new_column = dst_x;
|
|
}
|
|
}
|
|
}
|
|
if ( new_columns <= new_column )
|
|
new_column = new_columns ? new_column - 1 : 0;
|
|
if ( new_rows <= new_row )
|
|
new_row = new_rows ? new_row - 1 : 0;
|
|
free(scrollback);
|
|
scrollback = new_scrollback;
|
|
rows = new_rows;
|
|
columns = new_columns;
|
|
row = new_row;
|
|
column = new_column;
|
|
}
|
|
|
|
static void fill(size_t from_x, size_t from_y, size_t to_x, size_t to_y,
|
|
struct entry with)
|
|
{
|
|
// TODO: Assert within bounds?
|
|
size_t from = from_y * columns + from_x;
|
|
size_t to = to_y * columns + to_x;
|
|
for ( size_t i = from; i <= to; i++ )
|
|
scrollback[i] = with;
|
|
}
|
|
|
|
static void scroll(ssize_t offsigned, struct entry with)
|
|
{
|
|
if ( 0 < offsigned )
|
|
{
|
|
size_t off = offsigned;
|
|
if ( rows < off )
|
|
off = rows;
|
|
size_t dist = off * columns;
|
|
size_t end = rows * columns - dist;
|
|
for ( size_t i = 0; i < end; i++ )
|
|
scrollback[i] = scrollback[i + dist];
|
|
for ( size_t i = end; i < end + dist; i++ )
|
|
scrollback[i] = with;
|
|
}
|
|
else if ( offsigned < 0 )
|
|
{
|
|
size_t off = -offsigned; // TODO: Negation overflow.
|
|
if ( rows < off )
|
|
off = rows;
|
|
size_t dist = off * columns;
|
|
size_t end = rows * columns;
|
|
for ( size_t i = end-1; dist <= i; i-- )
|
|
scrollback[i] = scrollback[i - dist];
|
|
for ( size_t i = 0; i < dist; i++ )
|
|
scrollback[i] = with;
|
|
}
|
|
}
|
|
|
|
static void newline(void)
|
|
{
|
|
if ( row + 1 < rows )
|
|
{
|
|
row++;
|
|
return;
|
|
}
|
|
struct entry entry;
|
|
entry.attr = 0;
|
|
entry.fgcolor = current_fgcolor;
|
|
entry.bgcolor = current_bgcolor;
|
|
entry.wc = 0;
|
|
scroll(1, entry);
|
|
}
|
|
|
|
static void run_ansi_command(char c)
|
|
{
|
|
switch ( c )
|
|
{
|
|
case 'A': // Cursor up
|
|
{
|
|
unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
|
|
if ( row < dist )
|
|
row = 0;
|
|
else
|
|
row -= dist;
|
|
} break;
|
|
case 'B': // Cursor down
|
|
{
|
|
unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
|
|
if ( rows <= row + dist )
|
|
row = rows-1;
|
|
else
|
|
row += dist;
|
|
} break;
|
|
case 'C': // Cursor forward
|
|
{
|
|
unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
|
|
if ( columns <= column + dist )
|
|
column = columns-1;
|
|
else
|
|
column += dist;
|
|
} break;
|
|
case 'D': // Cursor backward
|
|
{
|
|
unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
|
|
if ( column < dist )
|
|
column = 0;
|
|
else
|
|
column -= dist;
|
|
} break;
|
|
case 'E': // Move to beginning of line N lines down.
|
|
{
|
|
column = 0;
|
|
unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
|
|
if ( rows <= row + dist )
|
|
row = rows-1;
|
|
else
|
|
row += dist;
|
|
} break;
|
|
case 'F': // Move to beginning of line N lines up.
|
|
{
|
|
column = 0;
|
|
unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
|
|
if ( row < dist )
|
|
row = 0;
|
|
else
|
|
row -= dist;
|
|
} break;
|
|
case 'G': // Move the cursor to column N.
|
|
{
|
|
unsigned pos = 0 < ansiusedparams ? ansiparams[0]-1 : 0;
|
|
if ( columns <= pos )
|
|
pos = columns-1;
|
|
column = pos;
|
|
} break;
|
|
case 'H': // Move the cursor to line Y, column X.
|
|
case 'f':
|
|
{
|
|
unsigned posy = 0 < ansiusedparams ? ansiparams[0]-1 : 0;
|
|
unsigned posx = 1 < ansiusedparams ? ansiparams[1]-1 : 0;
|
|
if ( columns <= posx )
|
|
posx = columns-1;
|
|
if ( rows <= posy )
|
|
posy = rows-1;
|
|
column = posx;
|
|
row = posy;
|
|
} break;
|
|
case 'J': // Erase parts of the screen.
|
|
{
|
|
unsigned mode = 0 < ansiusedparams ? ansiparams[0] : 0;
|
|
size_t from_x = 0, from_y = 0;
|
|
size_t to_x = 0, to_y = 0;
|
|
|
|
if ( mode == 0 ) // From cursor to end.
|
|
{
|
|
from_x = column;
|
|
from_y = row;
|
|
// TODO: Ensure the number of rows and columns are always non-zero.
|
|
to_x = columns - 1;
|
|
to_y = rows - 1;
|
|
}
|
|
|
|
if ( mode == 1 ) // From start to cursor.
|
|
{
|
|
from_x = 0;
|
|
from_y = 0;
|
|
to_x = columns - 1;
|
|
to_y = rows - 1;
|
|
}
|
|
|
|
if ( mode == 2 ) // Everything.
|
|
{
|
|
from_x = 0;
|
|
from_y = 0;
|
|
to_x = columns - 1;
|
|
to_y = rows - 1;
|
|
}
|
|
|
|
struct entry with;
|
|
with.attr = 0;
|
|
with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor;
|
|
with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor;
|
|
with.wc = 0;
|
|
fill(from_x, from_y, to_x, to_y, with);
|
|
} break;
|
|
case 'K': // Erase parts of the current line.
|
|
{
|
|
unsigned mode = 0 < ansiusedparams ? ansiparams[0] : 0;
|
|
size_t from_x = 0, from_y = row;
|
|
size_t to_x = 0, to_y = row;
|
|
|
|
if ( mode == 0 ) // From cursor to end.
|
|
{
|
|
from_x = column;
|
|
to_x = columns - 1;
|
|
}
|
|
|
|
if ( mode == 1 ) // From start to cursor.
|
|
{
|
|
from_x = 0;
|
|
to_x = column;
|
|
}
|
|
|
|
if ( mode == 2 ) // Everything.
|
|
{
|
|
from_x = 0;
|
|
to_x = columns - 1;
|
|
}
|
|
|
|
struct entry with;
|
|
with.attr = 0;
|
|
with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor;
|
|
with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor;
|
|
with.wc = 0;
|
|
fill(from_x, from_y, to_x, to_y, with);
|
|
} break;
|
|
// TODO: CSI Ps M Delete Ps Line(s) (default = 1) (DL).
|
|
// (delete those lines and move the rest of the lines upwards).
|
|
// TODO: CSI Ps P Delete Ps Character(s) (default = 1) (DCH).
|
|
// (delete those characters and move the rest of the line leftward).
|
|
case 'S': // Scroll a line up and place a new line at the buttom.
|
|
{
|
|
struct entry with;
|
|
with.attr = 0;
|
|
with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor;
|
|
with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor;
|
|
with.wc = 0;
|
|
scroll(1, with);
|
|
row = rows - 1;
|
|
} break;
|
|
case 'T': // Scroll a line up and place a new line at the top.
|
|
{
|
|
struct entry with;
|
|
with.attr = 0;
|
|
with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor;
|
|
with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor;
|
|
with.wc = 0;
|
|
scroll(-1, with);
|
|
row = 0;
|
|
} break;
|
|
case 'd': // Move the cursor to line N.
|
|
{
|
|
unsigned posy = 0 < ansiusedparams ? ansiparams[0]-1 : 0;
|
|
if ( rows <= posy )
|
|
posy = rows-1;
|
|
row = posy;
|
|
} break;
|
|
case 'm': // Change how the text is rendered.
|
|
{
|
|
if ( ansiusedparams == 0 )
|
|
{
|
|
ansiparams[0] = 0;
|
|
ansiusedparams++;
|
|
}
|
|
|
|
for ( size_t i = 0; i < ansiusedparams; i++ )
|
|
{
|
|
unsigned cmd = ansiparams[i];
|
|
// Turn all attributes off.
|
|
if ( cmd == 0 )
|
|
{
|
|
attr = 0;
|
|
current_fgcolor = default_fgcolor;
|
|
current_bgcolor = default_bgcolor;
|
|
}
|
|
// Boldness.
|
|
else if ( cmd == 1 )
|
|
attr |= ATTR_BOLD;
|
|
// TODO: 2, Faint
|
|
// TODO: 3, Italicized
|
|
// Underline.
|
|
else if ( cmd == 4 )
|
|
attr |= ATTR_UNDERLINE;
|
|
// TODO: 5, Blink (appears as Bold)
|
|
// Inverse.
|
|
else if ( cmd == 7 )
|
|
attr |= ATTR_INVERSE;
|
|
// TODO: 8, Invisible
|
|
// TODO: 9, Crossed-out
|
|
// TODO: 21, Doubly-underlined
|
|
// Normal (neither bold nor faint).
|
|
else if ( cmd == 22 )
|
|
attr &= ~ATTR_BOLD;
|
|
// TODO: 23, Not italicized
|
|
// Not underlined.
|
|
else if ( cmd == 24 )
|
|
attr &= ~ATTR_UNDERLINE;
|
|
// TODO: 25, Steady (not blinking)
|
|
// Positive (not inverse).
|
|
else if ( cmd == 27 )
|
|
attr &= ~ATTR_INVERSE;
|
|
// TODO: 28, Visible (not hidden)
|
|
// Set text color.
|
|
else if ( 30 <= cmd && cmd <= 37 )
|
|
{
|
|
unsigned val = cmd - 30;
|
|
current_fgcolor = palette[val] | 0xFF000000;
|
|
}
|
|
// Set text color.
|
|
else if ( cmd == 38 )
|
|
{
|
|
if ( 5 <= ansiusedparams - i && ansiparams[i+1] == 2 )
|
|
{
|
|
uint8_t r = ansiparams[i+2];
|
|
uint8_t g = ansiparams[i+3];
|
|
uint8_t b = ansiparams[i+4];
|
|
i += 5 - 1;
|
|
current_fgcolor = make_color(r, g, b);
|
|
}
|
|
else if ( 3 <= ansiusedparams - i && ansiparams[i+1] == 5 )
|
|
{
|
|
uint8_t index = ansiparams[i+2];
|
|
i += 3 - 1;
|
|
current_fgcolor = palette[index] | 0xFF000000;
|
|
}
|
|
}
|
|
// Set default text color.
|
|
else if ( cmd == 39 )
|
|
{
|
|
current_fgcolor = default_fgcolor;
|
|
}
|
|
// Set background color.
|
|
else if ( 40 <= cmd && cmd <= 47 )
|
|
{
|
|
unsigned val = cmd - 40;
|
|
current_bgcolor = palette[val] | 0xFF000000;
|
|
}
|
|
// Set background color.
|
|
else if ( cmd == 48 )
|
|
{
|
|
if ( 5 <= ansiusedparams - i && ansiparams[i+1] == 2 )
|
|
{
|
|
uint8_t r = ansiparams[i+2];
|
|
uint8_t g = ansiparams[i+3];
|
|
uint8_t b = ansiparams[i+4];
|
|
i += 5 - 1;
|
|
current_bgcolor = make_color(r, g, b);
|
|
}
|
|
else if ( 3 <= ansiusedparams - i && ansiparams[i+1] == 5 )
|
|
{
|
|
uint8_t index = ansiparams[i+2];
|
|
i += 3 - 1;
|
|
current_bgcolor = palette[index] | 0xFF000000;
|
|
}
|
|
}
|
|
// Set default background color.
|
|
else if ( cmd == 49 )
|
|
{
|
|
current_bgcolor = default_bgcolor;
|
|
}
|
|
// Set text color.
|
|
else if ( 90 <= cmd && cmd <= 97 )
|
|
{
|
|
unsigned val = cmd - 90 + 8;
|
|
current_fgcolor = palette[val] | 0xFF000000;
|
|
}
|
|
// Set background color.
|
|
else if ( 100 <= cmd && cmd <= 107 )
|
|
{
|
|
unsigned val = cmd - 100 + 8;
|
|
current_bgcolor = palette[val] | 0xFF000000;
|
|
}
|
|
else
|
|
{
|
|
ansimode = NONE;
|
|
}
|
|
}
|
|
} break;
|
|
case 'n': // Request special information from terminal.
|
|
{
|
|
ansimode = NONE;
|
|
// TODO: Handle this code.
|
|
} break;
|
|
case 's': // Save cursor position.
|
|
{
|
|
ansisavedposx = column;
|
|
ansisavedposy = row;
|
|
} break;
|
|
case 'u': // Restore cursor position.
|
|
{
|
|
column = ansisavedposx;
|
|
row = ansisavedposy;
|
|
if ( columns <= column )
|
|
column = columns-1;
|
|
if ( rows <= row )
|
|
row = rows-1;
|
|
} break;
|
|
case 'l': // Hide cursor.
|
|
{
|
|
// TODO: This is somehow related to the special char '?'.
|
|
if ( 0 < ansiusedparams && ansiparams[0] == 25 )
|
|
draw_cursor = false;
|
|
if ( 0 < ansiusedparams && ansiparams[0] == 1049 )
|
|
{}; // TODO: Save scrollback.
|
|
} break;
|
|
case 'h': // Show cursor.
|
|
{
|
|
// TODO: This is somehow related to the special char '?'.
|
|
if ( 0 < ansiusedparams && ansiparams[0] == 25 )
|
|
draw_cursor = true;
|
|
if ( 0 < ansiusedparams && ansiparams[0] == 1049 )
|
|
{}; // TODO: Restore scrollback.
|
|
} break;
|
|
default:
|
|
{
|
|
ansimode = NONE;
|
|
}
|
|
// TODO: Handle other cases.
|
|
}
|
|
|
|
ansimode = NONE;
|
|
}
|
|
|
|
static void put_ansi_escaped(char c)
|
|
{
|
|
// Check the proper prefixes are used.
|
|
if ( ansimode == CSI )
|
|
{
|
|
if ( c == '[' )
|
|
ansimode = COMMAND;
|
|
else if ( c == '(' || c == ')' || c == '*' || c == '+' ||
|
|
c == '-' || c == '.' || c == '/' )
|
|
ansimode = CHARSET;
|
|
// TODO: Enter and exit alternatve keypad mode.
|
|
else if ( c == '=' || c == '>' )
|
|
ansimode = NONE;
|
|
else
|
|
{
|
|
ansimode = NONE;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ( ansimode == CHARSET )
|
|
{
|
|
ansimode = NONE;
|
|
return;
|
|
}
|
|
|
|
// Read part of a parameter.
|
|
if ( '0' <= c && c <= '9' )
|
|
{
|
|
if ( ansiusedparams == 0 )
|
|
ansiusedparams++;
|
|
unsigned val = c - '0';
|
|
ansiparams[ansiusedparams-1] *= 10;
|
|
ansiparams[ansiusedparams-1] += val;
|
|
}
|
|
|
|
// Parameter delimiter.
|
|
else if ( c == ';' )
|
|
{
|
|
if ( ansiusedparams == ANSI_NUM_PARAMS )
|
|
{
|
|
ansimode = NONE;
|
|
return;
|
|
}
|
|
ansiparams[ansiusedparams++] = 0;
|
|
}
|
|
|
|
// Left for future standardization, so discard this sequence.
|
|
else if ( c == ':' )
|
|
{
|
|
ignore_sequence = true;
|
|
}
|
|
|
|
else if ( c == '>' )
|
|
{
|
|
ansimode = GREATERTHAN;
|
|
}
|
|
|
|
// Run a command.
|
|
else if ( 64 <= c && c <= 126 )
|
|
{
|
|
if ( !ignore_sequence )
|
|
{
|
|
if ( ansimode == COMMAND )
|
|
run_ansi_command(c);
|
|
else if ( ansimode == GREATERTHAN )
|
|
{
|
|
// Send Device Attributes
|
|
if ( c == 'c' )
|
|
{
|
|
// TODO: Send an appropriate response through the terminal.
|
|
}
|
|
else
|
|
{
|
|
ansimode = NONE;
|
|
return;
|
|
}
|
|
ansimode = NONE;
|
|
}
|
|
}
|
|
else
|
|
ansimode = NONE;
|
|
}
|
|
|
|
// Something I don't understand, and ignore intentionally.
|
|
else if ( c == '?' )
|
|
{
|
|
//ansimode = NONE;
|
|
}
|
|
|
|
// TODO: There are some rare things that should be supported here.
|
|
|
|
// Ignore unknown input.
|
|
else
|
|
{
|
|
ansimode = NONE;
|
|
}
|
|
}
|
|
|
|
static uint32_t boldify(uint32_t color)
|
|
{
|
|
int b = color >> 0 & 0xFF;
|
|
int g = color >> 8 & 0xFF;
|
|
int r = color >> 16 & 0xFF;
|
|
int a = color >> 24 & 0xFF;
|
|
b += 63;
|
|
if ( 255 < b )
|
|
b = 255;
|
|
g += 63;
|
|
if ( 255 < g )
|
|
g = 255;
|
|
r += 63;
|
|
if ( 255 < r )
|
|
r = 255;
|
|
return make_color_a(r, g, b, a);
|
|
}
|
|
|
|
static void outwc(wchar_t wc)
|
|
{
|
|
if ( wc == L'\a' )
|
|
{
|
|
}
|
|
else if ( wc == L'\n' )
|
|
{
|
|
newline();
|
|
}
|
|
else if ( wc == L'\r' )
|
|
{
|
|
column = 0;
|
|
}
|
|
else if ( wc == L'\b' )
|
|
{
|
|
if ( column )
|
|
{
|
|
column--;
|
|
struct entry* entry = &scrollback[row * columns + column];
|
|
next_attr = entry->attr & (ATTR_BOLD | ATTR_UNDERLINE);
|
|
if ( entry->wc == L'_' )
|
|
next_attr |= ATTR_UNDERLINE;
|
|
else if ( entry->wc == L' ' )
|
|
next_attr &= ~ATTR_BOLD;
|
|
else
|
|
next_attr |= ATTR_BOLD;
|
|
}
|
|
}
|
|
else if ( wc == L'\t' )
|
|
{
|
|
if ( column == columns )
|
|
{
|
|
newline();
|
|
column = 0;
|
|
}
|
|
column++;
|
|
column = -(-column & ~((size_t)0x7));
|
|
if ( columns <= column )
|
|
column = columns;
|
|
}
|
|
else if ( wc == L'\e' )
|
|
{
|
|
next_attr = 0;
|
|
ansiusedparams = 0;
|
|
ansiparams[0] = 0;
|
|
ignore_sequence = false;
|
|
ansimode = CSI;
|
|
}
|
|
else
|
|
{
|
|
if ( column == columns )
|
|
{
|
|
newline();
|
|
column = 0;
|
|
}
|
|
struct entry* entry = &scrollback[row * columns + column++];
|
|
entry->attr = attr | next_attr;
|
|
if ( !(entry->attr & ATTR_INVERSE) )
|
|
{
|
|
entry->fgcolor = current_fgcolor;
|
|
entry->bgcolor = current_bgcolor;
|
|
}
|
|
else
|
|
{
|
|
entry->fgcolor = current_bgcolor;
|
|
entry->bgcolor = current_fgcolor;
|
|
}
|
|
if ( entry->attr & ATTR_BOLD )
|
|
entry->fgcolor = boldify(entry->fgcolor);
|
|
entry->wc = wc;
|
|
next_attr = 0;
|
|
}
|
|
}
|
|
|
|
static void outc(char c)
|
|
{
|
|
if ( ansimode != NONE )
|
|
{
|
|
put_ansi_escaped(c);
|
|
return;
|
|
}
|
|
wchar_t wc;
|
|
size_t amount = mbrtowc(&wc, &c, 1, &out_ps);
|
|
if ( amount == (size_t) -2 )
|
|
return;
|
|
if ( amount == (size_t) -1 )
|
|
{
|
|
memset(&out_ps, 0, sizeof(out_ps));
|
|
wc = 0xFFFD; /* REPLACEMENT CHARACTER */;
|
|
}
|
|
if ( amount == (size_t) 0 )
|
|
wc = L' ';
|
|
outwc(wc);
|
|
}
|
|
|
|
static pthread_mutex_t scrollback_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
static int master_fd;
|
|
|
|
static void* outgoing_thread(void* ctx)
|
|
{
|
|
(void) ctx;
|
|
const char* getcursor = "\e[6n";
|
|
size_t i = 0;
|
|
char c;
|
|
ssize_t amount = 0;
|
|
while ( 0 < (amount = read(master_fd, &c, 1)) )
|
|
{
|
|
// TODO: Do escape code handling in the escape code parsing.
|
|
if ( c == getcursor[i] )
|
|
{
|
|
i++;
|
|
if ( !getcursor[i] )
|
|
{
|
|
i = 0;
|
|
char buf[64];
|
|
snprintf(buf, sizeof(buf), "\e[%zu;%zuR",
|
|
(size_t) (row + 1), (size_t) (column + 1));
|
|
for ( size_t n = 0; buf[n]; n++ )
|
|
{
|
|
if ( write(master_fd, &buf[n], 1) <= 0 )
|
|
{
|
|
warn("incoming write");
|
|
break; // TODO: This break is incorrect.
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
pthread_mutex_lock(&scrollback_mutex);
|
|
for ( size_t j = 0; j < i; j++ )
|
|
outc(getcursor[j]);
|
|
i = 0;
|
|
outc(c);
|
|
if ( !redraw_pipe_written )
|
|
{
|
|
char c = 'X';
|
|
if ( write(redraw_pipe[1], &c, 1) < 0 )
|
|
warn("write: redraw_pipe");
|
|
else
|
|
redraw_pipe_written = true;
|
|
}
|
|
pthread_mutex_unlock(&scrollback_mutex);
|
|
}
|
|
if ( amount < 0 )
|
|
warn("outgoing read");
|
|
return NULL;
|
|
}
|
|
|
|
void on_disconnect(void* ctx)
|
|
{
|
|
(void) ctx;
|
|
need_exit = true;
|
|
}
|
|
|
|
void on_quit(void* ctx, uint32_t window_id)
|
|
{
|
|
(void) ctx;
|
|
if ( window_id != WINDOW_ID )
|
|
return;
|
|
need_exit = true;
|
|
}
|
|
|
|
void on_resize(void* ctx, uint32_t window_id, uint32_t width, uint32_t height)
|
|
{
|
|
(void) ctx;
|
|
if ( window_id != WINDOW_ID )
|
|
return;
|
|
// TODO: The resolution 0x0 gets sent for newly created windows that hasn't
|
|
// been shown yet, fix the compositor to not do this.
|
|
if ( width == 0 && height == 0 )
|
|
return;
|
|
pthread_mutex_lock(&scrollback_mutex);
|
|
size_t new_rows = height / FONT_HEIGHT;
|
|
size_t new_columns = width / FONT_WIDTH;
|
|
scrollback_resize(new_rows, new_columns);
|
|
struct winsize ws;
|
|
ws.ws_row = rows;
|
|
ws.ws_col = columns;
|
|
if ( ioctl(master_fd, TIOCSWINSZ, &ws) < 0 )
|
|
warn("TIOCSWINSZ");
|
|
pthread_mutex_unlock(&scrollback_mutex);
|
|
need_redraw = true;
|
|
WINDOW_WIDTH = width;
|
|
WINDOW_HEIGHT = height;
|
|
}
|
|
|
|
void inuc(unsigned char uc)
|
|
{
|
|
write(master_fd, &uc, 1);
|
|
}
|
|
|
|
void on_keycode(int kbkey)
|
|
{
|
|
if ( kbkey < 0 )
|
|
return;
|
|
|
|
if ( kbkey == KBKEY_ESC )
|
|
{
|
|
inuc('\e');
|
|
return;
|
|
}
|
|
|
|
const struct kbkey_sequence* seq = lookup_keystroke_sequence(kbkey);
|
|
if ( !seq )
|
|
return;
|
|
|
|
const char* str = seq->sequence;
|
|
size_t len = strlen(str);
|
|
|
|
int mods = 0;
|
|
if ( modifiers & (MODIFIER_LSHIFT | MODIFIER_RSHIFT) )
|
|
mods |= 1;
|
|
if ( modifiers & MODIFIER_ALT )
|
|
mods |= 2;
|
|
if ( modifiers & (MODIFIER_LCONTROL | MODIFIER_RCONTROL) )
|
|
mods |= 4;
|
|
|
|
if ( (seq->flags & SEQUENCE_OSHORT) && mods == 0 )
|
|
{
|
|
inuc('\e');
|
|
inuc('O');
|
|
inuc((unsigned char) str[len-1]);
|
|
return;
|
|
}
|
|
|
|
for ( size_t i = 0; i < len - 1; i++ )
|
|
inuc((unsigned char) str[i]);
|
|
if ( seq->flags & SEQUENCE_1IFMOD && mods != 0 )
|
|
inuc('1');
|
|
if ( mods )
|
|
{
|
|
inuc(';');
|
|
inuc('1' + mods);
|
|
}
|
|
inuc(str[len-1]);
|
|
}
|
|
|
|
void on_keyboard(void* ctx, uint32_t window_id, uint32_t codepoint)
|
|
{
|
|
(void) ctx;
|
|
if ( window_id != WINDOW_ID )
|
|
return;
|
|
int kbkey = KBKEY_DECODE(codepoint);
|
|
if ( kbkey != 0 )
|
|
{
|
|
// TODO: Don't do this here, let the compositor do this.
|
|
if ( kbkey == KBKEY_LALT )
|
|
modifiers |= MODIFIER_ALT;
|
|
else if ( kbkey == -KBKEY_LALT )
|
|
modifiers &= ~MODIFIER_ALT;
|
|
else if ( kbkey == KBKEY_LSHIFT )
|
|
modifiers |= MODIFIER_LSHIFT;
|
|
else if ( kbkey == -KBKEY_LSHIFT )
|
|
modifiers &= ~MODIFIER_LSHIFT;
|
|
else if ( kbkey == KBKEY_RSHIFT )
|
|
modifiers |= MODIFIER_RSHIFT;
|
|
else if ( kbkey == -KBKEY_RSHIFT )
|
|
modifiers &= ~MODIFIER_RSHIFT;
|
|
else if ( kbkey == KBKEY_LCTRL )
|
|
modifiers |= MODIFIER_LCONTROL;
|
|
else if ( kbkey == -KBKEY_LCTRL )
|
|
modifiers &= ~MODIFIER_LCONTROL;
|
|
else if ( kbkey == KBKEY_RCTRL )
|
|
modifiers |= MODIFIER_RCONTROL;
|
|
else if ( kbkey == -KBKEY_RCTRL )
|
|
modifiers &= ~MODIFIER_RCONTROL;
|
|
on_keycode(kbkey);
|
|
return;
|
|
}
|
|
if ( codepoint == '\n' )
|
|
codepoint = '\r';
|
|
bool control = modifiers & (MODIFIER_LCONTROL | MODIFIER_RCONTROL);
|
|
if ( codepoint == '\b' )
|
|
codepoint = 127;
|
|
if ( modifiers & MODIFIER_ALT )
|
|
inuc('\e');
|
|
if ( control && codepoint == L' ' )
|
|
inuc(0);
|
|
else if ( control && (L'`' <= codepoint && codepoint <= L'}') )
|
|
inuc(codepoint - L'`');
|
|
else if ( control && (L'@' <= codepoint && codepoint <= L'_') )
|
|
inuc(codepoint - L'@');
|
|
else if ( control && codepoint == L'?' )
|
|
inuc(127);
|
|
else
|
|
{
|
|
wchar_t wc = codepoint;
|
|
char mb[MB_CUR_MAX];
|
|
size_t amount = wcrtomb(mb, wc, &in_ps);
|
|
if ( amount == (size_t) -1 )
|
|
memset(&in_ps, 0, sizeof(in_ps));
|
|
else
|
|
{
|
|
for ( size_t i = 0; i < amount; i++ )
|
|
inuc((unsigned char) mb[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void draw(struct display_connection* connection)
|
|
{
|
|
uint32_t* framebuffer = (uint32_t*)
|
|
calloc(sizeof(uint32_t), WINDOW_WIDTH * WINDOW_HEIGHT);
|
|
assert(framebuffer);
|
|
|
|
struct framebuffer fb;
|
|
fb.pitch = WINDOW_WIDTH;
|
|
fb.buffer = framebuffer;
|
|
fb.xres = WINDOW_WIDTH;
|
|
fb.yres = WINDOW_HEIGHT;
|
|
|
|
pthread_mutex_lock(&scrollback_mutex);
|
|
size_t draw_rows = WINDOW_HEIGHT / FONT_HEIGHT;
|
|
size_t draw_columns = WINDOW_WIDTH / FONT_WIDTH;
|
|
if ( rows < draw_rows )
|
|
draw_rows = rows;
|
|
if ( columns < draw_columns )
|
|
draw_rows = columns;
|
|
for ( size_t y = 0; y < rows; y++ )
|
|
{
|
|
size_t yoff = FONT_HEIGHT * y;
|
|
for ( size_t x = 0; x < columns; x++ )
|
|
{
|
|
size_t xoff = FONT_WIDTH * x;
|
|
size_t cell_width = FONT_WIDTH;
|
|
size_t cell_height = FONT_HEIGHT;
|
|
if ( x + 1 == columns )
|
|
cell_width = WINDOW_WIDTH - xoff;
|
|
if ( y + 1 == rows )
|
|
cell_height = WINDOW_HEIGHT - yoff;
|
|
struct framebuffer charfb =
|
|
framebuffer_crop(fb, xoff, yoff, cell_width, cell_height);
|
|
struct entry* entry = &scrollback[y * columns + x];
|
|
for ( size_t py = 0; py < charfb.yres; py++ )
|
|
{
|
|
uint32_t* line = charfb.buffer + py * charfb.pitch;
|
|
for ( size_t px = 0; px < charfb.xres; px++ )
|
|
line[px] = entry->bgcolor;
|
|
}
|
|
render_char(charfb, entry->wc, entry->fgcolor);
|
|
size_t entry_width = FONT_WIDTH;
|
|
size_t entry_height = FONT_HEIGHT;
|
|
if ( charfb.xres < entry_width )
|
|
entry_width = charfb.xres;
|
|
if ( charfb.yres < entry_height )
|
|
entry_height = charfb.yres;
|
|
size_t underlines = 0;
|
|
if ( draw_cursor && y == row && x == column )
|
|
underlines = 2;
|
|
else if ( entry->attr & ATTR_UNDERLINE )
|
|
underlines = 1;
|
|
size_t start_underlines = FONT_HEIGHT - underlines;
|
|
for ( size_t py = start_underlines; py < entry_height; py++ )
|
|
{
|
|
uint32_t* line = charfb.buffer + py * charfb.pitch;
|
|
for ( size_t x = 0; x < entry_width; x++ )
|
|
line[x] = blend_pixel(line[x], entry->fgcolor);
|
|
}
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&scrollback_mutex);
|
|
|
|
display_render_window(connection, WINDOW_ID, 0, 0, WINDOW_WIDTH,
|
|
WINDOW_HEIGHT, framebuffer);
|
|
|
|
free(framebuffer);
|
|
}
|
|
|
|
static void signal_handler(int signum)
|
|
{
|
|
(void) signum;
|
|
}
|
|
|
|
int main(int argc, char* argv[])
|
|
{
|
|
struct display_connection* connection = display_connect_default();
|
|
if ( !connection && errno == ECONNREFUSED )
|
|
display_spawn(argc, argv);
|
|
if ( !connection )
|
|
err(1, "Could not connect to display server");
|
|
|
|
int opt;
|
|
while ( (opt = getopt(argc, argv, "")) != -1 )
|
|
{
|
|
switch ( opt )
|
|
{
|
|
default: return 1;
|
|
}
|
|
}
|
|
|
|
load_font();
|
|
|
|
if ( pipe(redraw_pipe) < 0 )
|
|
err(1, "pipe");
|
|
|
|
rows = 25;
|
|
columns = 80;
|
|
|
|
// TODO: Overflow.
|
|
scrollback = calloc(sizeof(struct entry), rows * columns);
|
|
if ( !scrollback )
|
|
err(1, "malloc");
|
|
|
|
WINDOW_WIDTH = columns * FONT_WIDTH;
|
|
WINDOW_HEIGHT = rows * FONT_HEIGHT;
|
|
|
|
default_bgcolor = make_color_a(0, 0, 0, 220);
|
|
default_fgcolor = palette[7] | 0xFF000000;
|
|
current_bgcolor = default_bgcolor;
|
|
current_fgcolor = default_fgcolor;
|
|
for ( size_t y = 0; y < rows; y++ )
|
|
{
|
|
for ( size_t x = 0; x < columns; x++ )
|
|
{
|
|
struct entry* entry = &scrollback[y * columns + x];
|
|
entry->attr = 0;
|
|
entry->fgcolor = current_fgcolor;
|
|
entry->bgcolor = current_bgcolor;
|
|
entry->wc = 0;
|
|
}
|
|
}
|
|
|
|
display_create_window(connection, WINDOW_ID);
|
|
display_resize_window(connection, WINDOW_ID, WINDOW_WIDTH, WINDOW_HEIGHT);
|
|
display_title_window(connection, WINDOW_ID, "Terminal");
|
|
|
|
struct winsize ws;
|
|
ws.ws_row = rows;
|
|
ws.ws_col = columns;
|
|
char path[TTY_NAME_MAX + 1];
|
|
int slave_fd;
|
|
if ( openpty(&master_fd, &slave_fd, path, NULL, &ws) < 0 )
|
|
err(1, "openpty");
|
|
|
|
sigset_t saved_mask, sigchld_mask;
|
|
sigemptyset(&sigchld_mask);
|
|
sigaddset(&sigchld_mask, SIGCHLD);
|
|
sigprocmask(SIG_BLOCK, &sigchld_mask, &saved_mask);
|
|
struct sigaction sa = { .sa_handler = signal_handler };
|
|
struct sigaction old_sa;
|
|
sigaction(SIGCHLD, &sa, &old_sa);
|
|
|
|
pid_t child_pid = fork();
|
|
if ( child_pid < 0 )
|
|
err(1, "fork");
|
|
|
|
if ( !child_pid )
|
|
{
|
|
sigprocmask(SIG_SETMASK, &saved_mask, NULL);
|
|
if ( setsid() < 0 )
|
|
{
|
|
warn("setsid");
|
|
_exit(1);
|
|
}
|
|
if ( ioctl(slave_fd, TIOCSCTTY) < 0 )
|
|
{
|
|
warn("ioctl: TIOCSCTTY");
|
|
_exit(1);
|
|
}
|
|
if ( close(0) < 0 || close(1) < 0 || close(2) < 0 )
|
|
{
|
|
warn("close");
|
|
_exit(1);
|
|
}
|
|
if ( dup2(slave_fd, 0) != 0 ||
|
|
dup2(slave_fd, 1) != 1 ||
|
|
dup2(slave_fd, 2) != 2 )
|
|
{
|
|
warn("dup");
|
|
_exit(1);
|
|
}
|
|
if ( closefrom(3) < 0 )
|
|
{
|
|
warn("closefrom");
|
|
_exit(1);
|
|
}
|
|
const char* program;
|
|
if ( argc <= optind )
|
|
{
|
|
program = "sh";
|
|
uid_t uid = getuid();
|
|
struct passwd* pwd = getpwuid(uid);
|
|
if ( !pwd )
|
|
warn("getpwuid: %ju", (uintmax_t) uid);
|
|
else
|
|
program = pwd->pw_shell;
|
|
char* login_program;
|
|
if ( asprintf(&login_program, "-%s", program) < 0 )
|
|
{
|
|
warn("malloc");
|
|
_exit(1);
|
|
}
|
|
execlp(program, login_program, (const char*) NULL);
|
|
}
|
|
else
|
|
{
|
|
// Support explicit login shells using a leading dash.
|
|
program = argv[optind];
|
|
if ( program[0] == '-' )
|
|
program++;
|
|
execvp(program, argv + optind);
|
|
}
|
|
warn("%s", program);
|
|
_exit(127);
|
|
}
|
|
|
|
int errnum;
|
|
pthread_t outthread;
|
|
if ( (errnum = pthread_create(&outthread, NULL, outgoing_thread, NULL)) )
|
|
{
|
|
errno = errnum;
|
|
err(1, "pthread_create");
|
|
}
|
|
|
|
struct display_event_handlers handlers;
|
|
memset(&handlers, 0, sizeof(handlers));
|
|
handlers.disconnect_handler = on_disconnect;
|
|
handlers.quit_handler = on_quit;
|
|
handlers.resize_handler = on_resize;
|
|
handlers.keyboard_handler = on_keyboard;
|
|
|
|
const nfds_t nfds = 2;
|
|
struct pollfd pfds[nfds];
|
|
pfds[0].fd = redraw_pipe[0];
|
|
pfds[0].events = POLLIN;
|
|
pfds[0].revents = 0;
|
|
pfds[1].fd = display_connection_fd(connection);
|
|
pfds[1].events = POLLIN;
|
|
pfds[1].revents = 0;
|
|
|
|
while ( !need_exit )
|
|
{
|
|
int status;
|
|
if ( 0 < waitpid(child_pid, &status, WNOHANG) )
|
|
break;
|
|
|
|
if ( need_redraw )
|
|
{
|
|
draw(connection);
|
|
need_redraw = false;
|
|
}
|
|
|
|
if ( need_show )
|
|
{
|
|
display_show_window(connection, WINDOW_ID);
|
|
need_show = false;
|
|
}
|
|
|
|
if ( ppoll(pfds, nfds, NULL, &saved_mask) < 0 )
|
|
{
|
|
if ( errno == EINTR )
|
|
continue;
|
|
err(1, "poll");
|
|
}
|
|
|
|
if ( pfds[0].revents )
|
|
{
|
|
pthread_mutex_lock(&scrollback_mutex);
|
|
char c;
|
|
ssize_t amount = read(redraw_pipe[0], &c, 1);
|
|
if ( amount < 0 )
|
|
err(1, "read: redraw_pipe");
|
|
if ( 0 < amount )
|
|
{
|
|
redraw_pipe_written = false;
|
|
need_redraw = true;
|
|
}
|
|
pthread_mutex_unlock(&scrollback_mutex);
|
|
}
|
|
|
|
if ( pfds[1].revents )
|
|
{
|
|
while ( display_poll_event(connection, &handlers) == 0 );
|
|
}
|
|
}
|
|
|
|
display_disconnect(connection);
|
|
|
|
return 0;
|
|
}
|