1
0
mirror of https://github.com/rkd77/elinks.git synced 2024-12-04 14:46:47 -05:00
elinks/src/osdep/osdep.c
Kalle Olavi Niemitalo b6dfdf86a6 Bug 885: Proper charset support in xterm window title
When ELinks runs in an X11 terminal emulator (e.g. xterm), or in GNU
Screen, it tries to update the title of the window to match the title
of the current document.  To do this, ELinks sends an "OSC 1 ; Pt BEL"
sequence to the terminal.  Unfortunately, xterm expects the Pt string
to be in the ISO-8859-1 charset, making it impossible to display e.g.
Cyrillic characters.  In xterm patch #210 (2006-03-12) however, there
is a menu item and a resource that can make xterm take the Pt string
in UTF-8 instead, allowing characters from all around the world.
The downside is that ELinks apparently cannot ask xterm whether the
setting is on or off; so add a terminal._template_.latin1_title option
to ELinks and let the user edit that instead.

Complete list of changes:

- Add the terminal._template_.latin1_title option.  But do not add
  that to the terminal options window because it's already rather
  crowded there.

- In set_window_title(), take a new codepage argument.  Use it to
  decode the title into Unicode characters, and remove only actual
  control characters.  For example, CP437 has graphical characters in
  the 0x80...0x9F range, so don't remove those, even though ISO-8859-1
  has control characters in the same range.  Likewise, don't
  misinterpret single bytes of UTF-8 characters as control characters.

- In set_window_title(), do not truncate the title to the width of the
  window.  The font is likely to be different and proportional anyway.
  But do truncate before 1024 bytes, an xterm limit.

- In struct itrm, add a title_codepage member to remember which
  charset the master said it was going to use in the terminal window
  title.  Initialize title_codepage in handle_trm(), update it in
  dispatch_special() if the master sends the new request
  TERM_FN_TITLE_CODEPAGE, and use it in most set_window_title() calls;
  but not in the one that sets $TERM as the title, because that string
  was not received from the master and should consist of ASCII
  characters only.

- In set_terminal_title(), convert the caller-provided title to
  ISO-8859-1 or UTF-8 if appropriate, and report the codepage to the
  slave with the new TERM_FN_TITLE_CODEPAGE request.  The conversion
  can run out of memory, so return a success/error flag, rather than
  void.  In display_window_title(), check this result and don't update
  caches on error.

- Add a NEWS entry for all of this.
2009-01-01 16:17:03 +00:00

912 lines
18 KiB
C

/* Features which vary with the OS */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <errno.h>
#ifdef HAVE_IO_H
#include <io.h> /* For win32 && set_bin(). */
#endif
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif
#ifdef HAVE_SYS_SIGNAL_H
#include <sys/signal.h>
#endif
#include <sys/types.h>
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h> /* Need to be after sys/types.h */
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h> /* OS/2 needs this after sys/types.h */
#endif
/* We need to have it here. Stupid BSD. */
#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#ifdef HAVE_ARPA_INET_H
#include <arpa/inet.h>
#endif
/* This is for some exotic TOS mangling when handling passive FTP sockets. */
#ifdef HAVE_NETINET_IN_SYSTM_H
#include <netinet/in_systm.h>
#else
#ifdef HAVE_NETINET_IN_SYSTEM_H
#include <netinet/in_system.h>
#endif
#endif
#ifdef HAVE_NETINET_IP_H
#include <netinet/ip.h>
#endif
#ifdef HAVE_TERMIOS_H
#include <termios.h>
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_LOCALE_H
/* For the sake of SunOS, keep this away from files including
* intl/gettext/libintl.h because <locale.h> includes system <libintl.h> which
* either includes system gettext header or contains gettext function
* declarations. */
#include <locale.h>
#endif
#ifdef HAVE_X11
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#endif
#include "elinks.h"
#include "main/select.h"
#include "osdep/osdep.h"
#include "osdep/signals.h"
#include "terminal/terminal.h"
#include "util/conv.h"
#include "util/memory.h"
#include "util/string.h"
/* Set a file descriptor to non-blocking mode. It returns a non-zero value
* on error. */
int
set_nonblocking_fd(int fd)
{
#if defined(O_NONBLOCK) || defined(O_NDELAY)
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) return -1;
#if defined(O_NONBLOCK)
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
#else
return fcntl(fd, F_SETFL, flags | O_NDELAY);
#endif
#elif defined(FIONBIO)
int flag = 1;
return ioctl(fd, FIONBIO, &flag);
#else
return 0;
#endif
}
/* Set a file descriptor to blocking mode. It returns a non-zero value on
* error. */
int
set_blocking_fd(int fd)
{
#if defined(O_NONBLOCK) || defined(O_NDELAY)
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) return -1;
#if defined(O_NONBLOCK)
return fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
#else
return fcntl(fd, F_SETFL, flags & ~O_NDELAY);
#endif
#elif defined(FIONBIO)
int flag = 0;
return ioctl(fd, FIONBIO, &flag);
#else
return 0;
#endif
}
void
set_ip_tos_throughput(int socket)
{
#if defined(IP_TOS) && defined(IPTOS_THROUGHPUT)
int on = IPTOS_THROUGHPUT;
setsockopt(socket, IPPROTO_IP, IP_TOS, (char *) &on, sizeof(on));
#endif
}
int
get_e(unsigned char *env)
{
char *v = getenv(env);
return (v ? atoi(v) : 0);
}
unsigned char *
get_cwd(void)
{
int bufsize = 128;
unsigned char *buf;
while (1) {
buf = mem_alloc(bufsize);
if (!buf) return NULL;
if (getcwd(buf, bufsize)) return buf;
mem_free(buf);
if (errno == EINTR) continue;
if (errno != ERANGE) return NULL;
bufsize += 128;
}
return NULL;
}
void
set_cwd(unsigned char *path)
{
if (path) while (chdir(path) && errno == EINTR);
}
unsigned char *
get_shell(void)
{
unsigned char *shell = GETSHELL;
if (!shell || !*shell)
shell = DEFAULT_SHELL;
return shell;
}
/* Terminal size */
#if !defined(CONFIG_OS_OS2) && !defined(CONFIG_OS_WIN32)
static void
sigwinch(void *s)
{
((void (*)(void)) s)();
}
void
handle_terminal_resize(int fd, void (*fn)(void))
{
install_signal_handler(SIGWINCH, sigwinch, fn, 0);
}
void
unhandle_terminal_resize(int fd)
{
install_signal_handler(SIGWINCH, NULL, NULL, 0);
}
void
get_terminal_size(int fd, int *x, int *y)
{
struct winsize ws;
if (ioctl(1, TIOCGWINSZ, &ws) != -1) {
*x = ws.ws_col;
*y = ws.ws_row;
} else {
*x = 0;
*y = 0;
}
if (!*x) {
*x = get_e("COLUMNS");
if (!*x) *x = DEFAULT_TERMINAL_WIDTH;
}
if (!*y) {
*y = get_e("LINES");
if (!*y) *y = DEFAULT_TERMINAL_HEIGHT;
}
}
#endif
/* Pipe */
#if defined(CONFIG_OS_UNIX) || defined(CONFIG_OS_BEOS) || defined(CONFIG_OS_RISCOS)
void
set_bin(int fd)
{
}
int
c_pipe(int *fd)
{
return pipe(fd);
}
#elif defined(CONFIG_OS_OS2) || defined(CONFIG_OS_WIN32)
void
set_bin(int fd)
{
setmode(fd, O_BINARY);
}
int
c_pipe(int *fd)
{
int r = pipe(fd);
if (!r) {
set_bin(fd[0]);
set_bin(fd[1]);
}
return r;
}
#endif
/* Exec */
int
is_twterm(void) /* Check if it make sense to call a twterm. */
{
static int tw = -1;
if (tw == -1) tw = !!getenv("TWDISPLAY");
return tw;
}
int
is_gnuscreen(void)
{
static int screen = -1;
if (screen == -1) screen = !!getenv("STY");
return screen;
}
#if defined(CONFIG_OS_UNIX) || defined(CONFIG_OS_WIN32)
int
is_xterm(void)
{
static int xt = -1;
if (xt == -1) {
/* Paraphrased from debian bug 228977:
*
* It is not enough to simply check the DISPLAY env variable,
* as it is pretty legal to have a DISPLAY set. While these
* days this practice is pretty uncommon, it still makes sense
* sometimes, especially for people who prefer the text mode
* for some reason. Only relying on DISPLAY will results in bad
* codes being written to the terminal.
*
* Any real xterm derivative sets WINDOWID as well.
* Unfortunately, konsole is an exception, and it needs to be
* checked for separately.
*
* FIXME: The code below still fails to detect some terminals
* that do support a title (like the popular PuTTY ssh client).
* In general, proper xterm detection is a nightmarish task...
*
* -- Adam Borowski <kilobyte@mimuw.edu.pl> */
unsigned char *display = getenv("DISPLAY");
unsigned char *windowid = getenv("WINDOWID");
if (!windowid || !*windowid)
windowid = getenv("KONSOLE_DCOP_SESSION");
xt = (display && *display && windowid && *windowid);
}
return xt;
}
#endif
unsigned int resize_count = 0;
#ifndef CONFIG_OS_OS2
#if !(defined(CONFIG_OS_BEOS) && defined(HAVE_SETPGID)) && !defined(CONFIG_OS_WIN32)
int
exe(unsigned char *path)
{
return system(path);
}
#endif
static unsigned char *clipboard;
unsigned char *
get_clipboard_text(void)
{
/* The following support for GNU Screen's clipboard is
* disabled for two reasons:
*
* 1. It does not actually return the string from that
* clipboard, but rather causes the clipboard contents to
* appear in stdin. get_clipboard_text is normally called
* because the user pressed a Paste key in an input field,
* so the characters end up being inserted in that field;
* but if there are newlines in the clipboard, then the
* field may lose focus, in which case the remaining
* characters may trigger arbitrary actions in ELinks.
*
* 2. It pastes from both GNU Screen's clipboard and the ELinks
* internal clipboard. Because set_clipboard_text also sets
* them both, the same text would typically get pasted twice.
*
* Users can instead use the GNU Screen key bindings to run the
* paste command. This method still suffers from problem 1 but
* any user of GNU Screen should know that already. */
#if 0
/* GNU Screen's clipboard */
if (is_gnuscreen()) {
struct string str;
if (!init_string(&str)) return NULL;
add_to_string(&str, "screen -X paste .");
if (str.length) exe(str.source);
if (str.source) done_string(&str);
}
#endif
return stracpy(empty_string_or_(clipboard));
}
void
set_clipboard_text(unsigned char *data)
{
/* GNU Screen's clipboard */
if (is_gnuscreen()) {
struct string str;
if (!init_string(&str)) return;
add_to_string(&str, "screen -X register . ");
add_shell_quoted_to_string(&str, data, strlen(data));
if (str.length) exe(str.source);
if (str.source) done_string(&str);
}
/* Shouldn't complain about leaks. */
if (clipboard) free(clipboard);
clipboard = strdup(data);
}
/* Set xterm-like term window's title. */
void
set_window_title(unsigned char *title, int codepage)
{
struct string filtered;
#ifndef HAVE_SYS_CYGWIN_H
/* Check if we're in a xterm-like terminal. */
if (!is_xterm() && !is_gnuscreen()) return;
#endif
if (!init_string(&filtered)) return;
/* Copy title to filtered if different from NULL */
if (title) {
unsigned char *scan = title;
unsigned char *end = title + strlen(title);
/* Remove control characters, so that they cannot
* interfere with the command we send to the terminal.
* However, do not attempt to limit the title length
* to terminal width, because the title is usually
* drawn in a different font anyway. */
/* Note that this is the right place where to do it, since
* potential alternative set_window_title() routines might
* want to take different precautions. */
for (;;) {
unsigned char *charbegin = scan;
unicode_val_T unicode
= cp_to_unicode(codepage, &scan, end);
int charlen = scan - charbegin;
if (unicode == UCS_NO_CHAR)
break;
/* This need not recognize all Unicode control
* characters. Only those that can make the
* terminal misparse the command. */
if (unicode < 0x20
|| (unicode >= 0x7F && unicode < 0xA0))
continue;
/* xterm entirely rejects 1024-byte or longer
* titles. */
if (filtered.length + charlen >= 1024 - 3) {
add_to_string(&filtered, "...");
break;
}
add_bytes_to_string(&filtered, charbegin, charlen);
}
}
/* Send terminal escape sequence + title string */
printf("\033]0;%s\a", filtered.source);
#if 0
/* Miciah don't like this so it is disabled because it changes the
* default window name. --jonas */
/* Set the GNU screen window name */
if (is_gnuscreen())
printf("\033k%s\033\134", filtered.source);
#endif
fflush(stdout);
done_string(&filtered);
}
#ifdef HAVE_X11
static int x_error = 0;
static int
catch_x_error(void)
{
x_error = 1;
return 0;
}
#endif
unsigned char *
get_window_title(void)
{
#ifdef HAVE_X11
/* Following code is stolen from our beloved vim. */
unsigned char *winid;
Display *display;
Window window, root, parent, *children;
XTextProperty text_prop;
Status status;
unsigned int num_children;
unsigned char *ret = NULL;
if (!is_xterm())
return NULL;
winid = getenv("WINDOWID");
if (!winid)
return NULL;
window = (Window) atol(winid);
if (!window)
return NULL;
display = XOpenDisplay(NULL);
if (!display)
return NULL;
/* If WINDOWID is bad, we don't want X to abort us. */
x_error = 0;
XSetErrorHandler((int (*)(Display *, XErrorEvent *)) catch_x_error);
status = XGetWMName(display, window, &text_prop);
/* status = XGetWMIconName(x11_display, x11_window, &text_prop); */
while (!x_error && (!status || !text_prop.value)) {
if (!XQueryTree(display, window, &root, &parent, &children, &num_children))
break;
if (children)
XFree((void *) children);
if (parent == root || parent == 0)
break;
window = parent;
status = XGetWMName(display, window, &text_prop);
}
if (!x_error && status && text_prop.value) {
ret = stracpy(text_prop.value);
XFree(text_prop.value);
}
XCloseDisplay(display);
return ret;
#else
/* At least reset the window title to a blank one. */
return stracpy("");
#endif
}
int
resize_window(int width, int height, int old_width, int old_height)
{
#ifdef HAVE_X11
/* Following code is stolen from our beloved vim. */
unsigned char *winid;
Display *display;
Window window;
Status status;
XWindowAttributes attributes;
if (!is_xterm())
return -1;
winid = getenv("WINDOWID");
if (!winid)
return -1;
window = (Window) atol(winid);
if (!window)
return -1;
display = XOpenDisplay(NULL);
if (!display)
return -1;
/* If WINDOWID is bad, we don't want X to abort us. */
x_error = 0;
XSetErrorHandler((int (*)(Display *, XErrorEvent *)) catch_x_error);
status = XGetWindowAttributes(display, window, &attributes);
while (!x_error && !status) {
Window root, parent, *children;
unsigned int num_children;
if (!XQueryTree(display, window, &root, &parent, &children, &num_children))
break;
if (children)
XFree((void *) children);
if (parent == root || parent == 0)
break;
window = parent;
status = XGetWindowAttributes(display, window, &attributes);
}
if (!x_error && status) {
XSizeHints *size_hints;
long mask;
int px_width = 0;
int px_height = 0;
/* With xterm 210, a window with 80x24 characters at
* a 6x13 font appears to have 484x316 pixels; both
* the width and height include four extra pixels.
* Computing a new size by scaling these values often
* results in windows that cannot display as many
* characters as was intended. We can do better if we
* can find out the actual size of character cells.
* If the terminal emulator has set a window size
* increment, assume that is the cell size. */
size_hints = XAllocSizeHints();
if (size_hints != NULL
&& XGetWMNormalHints(display, window, size_hints, &mask)
&& (mask & PResizeInc) != 0) {
px_width = attributes.width
+ (width - old_width) * size_hints->width_inc;
px_height = attributes.height
+ (height - old_height) * size_hints->height_inc;
}
if (px_width <= 0 || px_height <= 0) {
double ratio_width = (double) attributes.width / old_width;
double ratio_height = (double) attributes.height / old_height;
px_width = (int) ((double) width * ratio_width);
px_height = (int) ((double) height * ratio_height);
}
if (size_hints)
XFree(size_hints);
status = XResizeWindow(display, window, px_width, px_height);
while (!x_error && !status) {
Window root, parent, *children;
unsigned int num_children;
if (!XQueryTree(display, window, &root, &parent, &children, &num_children))
break;
if (children)
XFree((void *) children);
if (parent == root || parent == 0)
break;
window = parent;
status = XResizeWindow(display, window, px_width, px_height);
}
}
XCloseDisplay(display);
return 0;
#else
return -1;
#endif
}
#endif
/* Threads */
#if defined(HAVE_BEGINTHREAD) || defined(CONFIG_OS_BEOS)
struct tdata {
void (*fn)(void *, int);
int h;
unsigned char data[1];
};
void
bgt(struct tdata *t)
{
#ifdef SIGPIPE
signal(SIGPIPE, SIG_IGN);
#endif
t->fn(t->data, t->h);
write(t->h, "x", 1);
close(t->h);
free(t);
}
#else
int
start_thread(void (*fn)(void *, int), void *ptr, int l)
{
int p[2];
pid_t pid;
if (c_pipe(p) < 0) return -1;
if (set_nonblocking_fd(p[0]) < 0) return -1;
if (set_nonblocking_fd(p[1]) < 0) return -1;
pid = fork();
if (!pid) {
struct terminal *term;
/* Close input in this thread; otherwise, if it will live
* longer than its parent, it'll block the terminal until it'll
* quit as well; this way it will hopefully just die unseen and
* in background, causing no trouble. */
/* Particularly, when async dns resolving was in progress and
* someone quitted ELinks, it could make a delay before the
* terminal would be really freed and returned to shell. */
foreach (term, terminals)
if (term->fdin > 0)
close(term->fdin);
close(p[0]);
fn(ptr, p[1]);
write(p[1], "x", 1);
close(p[1]);
/* We use _exit() here instead of exit(), see
* http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC6 for
* reasons. Fixed by Sven Neumann <sven@convergence.de>. */
_exit(0);
}
if (pid == -1) {
close(p[0]);
close(p[1]);
return -1;
}
close(p[1]);
return p[0];
}
#endif
#ifndef OS2_MOUSE
void
want_draw(void)
{
}
void
done_draw(void)
{
}
#endif
#if !defined(CONFIG_OS_WIN32)
int
get_output_handle(void)
{
return 1;
}
int
get_ctl_handle(void)
{
static int fd = -1;
if (isatty(0)) return 0;
if (fd < 0) fd = open("/dev/tty", O_RDONLY);
return fd;
}
#endif
#if !defined(CONFIG_OS_BEOS) && !(defined(HAVE_BEGINTHREAD) && defined(HAVE_READ_KBD)) \
&& !defined(CONFIG_OS_WIN32)
int
get_input_handle(void)
{
return get_ctl_handle();
}
#endif
#ifndef CONFIG_OS_WIN32
void
init_osdep(void)
{
#ifdef HAVE_LOCALE_H
setlocale(LC_ALL, "");
#endif
}
#endif
#if defined(CONFIG_OS_UNIX) || defined(CONFIG_OS_OS2) || defined(CONFIG_OS_RISCOS)
void
terminate_osdep(void)
{
}
#endif
#ifndef CONFIG_OS_BEOS
void
block_stdin(void)
{
}
void
unblock_stdin(void)
{
}
#endif
void
elinks_cfmakeraw(struct termios *t)
{
/* This elinks_cfmakeraw() intentionally leaves the following
* settings unchanged, even though the standard cfmakeraw()
* would change some of them:
*
* - c_cflag & CSIZE: number of bits per character.
* Bug 54 asked ELinks not to change this.
* - c_cflag & (PARENB | PARODD): parity bit in characters.
* Bug 54 asked ELinks not to change this.
* - c_iflag & (IXON | IXOFF | IXANY): XON/XOFF flow control.
*
* The reasoning is, if the user has set up unusual values for
* those settings before starting ELinks, then the terminal
* probably expects those values and ELinks should not mess
* with them. */
t->c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL);
t->c_oflag &= ~OPOST;
t->c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
t->c_cc[VMIN] = 1;
t->c_cc[VTIME] = 0;
}
#if !defined(CONFIG_MOUSE) || (!defined(CONFIG_GPM) && !defined(CONFIG_SYSMOUSE) && !defined(OS2_MOUSE))
void *
handle_mouse(int cons, void (*fn)(void *, unsigned char *, int),
void *data)
{
return NULL;
}
void
unhandle_mouse(void *data)
{
}
void
suspend_mouse(void *data)
{
}
void
resume_mouse(void *data)
{
}
#endif
#ifndef CONFIG_OS_WIN32
/* Create a bitmask consisting from system-independent envirnoment modifiers.
* This is then complemented by system-specific modifiers in an appropriate
* get_system_env() routine. */
static int
get_common_env(void)
{
int env = 0;
if (is_xterm()) env |= ENV_XWIN;
if (is_twterm()) env |= ENV_TWIN;
if (is_gnuscreen()) env |= ENV_SCREEN;
/* ENV_CONSOLE is always set now and indicates that we are working w/ a
* displaying-capable character-adressed terminal. Sounds purely
* theoretically now, but it already makes some things easier and it
* could give us interesting opportunities later (after graphical
* frontends will be introduced, when working in some mysterious daemon
* mode or who knows what ;). --pasky */
env |= ENV_CONSOLE;
return env;
}
#endif
#if defined(CONFIG_OS_UNIX) || defined(CONFIG_OS_RISCOS)
int
get_system_env(void)
{
return get_common_env();
}
#endif
int
can_resize_window(int environment)
{
return !!(environment & (ENV_OS2VIO | ENV_XWIN));
}
#ifndef CONFIG_OS_OS2
int
can_open_os_shell(int environment)
{
return 1;
}
void
set_highpri(void)
{
}
#endif
unsigned char *
get_system_str(int xwin)
{
return xwin ? SYSTEM_STR "-xwin" : SYSTEM_STR;
}