pax_global_header00006660000000000000000000000064132016656730014523gustar00rootroot0000000000000052 comment=d3ebef3411256d9b616de0993f508208349329fb pangoterm-0~bzr607/000077500000000000000000000000001320166567300143715ustar00rootroot00000000000000pangoterm-0~bzr607/.bzrignore000066400000000000000000000001261320166567300163720ustar00rootroot00000000000000.libs *.lo *.la pangoterm t/test t/suites.h t/externs.h t/harness src/encoding/*.inc pangoterm-0~bzr607/LICENSE000066400000000000000000000021121320166567300153720ustar00rootroot00000000000000 The MIT License Copyright (c) 2008 Paul Evans Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pangoterm-0~bzr607/Makefile000066400000000000000000000042011320166567300160260ustar00rootroot00000000000000ifeq ($(shell uname),Darwin) LIBTOOL ?= glibtool else LIBTOOL ?= libtool endif ifneq ($(VERBOSE),1) LIBTOOL +=--quiet endif CFLAGS +=-Wall -Iinclude -std=c99 -DGDK_DISABLE_DEPRECATED -DGTK_DISABLE_DEPRECATED -DGSEAL_ENABLE -DGTK_DISABLE_SINGLE_INCLUDES LDFLAGS +=-lutil ifeq ($(DEBUG),1) CFLAGS +=-ggdb -DDEBUG endif ifeq ($(PROFILE),1) CFLAGS +=-pg LDFLAGS+=-pg endif CFLAGS +=$(shell pkg-config --cflags vterm) LDFLAGS +=$(shell pkg-config --libs vterm) CFLAGS +=$(shell pkg-config --cflags gtk+-2.0) LDFLAGS +=$(shell pkg-config --libs gtk+-2.0) CFLAGS +=$(shell pkg-config --cflags cairo) LDFLAGS +=$(shell pkg-config --libs cairo) PREFIX=/usr/local BINDIR=$(PREFIX)/bin SHAREDIR=$(PREFIX)/share CFLAGS+=-DPANGOTERM_SHAREDIR="\"$(SHAREDIR)\"" HFILES=$(wildcard *.h) CFILES=$(wildcard *.c) OBJECTS=$(CFILES:.c=.lo) all: pangoterm pangoterm: $(OBJECTS) @echo LINK $@ @$(LIBTOOL) --mode=link --tag=CC $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) %.lo: %.c $(HFILES) @echo CC $< @$(LIBTOOL) --mode=compile --tag=CC $(CC) $(CFLAGS) -o $@ -c $< .PHONY: clean clean: $(LIBTOOL) --mode=clean rm -f $(OBJECTS) $(LIBTOOL) --mode=clean rm -f pangoterm .PHONY: install install: install-bin install-share # rm the old binary first in case it's still in use install-bin: pangoterm install -d $(DESTDIR)$(BINDIR) $(LIBTOOL) --mode=install cp --remove-destination pangoterm $(DESTDIR)$(BINDIR)/pangoterm install-share: install -d $(DESTDIR)$(SHAREDIR)/pixmaps $(LIBTOOL) --mode=install cp pangoterm.svg $(DESTDIR)$(SHAREDIR)/pixmaps/ install -d $(DESTDIR)$(SHAREDIR)/applications $(LIBTOOL) --mode=install cp pangoterm.desktop $(DESTDIR)$(SHAREDIR)/applications/ # DIST CUT VERSION=0 DISTDIR=pangoterm-$(VERSION) distdir: mkdir __distdir cp LICENSE __distdir cp *.c *.h __distdir cp pangoterm.svg pangoterm.desktop pangoterm.cfg __distdir sed "/^# DIST CUT/Q" __distdir/Makefile mv __distdir $(DISTDIR) TARBALL=$(DISTDIR).tar.gz dist: distdir tar -czf $(TARBALL) $(DISTDIR) rm -rf $(DISTDIR) dist+bzr: $(MAKE) dist VERSION=$(VERSION)+bzr`bzr revno` distdir+bzr: $(MAKE) distdir VERSION=$(VERSION)+bzr`bzr revno` pangoterm-0~bzr607/conf.c000066400000000000000000000242701320166567300154670ustar00rootroot00000000000000#include "conf.h" #include #include #include #include #include #include #include #define streq(a,b) (strcmp(a,b) == 0) ConfigEntry *configs = NULL; static char *profile = NULL; enum { SYMBOL_TRUE = G_TOKEN_LAST + 1, SYMBOL_FALSE, }; static int conf_from_file(const char *path) { int fd = open(path, O_RDONLY); if(!fd) { fprintf(stderr, "Cannot open configuration file %s: %s\n", path, strerror(errno)); return 0; } GScanner *scanner = g_scanner_new(NULL); /* Don't skip linefeeds */ scanner->config->cset_skip_characters = " \t"; /* Identifier needs to include * and - */ scanner->config->cset_identifier_first = G_CSET_A_2_Z G_CSET_a_2_z "_*"; scanner->config->cset_identifier_nth = G_CSET_A_2_Z G_CSET_a_2_z "_-*"; g_scanner_scope_add_symbol(scanner, 0, "true", GINT_TO_POINTER(SYMBOL_TRUE)); g_scanner_scope_add_symbol(scanner, 0, "false", GINT_TO_POINTER(SYMBOL_FALSE)); scanner->config->symbol_2_token = TRUE; scanner->input_name = g_strdup(path); g_scanner_input_file(scanner, fd); int matching_profile = 1; GTokenType t; while((t = g_scanner_get_next_token(scanner)) != G_TOKEN_EOF) { if(t == '\n') // Skip linefeeds here continue; if(t == G_TOKEN_IDENTIFIER) { if(!matching_profile) { // Skip tokens until linefeed while(g_scanner_get_next_token(scanner) != '\n') ; continue; } char *name = scanner->value.v_identifier; ConfigEntry *cfg = NULL; for(ConfigEntry *p = configs; p; p = p->next) { if(strcmp(name, p->longname) != 0) continue; cfg = p; break; } if(!cfg) { g_scanner_error(scanner, "\"%s\" is not a recognised setting name", name); goto abort; } int key; if(cfg->is_parametric) { if(g_scanner_get_next_token(scanner) != ':') { g_scanner_error(scanner, "Expected ':'"); goto abort; } if(g_scanner_get_next_token(scanner) != G_TOKEN_INT) { g_scanner_error(scanner, "Expected integer parameter index"); goto abort; } key = scanner->value.v_int; } if(g_scanner_get_next_token(scanner) != G_TOKEN_EQUAL_SIGN) { g_scanner_error(scanner, "Expected '='"); goto abort; } t = g_scanner_get_next_token(scanner); switch(cfg->type) { case CONF_TYPE_STRING: if(t != G_TOKEN_STRING) { g_scanner_error(scanner, "Expected \"%s\" to take a string value", cfg->longname); goto abort; } if(cfg->is_parametric) { ConfigValue value = { .s = scanner->value.v_string }; (*cfg->apply)(key, value); } else { if(cfg->from_file.s) g_free(cfg->from_file.s); cfg->from_file.s = g_strdup(scanner->value.v_string); cfg->var_set_from_file = TRUE; } break; case CONF_TYPE_INT: if(t != G_TOKEN_INT) { g_scanner_error(scanner, "Expected \"%s\" to take an integer value", cfg->longname); goto abort; } cfg->from_file.i = scanner->value.v_int; cfg->var_set_from_file = TRUE; break; case CONF_TYPE_DOUBLE: if(t == G_TOKEN_INT) { t = G_TOKEN_FLOAT; scanner->value.v_float = scanner->value.v_int; } if(t != G_TOKEN_FLOAT) { g_scanner_error(scanner, "Expected \"%s\" to take a float value", cfg->longname); goto abort; } cfg->from_file.d = scanner->value.v_float; cfg->var_set_from_file = TRUE; break; case CONF_TYPE_BOOL: if(t != (GTokenType)SYMBOL_TRUE && t != (GTokenType)SYMBOL_FALSE) { g_scanner_error(scanner, "Expected \"%s\" to take a boolean value", cfg->longname); goto abort; } cfg->from_file.i = (t == (GTokenType)SYMBOL_TRUE); cfg->var_set_from_file = TRUE; break; } if(g_scanner_get_next_token(scanner) != '\n') { g_scanner_error(scanner, "Expected EOL"); goto abort; } } else if(t == '[') { t = g_scanner_get_next_token(scanner); if(t != G_TOKEN_IDENTIFIER || strcmp(scanner->value.v_identifier, "Profile")) { g_scanner_error(scanner, "Expected 'Profile'"); goto abort; } if(g_scanner_get_next_token(scanner) != G_TOKEN_IDENTIFIER) { g_scanner_error(scanner, "Expected profile name"); goto abort; } matching_profile = profile && g_pattern_match_simple(scanner->value.v_identifier, profile); if(g_scanner_get_next_token(scanner) != ']') { g_scanner_error(scanner, "Expected ']'"); goto abort; } if(g_scanner_get_next_token(scanner) != '\n') { g_scanner_error(scanner, "Expected EOL"); goto abort; } } else { g_scanner_error(scanner, "Expected a setting name"); goto abort; } } g_scanner_destroy(scanner); close(fd); /* Config parse successful - copy to vars */ for(ConfigEntry *p = configs; p; p = p->next) { if(p->var_set || !p->var_set_from_file) continue; switch(p->type) { case CONF_TYPE_STRING: *(char**)p->var = p->from_file.s; break; case CONF_TYPE_INT: *(int*)p->var = p->from_file.i; break; case CONF_TYPE_DOUBLE: *(double*)p->var = p->from_file.d; break; case CONF_TYPE_BOOL: *(int*)p->var = p->from_file.i; break; } p->var_set = TRUE; } return 1; abort: g_scanner_destroy(scanner); close(fd); return 0; } int conf_parse(int *argcp, char ***argvp) { int n_entries = 0; for(ConfigEntry *p = configs; p; p = p->next) n_entries += (p->type == CONF_TYPE_BOOL) ? 2 : 1; GOptionEntry *option_entries = g_malloc0(sizeof(GOptionEntry) * (n_entries + 3)); char *config_file = NULL; option_entries[0].long_name = "config-file"; option_entries[0].short_name = 0; option_entries[0].flags = 0; option_entries[0].arg = G_OPTION_ARG_FILENAME; option_entries[0].arg_data = &config_file; option_entries[0].description = "Path to config file"; option_entries[0].arg_description = "PATH"; option_entries[1].long_name = "profile"; option_entries[1].short_name = 'p'; option_entries[1].flags = 0; option_entries[1].arg = G_OPTION_ARG_STRING; option_entries[1].arg_data = &profile; option_entries[1].description = "Profile name"; option_entries[1].arg_description = "PROFILE"; int i = 2; for(ConfigEntry *cfg = configs; cfg; cfg = cfg->next) { if(cfg->is_parametric) continue; char *longname = g_strdup(cfg->longname); /* Convert foo_bar to foo-bar; easier on commandline */ for(char *s = longname; s[0]; s++) if(s[0] == '_') s[0] = '-'; option_entries[i].long_name = longname; option_entries[i].short_name = cfg->shortname; option_entries[i].flags = 0; option_entries[i].arg_data = cfg->var; option_entries[i].description = cfg->desc; option_entries[i].arg_description = cfg->argdesc; switch(cfg->type) { case CONF_TYPE_STRING: option_entries[i].arg = G_OPTION_ARG_STRING; break; case CONF_TYPE_INT: option_entries[i].arg = G_OPTION_ARG_INT; break; case CONF_TYPE_DOUBLE: option_entries[i].arg = G_OPTION_ARG_DOUBLE; break; case CONF_TYPE_BOOL: option_entries[i].arg = G_OPTION_ARG_NONE; i++; option_entries[i].long_name = g_strdup_printf("no-%s", longname); option_entries[i].short_name = 0; option_entries[i].arg = G_OPTION_ARG_NONE; option_entries[i].arg_data = &cfg->var_set; option_entries[i].description = g_strdup_printf("Disable %s", cfg->desc); break; } i++; } option_entries[i].long_name = NULL; GError *args_error = NULL; GOptionContext *args_context; args_context = g_option_context_new("commandline..."); g_option_context_add_main_entries(args_context, option_entries, NULL); g_option_context_add_group(args_context, gtk_get_option_group(TRUE)); /* Convert -e to -- so as to treat a -e option as a commandline vector * to run, and satisfy the x-terminal-emulator spec */ for(i = 0; i < *argcp; i++) if(streq((*argvp)[i], "-e")) { (*argvp)[i] = "--"; break; } if(!g_option_context_parse(args_context, argcp, argvp, &args_error)) { fprintf(stderr, "Option parsing failed: %s\n", args_error->message); return 0; } for(i = 2; i < n_entries + 1; i++) g_free((void*)option_entries[i].long_name); free(option_entries); /* g_option doesn't give us any way to tell if variables were set or not; * this is the best we can do */ for(ConfigEntry *cfg = configs; cfg; cfg = cfg->next) { if(cfg->is_parametric) continue; switch(cfg->type) { case CONF_TYPE_STRING: cfg->var_set = *(void**)cfg->var != NULL; break; case CONF_TYPE_INT: cfg->var_set = *(int*)cfg->var != -1; break; case CONF_TYPE_DOUBLE: cfg->var_set = *(double*)cfg->var != -1.0; break; case CONF_TYPE_BOOL: if(*(int*)cfg->var) cfg->var_set = 1; break; } } if(config_file) { if(!conf_from_file(config_file)) return 0; } else { config_file = g_strdup_printf("%s/pangoterm.cfg", g_get_user_config_dir()); struct stat st; if(stat(config_file, &st) == 0) if(!conf_from_file(config_file)) return 0; g_free(config_file); } for(ConfigEntry *cfg = configs; cfg; cfg = cfg->next) { if(cfg->is_parametric) continue; switch(cfg->type) { case CONF_TYPE_STRING: if(!cfg->var_set) *(char**)cfg->var = cfg->dflt.s; break; case CONF_TYPE_INT: if(!cfg->var_set) *(int*)cfg->var = cfg->dflt.i; break; case CONF_TYPE_DOUBLE: if(!cfg->var_set) *(double*)cfg->var = cfg->dflt.d; break; case CONF_TYPE_BOOL: if(!cfg->var_set) *(int*)cfg->var = cfg->dflt.i; } } return 1; } pangoterm-0~bzr607/conf.h000066400000000000000000000126441320166567300154760ustar00rootroot00000000000000#ifndef __CONF_H__ #define __CONF_H__ #include typedef enum { CONF_TYPE_STRING, CONF_TYPE_INT, CONF_TYPE_DOUBLE, CONF_TYPE_BOOL, } ConfigType; typedef union { char *s; int i; double d; } ConfigValue; typedef struct ConfigEntry ConfigEntry; struct ConfigEntry { ConfigEntry *next; const char *longname; char shortname; ConfigType type; bool is_parametric; union { void *var; /* ptr to char* or int or double */ void (*apply)(int key, ConfigValue value); }; int var_set; const char *desc; const char *argdesc; ConfigValue from_file; int var_set_from_file; ConfigValue dflt; }; extern ConfigEntry *configs; #define CONF_STRING(name,shortname_,dflt_,desc_,argdesc_) \ static char *CONF_##name; \ static void __attribute__((constructor)) DECLARE_##name(void) { \ static ConfigEntry config = { \ .longname = #name, \ .shortname = shortname_, \ .type = CONF_TYPE_STRING, \ .var = &CONF_##name, \ .var_set = FALSE, \ .desc = desc_, \ .argdesc = argdesc_, \ .dflt.s = dflt_, \ }; \ config.next = configs; \ configs = &config; \ } #define CONF_PARAMETRIC_STRING(name,shortname_,func,desc_,argdesc_) \ static void __attribute__((constructor)) DECLARE_##name(void) { \ static ConfigEntry config = { \ .longname = #name, \ .shortname = shortname_, \ .type = CONF_TYPE_STRING, \ .is_parametric = true, \ .apply = func, \ .desc = desc_, \ .argdesc = argdesc_, \ }; \ config.next = configs; \ configs = &config; \ } #define CONF_INT(name,shortname_,dflt_,desc_,argdesc_) \ static int CONF_##name = -1; \ static void __attribute__((constructor)) DECLARE_##name(void) { \ static ConfigEntry config = { \ .longname = #name, \ .shortname = shortname_, \ .type = CONF_TYPE_INT, \ .var = &CONF_##name, \ .var_set = FALSE, \ .desc = desc_, \ .argdesc = argdesc_, \ .dflt.i = dflt_, \ }; \ config.next = configs; \ configs = &config; \ } #define CONF_DOUBLE(name,shortname_,dflt_,desc_,argdesc_) \ static double CONF_##name = -1.0; \ static void __attribute__((constructor)) DECLARE_##name(void) { \ static ConfigEntry config = { \ .longname = #name, \ .shortname = shortname_, \ .type = CONF_TYPE_DOUBLE, \ .var = &CONF_##name, \ .var_set = FALSE, \ .desc = desc_, \ .argdesc = argdesc_, \ .dflt.d = dflt_, \ }; \ config.next = configs; \ configs = &config; \ } #define CONF_BOOL(name,shortname_,dflt_,desc_) \ static int CONF_##name = FALSE; \ static void __attribute__((constructor)) DECLARE_##name(void) { \ static ConfigEntry config = { \ .longname = #name, \ .shortname = shortname_, \ .type = CONF_TYPE_BOOL, \ .var = &CONF_##name, \ .var_set = FALSE, \ .desc = desc_, \ .argdesc = NULL, \ .dflt.i = dflt_, \ }; \ config.next = configs; \ configs = &config; \ } int conf_parse(int *argcp, char ***argvp); #endif pangoterm-0~bzr607/main.c000066400000000000000000000132001320166567300154550ustar00rootroot00000000000000/* for putenv() */ #define _XOPEN_SOURCE /* for ECHOCTL, ECHOKE, cfsetspeed() */ #if defined(__NetBSD__) # define _NETBSD_SOURCE #endif #define _DEFAULT_SOURCE /* _BSD_SOURCE is deprecated, replaced with _DEFAULT_SOURCE. */ #define _BSD_SOURCE #include #include #include #include #include /* suck up the non-standard openpty/forkpty */ #if defined(__FreeBSD__) # include # include # include #elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) # include # include # include #else # include #endif #include #include "pangoterm.h" #include "conf.h" CONF_STRING(font, 0, "DejaVu Sans Mono", "Font name", "STR"); CONF_STRING(title, 'T', "pangoterm", "Title", "STR"); CONF_INT(lines, 0, 25, "Number of lines", "NUM"); CONF_INT(cols, 0, 80, "Number of columns", "NUM"); CONF_STRING(term, 0, "xterm", "Terminal type", "STR"); static char *alt_fonts[] = { "Courier 10 Pitch", NULL }; static int master; static size_t write_master(const char *bytes, size_t len, void *user) { return write(master, bytes, len); } static void resized(int rows, int cols, void *user) { struct winsize size = { rows, cols, 0, 0 }; ioctl(master, TIOCSWINSZ, &size); } static gboolean master_readable(GIOChannel *source, GIOCondition cond, gpointer user_data) { PangoTerm *pt = user_data; pangoterm_begin_update(pt); /* Make sure we don't take longer than 20msec doing this */ guint64 deadline_time = g_get_real_time() + 20*1000; while(1) { /* Linux kernel's PTY buffer is a fixed 4096 bytes (1 page) so there's * never any point read()ing more than that */ char buffer[4096]; ssize_t bytes = read(master, buffer, sizeof buffer); if(bytes == -1 && errno == EAGAIN) break; if(bytes == 0 || (bytes == -1 && errno == EIO)) { gtk_main_quit(); return FALSE; } if(bytes < 0) { fprintf(stderr, "read(master) failed - %s\n", strerror(errno)); exit(1); } #ifdef DEBUG_PRINT_INPUT printf("Read %zd bytes from master:\n", bytes); int i; for(i = 0; i < bytes; i++) { printf(i % 16 == 0 ? " | %02x" : " %02x", buffer[i]); if(i % 16 == 15) printf("\n"); } if(i % 16) printf("\n"); #endif pangoterm_push_bytes(pt, buffer, bytes); if(g_get_real_time() >= deadline_time) break; } pangoterm_end_update(pt); return TRUE; } int main(int argc, char *argv[]) { if(!conf_parse(&argc, &argv)) exit(1); // GLib has consumed the options, but it might leave a -- in place in argv[1] if(argc > 1 && strcmp(argv[1], "--") == 0) { argv++; argc--; } gtk_init(&argc, &argv); setlocale(LC_CTYPE, NULL); PangoTerm *pt = pangoterm_new(CONF_lines, CONF_cols); pangoterm_set_fonts(pt, CONF_font, alt_fonts); pangoterm_set_title(pt, CONF_title); /* None of the docs about termios explain how to construct a new one of * these, so this is largely a guess */ struct termios termios = { .c_iflag = ICRNL|IXON, .c_oflag = OPOST|ONLCR #ifdef TAB0 |TAB0 #endif , .c_cflag = CS8|CREAD, .c_lflag = ISIG|ICANON|IEXTEN|ECHO|ECHOE|ECHOK, /* c_cc later */ }; #ifdef IUTF8 termios.c_iflag |= IUTF8; #endif #ifdef NL0 termios.c_oflag |= NL0; #endif #ifdef CR0 termios.c_oflag |= CR0; #endif #ifdef BS0 termios.c_oflag |= BS0; #endif #ifdef VT0 termios.c_oflag |= VT0; #endif #ifdef FF0 termios.c_oflag |= FF0; #endif #ifdef ECHOCTL termios.c_lflag |= ECHOCTL; #endif #ifdef ECHOKE termios.c_lflag |= ECHOKE; #endif cfsetspeed(&termios, 38400); termios.c_cc[VINTR] = 0x1f & 'C'; termios.c_cc[VQUIT] = 0x1f & '\\'; termios.c_cc[VERASE] = 0x7f; termios.c_cc[VKILL] = 0x1f & 'U'; termios.c_cc[VEOF] = 0x1f & 'D'; termios.c_cc[VEOL] = _POSIX_VDISABLE; termios.c_cc[VEOL2] = _POSIX_VDISABLE; termios.c_cc[VSTART] = 0x1f & 'Q'; termios.c_cc[VSTOP] = 0x1f & 'S'; termios.c_cc[VSUSP] = 0x1f & 'Z'; termios.c_cc[VREPRINT] = 0x1f & 'R'; termios.c_cc[VWERASE] = 0x1f & 'W'; termios.c_cc[VLNEXT] = 0x1f & 'V'; termios.c_cc[VMIN] = 1; termios.c_cc[VTIME] = 0; struct winsize size = { CONF_lines, CONF_cols, 0, 0 }; /* Save the real stderr before forkpty so we can still print errors to it if * we fail */ int stderr_save_fileno = dup(2); pid_t kid = forkpty(&master, NULL, &termios, &size); if(kid == 0) { fcntl(stderr_save_fileno, F_SETFD, fcntl(stderr_save_fileno, F_GETFD) | FD_CLOEXEC); FILE *stderr_save = fdopen(stderr_save_fileno, "a"); /* Restore the ISIG signals back to defaults */ signal(SIGINT, SIG_DFL); signal(SIGQUIT, SIG_DFL); signal(SIGSTOP, SIG_DFL); signal(SIGCONT, SIG_DFL); gchar *term = g_strdup_printf("TERM=%s", CONF_term); putenv(term); /* Do not free 'term', it is part of the environment */ if(argc > 1) { execvp(argv[1], argv + 1); fprintf(stderr_save, "Cannot exec(%s) - %s\n", argv[1], strerror(errno)); } else { char *shell = getenv("SHELL"); char *args[2] = { shell, NULL }; execvp(shell, args); fprintf(stderr_save, "Cannot exec(%s) - %s\n", shell, strerror(errno)); } _exit(1); } close(stderr_save_fileno); fcntl(master, F_SETFL, fcntl(master, F_GETFL) | O_NONBLOCK); GIOChannel *gio_master = g_io_channel_unix_new(master); g_io_add_watch(gio_master, G_IO_IN|G_IO_HUP, master_readable, pt); pangoterm_set_write_fn(pt, &write_master, NULL); pangoterm_set_resized_fn(pt, &resized, NULL); pangoterm_start(pt); gtk_main(); pangoterm_free(pt); return 0; } pangoterm-0~bzr607/pangoterm.c000066400000000000000000001645011320166567300165400ustar00rootroot00000000000000#include "pangoterm.h" #include // memmove #include #include #include #include #include #include "conf.h" CONF_STRING(foreground, 0, "gray90", "Foreground colour", "COL"); CONF_STRING(background, 0, "black", "Background colour", "COL"); CONF_STRING(cursor, 0, "white", "Cursor colour", "COL"); CONF_INT(border, 0, 2, "Border width", "PIXELS"); static struct { GdkColor col; gboolean is_set; } colours[16]; static void apply_colour(int index, ConfigValue v) { if(index < 0 || index > 16) return; colours[index].col = (GdkColor){ 0, 0, 0 }; gdk_color_parse(v.s, &colours[index].col); colours[index].is_set = true; } CONF_PARAMETRIC_STRING(colour, 0, apply_colour, "Palette colour", "COL"); CONF_INT(cursor_shape, 0, 1, "Cursor shape (1=block 2=underbar 3=vertical bar)", "SHAPE"); CONF_DOUBLE(size, 's', 9.0, "Font size", "NUM"); CONF_INT(cursor_blink_interval, 0, 500, "Cursor blink interval", "MSEC"); CONF_BOOL(bold_highbright, 0, TRUE, "Bold is high-brightness"); CONF_BOOL(altscreen, 0, TRUE, "Alternate screen buffer switching"); CONF_BOOL(altscreen_scroll, 0, FALSE, "Emulate arrows for mouse scrolling in alternate screen buffer"); CONF_INT(scrollback_size, 0, 1000, "Scrollback size", "LINES"); CONF_INT(scrollbar_width, 0, 3, "Scroll bar width", "PIXELS"); CONF_INT(scroll_wheel_delta, 0, 3, "Number of lines to scroll on mouse wheel", "LINES"); CONF_BOOL(unscroll_on_output, 0, TRUE, "Scroll to bottom on output"); CONF_BOOL(unscroll_on_key, 0, TRUE, "Scroll to bottom on keypress"); CONF_BOOL(doubleclick_fullword, 0, FALSE, "Double-click selects fullwords (until whitespace)"); CONF_STRING(geometry, 0, "", "Initial window geometry", "GEOM"); CONF_BOOL(chord_shift_space, 0, TRUE, "Shift-Space chording"); CONF_BOOL(chord_shift_backspace, 0, TRUE, "Shift-Backspace chording"); CONF_BOOL(chord_shift_enter, 0, TRUE, "Shift-Enter chording"); #define VTERM_COLOR_FROM_GDK_COLOR(c) \ ((VTermColor){ .red = (c).red / 257, .green = (c).green / 257, .blue = (c).blue / 257 }) #define GDK_COLOR_FROM_VTERM_COLOR(c) \ ((GdkColor){ .red = 257 * (c).red, .green = 257 * (c).green, .blue = 257 * (c).blue }) #ifdef DEBUG # define DEBUG_PRINT_INPUT #endif /* To accomodate scrollback scrolling, we'll adopt the convention that VTermPos * and VTermRect instances always refer to virtual locations within the * VTermScreen buffer (or our scrollback buffer if row is negative), and * PhyPos and PhyRect instances refer to physical onscreen positions */ typedef struct { int prow, pcol; } PhyPos; #define PHYSPOS_FROM_VTERMPOS(pt, pos) \ { \ .prow = pos.row + pt->scroll_offs, \ .pcol = pos.col, \ } #define VTERMPOS_FROM_PHYSPOS(pt, pos) \ { \ .row = pos.prow - pt->scroll_offs, \ .col = pos.pcol, \ } typedef struct { int start_prow, end_prow, start_pcol, end_pcol; } PhyRect; #define PHYRECT_FROM_VTERMRECT(pt, rect) \ { \ .start_prow = rect.start_row + pt->scroll_offs, \ .end_prow = rect.end_row + pt->scroll_offs, \ .start_pcol = rect.start_col, \ .end_pcol = rect.end_col, \ } typedef struct { int cols; VTermScreenCell cells[]; } PangoTermScrollbackLine; struct PangoTerm { VTerm *vt; VTermScreen *vts; GtkIMContext *im_context; int mousemode; GdkRectangle pending_area; /* Pending glyphs to flush in flush_pending */ GString *glyphs; GArray *glyph_widths; /* Pending area to erase in flush_pending */ int erase_columns; /* Is pending area DWL? */ int pending_dwl; struct { struct { unsigned int bold : 1; unsigned int underline : 2; unsigned int italic : 1; unsigned int reverse : 1; unsigned int strike : 1; unsigned int font : 4; unsigned int dwl : 1; unsigned int dhl : 2; } attrs; GdkColor fg_col; GdkColor bg_col; PangoAttrList *pangoattrs; PangoLayout *layout; } pen; int rows; int cols; int on_altscreen; int scroll_offs; int scroll_size; int scroll_current; PangoTermScrollbackLine **sb_buffer; PangoTermWriteFn *writefn; void *writefn_data; PangoTermResizedFn *resizedfn; void *resizedfn_data; int n_fonts; char **fonts; double font_size; int cell_width_pango; int cell_width; int cell_height; GdkColor fg_col; GdkColor bg_col; int has_focus; int cursor_visible; /* VTERM_PROP_CURSORVISIBLE */ int cursor_blinkstate; /* during high state of blink */ int cursor_hidden_for_redraw; /* true to temporarily hide during redraw */ VTermPos cursorpos; GdkColor cursor_col; int cursor_shape; #define CURSOR_ENABLED(pt) ((pt)->cursor_visible && !(pt)->cursor_hidden_for_redraw) guint cursor_timer_id; GtkWidget *termwin; cairo_surface_t *buffer; GdkWindow *termdraw; /* area in buffer that needs flushing to termdraw */ GdkRectangle dirty_area; /* These four positions relate to the click/drag highlight state */ enum { NO_DRAG, DRAG_PENDING, DRAGGING } dragging; /* Initial mouse position of selection drag */ VTermPos drag_start; /* Current mouse position of selection drag */ VTermPos drag_pos; /* Start and stop bounds of the selection */ bool highlight_valid; VTermPos highlight_start; VTermPos highlight_stop; GtkClipboard *selection_primary; GtkClipboard *selection_clipboard; }; /* * Utility functions */ static VTermKey convert_keyval(guint gdk_keyval, VTermModifier *statep) { if(gdk_keyval >= GDK_KEY_F1 && gdk_keyval <= GDK_KEY_F35) return VTERM_KEY_FUNCTION(gdk_keyval - GDK_KEY_F1 + 1); switch(gdk_keyval) { case GDK_KEY_BackSpace: return VTERM_KEY_BACKSPACE; case GDK_KEY_Tab: case GDK_KEY_KP_Tab: return VTERM_KEY_TAB; case GDK_KEY_Return: return VTERM_KEY_ENTER; case GDK_KEY_Escape: return VTERM_KEY_ESCAPE; case GDK_KEY_Up: return VTERM_KEY_UP; case GDK_KEY_Down: return VTERM_KEY_DOWN; case GDK_KEY_Left: return VTERM_KEY_LEFT; case GDK_KEY_Right: return VTERM_KEY_RIGHT; case GDK_KEY_Insert: return VTERM_KEY_INS; case GDK_KEY_Delete: return VTERM_KEY_DEL; case GDK_KEY_Home: return VTERM_KEY_HOME; case GDK_KEY_End: return VTERM_KEY_END; case GDK_KEY_Page_Up: return VTERM_KEY_PAGEUP; case GDK_KEY_Page_Down: return VTERM_KEY_PAGEDOWN; case GDK_KEY_ISO_Left_Tab: /* This is Shift-Tab */ *statep |= VTERM_MOD_SHIFT; return VTERM_KEY_TAB; case GDK_KEY_KP_Insert: return VTERM_KEY_KP_0; case GDK_KEY_KP_End: return VTERM_KEY_KP_1; case GDK_KEY_KP_Down: return VTERM_KEY_KP_2; case GDK_KEY_KP_Page_Down: return VTERM_KEY_KP_3; case GDK_KEY_KP_Left: return VTERM_KEY_KP_4; case GDK_KEY_KP_Begin: return VTERM_KEY_KP_5; case GDK_KEY_KP_Right: return VTERM_KEY_KP_6; case GDK_KEY_KP_Home: return VTERM_KEY_KP_7; case GDK_KEY_KP_Up: return VTERM_KEY_KP_8; case GDK_KEY_KP_Page_Up: return VTERM_KEY_KP_9; case GDK_KEY_KP_Delete: return VTERM_KEY_KP_PERIOD; case GDK_KEY_KP_Enter: return VTERM_KEY_KP_ENTER; case GDK_KEY_KP_Add: return VTERM_KEY_KP_PLUS; case GDK_KEY_KP_Subtract: return VTERM_KEY_KP_MINUS; case GDK_KEY_KP_Multiply: return VTERM_KEY_KP_MULT; case GDK_KEY_KP_Divide: return VTERM_KEY_KP_DIVIDE; case GDK_KEY_KP_Equal: return VTERM_KEY_KP_EQUAL; default: return VTERM_KEY_NONE; } } static VTermModifier convert_modifier(int state) { VTermModifier mod = VTERM_MOD_NONE; if(state & GDK_SHIFT_MASK) mod |= VTERM_MOD_SHIFT; if(state & GDK_CONTROL_MASK) mod |= VTERM_MOD_CTRL; if(state & GDK_MOD1_MASK) mod |= VTERM_MOD_ALT; return mod; } static void term_flush_output(PangoTerm *pt) { size_t bufflen = vterm_output_get_buffer_current(pt->vt); if(bufflen) { char buffer[bufflen]; bufflen = vterm_output_read(pt->vt, buffer, bufflen); (*pt->writefn)(buffer, bufflen, pt->writefn_data); } } static void term_push_string(PangoTerm *pt, gchar *str, gboolean paste) { if(paste) vterm_keyboard_start_paste(pt->vt); while(str && str[0]) { /* 6 bytes is always enough for any UTF-8 character */ if(vterm_output_get_buffer_remaining(pt->vt) < 6) term_flush_output(pt); vterm_keyboard_unichar(pt->vt, g_utf8_get_char(str), 0); str = g_utf8_next_char(str); } if(paste) vterm_keyboard_end_paste(pt->vt); term_flush_output(pt); } static void pos_next(PangoTerm *pt, VTermPos *pos) { pos->col++; if(pos->col >= pt->cols) { pos->row++; pos->col = 0; } } static void pos_prev(PangoTerm *pt, VTermPos *pos) { pos->col--; if(pos->col < 0) { pos->row--; pos->col = pt->cols - 1; } } static void fetch_cell(PangoTerm *pt, VTermPos pos, VTermScreenCell *cell) { if(pos.row < 0) { if(-pos.row > pt->scroll_current) { fprintf(stderr, "ARGH! Attempt to fetch scrollback beyond buffer at line %d\n", -pos.row); abort(); } /* pos.row == -1 => sb_buffer[0], -2 => [1], etc... */ PangoTermScrollbackLine *sb_line = pt->sb_buffer[-pos.row-1]; if(pos.col < sb_line->cols) *cell = sb_line->cells[pos.col]; else { *cell = (VTermScreenCell) { { 0 } }; cell->width = 1; cell->bg = sb_line->cells[sb_line->cols - 1].bg; } } else { vterm_screen_get_cell(pt->vts, pos, cell); } } static int fetch_is_eol(PangoTerm *pt, VTermPos pos) { if(pos.row >= 0) return vterm_screen_is_eol(pt->vts, pos); PangoTermScrollbackLine *sb_line = pt->sb_buffer[-pos.row-1]; for(int col = pos.col; col < sb_line->cols; ) { if(sb_line->cells[col].chars[0]) return 0; col += sb_line->cells[col].width; } return 1; } static size_t fetch_line_text(PangoTerm *pt, gchar *str, size_t len, VTermRect rect) { size_t ret = 0; int skipped_blank = 0; int end_blank = 0; VTermPos pos = { .row = rect.start_row, .col = rect.start_col, }; while(pos.col < rect.end_col) { VTermScreenCell cell; fetch_cell(pt, pos, &cell); if(!cell.chars[0]) skipped_blank++; else for(; skipped_blank; skipped_blank--) { if(str) str[ret] = 0x20; ret++; } for(int i = 0; cell.chars[i]; i++) ret += g_unichar_to_utf8(cell.chars[i], str ? str + ret : NULL); end_blank = !cell.chars[0]; pos.col += cell.width; } if(end_blank) { if(str) str[ret] = 0x0a; ret++; } return ret; } static gchar *fetch_flow_text(PangoTerm *pt, VTermPos start, VTermPos stop) { size_t strlen = 0; char *str = NULL; // This logic looks so similar each time it's easier to loop it while(1) { size_t thislen = 0; VTermRect rect; if(start.row == stop.row) { rect.start_row = start.row; rect.start_col = start.col; rect.end_row = start.row + 1; rect.end_col = stop.col + 1; thislen += fetch_line_text(pt, str ? str + thislen : NULL, str ? strlen - thislen : 0, rect); } else { rect.start_row = start.row; rect.start_col = start.col; rect.end_row = start.row + 1; rect.end_col = pt->cols; thislen += fetch_line_text(pt, str ? str + thislen : NULL, str ? strlen - thislen : 0, rect); for(int row = start.row + 1; row < stop.row; row++) { rect.start_row = row; rect.start_col = 0; rect.end_row = row + 1; rect.end_col = pt->cols; thislen += fetch_line_text(pt, str ? str + thislen : NULL, str ? strlen - thislen : 0, rect); } rect.start_row = stop.row; rect.start_col = 0; rect.end_row = stop.row + 1; rect.end_col = stop.col + 1; thislen += fetch_line_text(pt, str ? str + thislen : NULL, str ? strlen - thislen : 0, rect); } if(str) break; strlen = thislen; str = malloc(strlen + 1); // Terminating NUL } str[strlen] = 0; return str; } #define GDKRECTANGLE_FROM_PHYRECT(pt, rect) \ { \ .x = rect.start_pcol * pt->cell_width, \ .y = rect.start_prow * pt->cell_height, \ .width = (rect.end_pcol - rect.start_pcol) * pt->cell_width, \ .height = (rect.end_prow - rect.start_prow) * pt->cell_height, \ } #define GDKRECTANGLE_FROM_PHYPOS_CELLS(pt, pos, width_mult) \ { \ .x = pos.pcol * pt->cell_width, \ .y = pos.prow * pt->cell_height, \ .width = pt->cell_width * width_mult, \ .height = pt->cell_height, \ } static int is_wordchar(uint32_t c) { if(CONF_doubleclick_fullword) return c && !iswspace(c); else return iswalnum(c) || (c == '_'); } static void lf_to_cr(gchar *str) { for( ; str[0]; str++) if(str[0] == '\n') str[0] = '\r'; } /* * Repainting operations */ static void blit_buffer(PangoTerm *pt, GdkRectangle *area) { cairo_surface_flush(pt->buffer); cairo_t* gc = gdk_cairo_create(pt->termdraw); gdk_cairo_rectangle(gc, area); cairo_clip(gc); int whole_width = 2 * CONF_border + pt->cols * pt->cell_width; bool scrollbar = (area->x + area->width) > (whole_width - CONF_scrollbar_width); int whole_height; GdkRectangle scrollbar_area; if(scrollbar) { /* Erase old scrollbar */ whole_height = pt->rows * pt->cell_height + 2 * CONF_border; scrollbar_area = (GdkRectangle){ .x = whole_width - CONF_scrollbar_width, .y = 0, .width = CONF_scrollbar_width, .height = whole_height, }; cairo_save(gc); gdk_cairo_rectangle(gc, &scrollbar_area); cairo_clip(gc); cairo_set_source_rgb(gc, pt->bg_col.red / 65535.0, pt->bg_col.green / 65535.0, pt->bg_col.blue / 65535.0); cairo_paint(gc); cairo_restore(gc); } { cairo_save(gc); /* clip rectangle will solve this efficiently */ cairo_set_source_surface(gc, pt->buffer, CONF_border, CONF_border); cairo_paint(gc); cairo_restore(gc); } if(scrollbar && pt->scroll_offs) { /* Map the whole pt->rows + pt->scrollback_current extent onto the entire * height of the window, and draw a brighter rectangle to represent the * part currently visible */ int pixels_from_bottom = (whole_height * pt->scroll_offs) / (pt->rows + pt->scroll_current); int pixels_tall = (whole_height * pt->rows) / (pt->rows + pt->scroll_current); cairo_save(gc); gdk_cairo_rectangle(gc, &scrollbar_area); cairo_clip(gc); cairo_set_source_rgba(gc, pt->fg_col.red / 65535.0, pt->fg_col.green / 65535.0, pt->fg_col.blue / 65535.0, 0.3); cairo_paint(gc); scrollbar_area.height = pixels_tall; scrollbar_area.y = whole_height - pixels_tall - pixels_from_bottom; gdk_cairo_rectangle(gc, &scrollbar_area); cairo_clip(gc); cairo_set_source_rgba(gc, pt->fg_col.red / 65535.0, pt->fg_col.green / 65535.0, pt->fg_col.blue / 65535.0, 0.7); cairo_paint(gc); cairo_restore(gc); } cairo_destroy(gc); } static void blit_dirty(PangoTerm *pt) { if(!pt->dirty_area.height || !pt->dirty_area.width) return; blit_buffer(pt, &(GdkRectangle){ .x = pt->dirty_area.x + CONF_border, .y = pt->dirty_area.y + CONF_border, .width = pt->dirty_area.width, .height = pt->dirty_area.height, }); pt->dirty_area.width = 0; pt->dirty_area.height = 0; } static void flush_pending(PangoTerm *pt) { if(!pt->pending_area.width) return; cairo_t* gc = cairo_create(pt->buffer); GdkRectangle pending_area = pt->pending_area; int glyphs_x = pending_area.x; int glyphs_y = pending_area.y; if(pt->pen.attrs.dwl) cairo_scale(gc, 2.0, 1.0); if(pt->pen.attrs.dhl) { cairo_scale(gc, 1.0, 2.0); pending_area.y /= 2; pending_area.height /= 2; glyphs_y = pending_area.y; if(pt->pen.attrs.dhl == 2) glyphs_y -= pending_area.height; } /* Background fill */ { cairo_save(gc); gdk_cairo_rectangle(gc, &pending_area); cairo_clip(gc); GdkColor bg = pt->pen.attrs.reverse ? pt->pen.fg_col : pt->pen.bg_col; gdk_cairo_set_source_color(gc, &bg); cairo_paint(gc); cairo_restore(gc); } if(pt->glyphs->len) { PangoLayout *layout = pt->pen.layout; pango_layout_set_text(layout, pt->glyphs->str, pt->glyphs->len); if(pt->pen.pangoattrs) pango_layout_set_attributes(layout, pt->pen.pangoattrs); // Now adjust all the widths PangoLayoutIter *iter = pango_layout_get_iter(layout); do { PangoLayoutRun *run = pango_layout_iter_get_run(iter); if(!run) continue; PangoGlyphString *glyph_str = run->glyphs; int i; for(i = 0; i < glyph_str->num_glyphs; i++) { PangoGlyphInfo *glyph = &glyph_str->glyphs[i]; int str_index = run->item->offset + glyph_str->log_clusters[i]; int char_width = g_array_index(pt->glyph_widths, int, str_index); if(glyph->geometry.width && glyph->geometry.width != char_width * pt->cell_width_pango) { /* Adjust its x_offset to match the width change, to ensure it still * remains centered in the cell */ glyph->geometry.x_offset -= (glyph->geometry.width - char_width * pt->cell_width_pango) / 2; glyph->geometry.width = char_width * pt->cell_width_pango; } } } while(pango_layout_iter_next_run(iter)); pango_layout_iter_free(iter); /* Draw glyphs */ GdkColor fg = pt->pen.attrs.reverse ? pt->pen.bg_col : pt->pen.fg_col; gdk_cairo_set_source_color(gc, &fg); cairo_move_to(gc, glyphs_x, glyphs_y); pango_cairo_show_layout(gc, layout); g_string_truncate(pt->glyphs, 0); } if(pt->pen.attrs.dwl) pt->pending_area.x *= 2, pt->pending_area.width *= 2; if(pt->dirty_area.width && pt->pending_area.height) gdk_rectangle_union(&pt->pending_area, &pt->dirty_area, &pt->dirty_area); else pt->dirty_area = pt->pending_area; pt->pending_area.width = 0; pt->pending_area.height = 0; pt->erase_columns = 0; cairo_destroy(gc); } static void put_glyph(PangoTerm *pt, const uint32_t chars[], int width, VTermPos pos) { PhyPos ph_pos = PHYSPOS_FROM_VTERMPOS(pt, pos); if(ph_pos.prow < 0 || ph_pos.prow >= pt->rows) return; GdkRectangle destarea = GDKRECTANGLE_FROM_PHYPOS_CELLS(pt, ph_pos, width); if(pt->erase_columns) flush_pending(pt); if(destarea.y != pt->pending_area.y || destarea.x != pt->pending_area.x + pt->pending_area.width) flush_pending(pt); char *chars_str = g_ucs4_to_utf8(chars, VTERM_MAX_CHARS_PER_CELL, NULL, NULL, NULL); g_array_set_size(pt->glyph_widths, pt->glyphs->len + 1); g_array_index(pt->glyph_widths, int, pt->glyphs->len) = width; g_string_append(pt->glyphs, chars_str); g_free(chars_str); if(pt->pending_area.width && pt->pending_area.height) gdk_rectangle_union(&destarea, &pt->pending_area, &pt->pending_area); else pt->pending_area = destarea; } static void put_erase(PangoTerm *pt, int width, VTermPos pos) { PhyPos ph_pos = PHYSPOS_FROM_VTERMPOS(pt, pos); if(ph_pos.prow < 0 || ph_pos.prow >= pt->rows) return; GdkRectangle destarea = GDKRECTANGLE_FROM_PHYPOS_CELLS(pt, ph_pos, width); if(!pt->erase_columns) flush_pending(pt); if(destarea.y != pt->pending_area.y || destarea.x != pt->pending_area.x + pt->pending_area.width) flush_pending(pt); if(pt->pending_area.width && pt->pending_area.height) gdk_rectangle_union(&destarea, &pt->pending_area, &pt->pending_area); else pt->pending_area = destarea; pt->erase_columns += width; } static void chpen(VTermScreenCell *cell, void *user_data, int cursoroverride) { PangoTerm *pt = user_data; GdkColor col; #define ADDATTR(a) \ do { \ PangoAttribute *newattr = (a); \ newattr->start_index = 0; \ newattr->end_index = -1; \ pango_attr_list_change(pt->pen.pangoattrs, newattr); \ } while(0) if(cell->attrs.bold != pt->pen.attrs.bold) { int bold = pt->pen.attrs.bold = cell->attrs.bold; flush_pending(pt); ADDATTR(pango_attr_weight_new(bold ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL)); } if(cell->attrs.underline != pt->pen.attrs.underline) { int underline = pt->pen.attrs.underline = cell->attrs.underline; flush_pending(pt); ADDATTR(pango_attr_underline_new(underline == 1 ? PANGO_UNDERLINE_SINGLE : underline == 2 ? PANGO_UNDERLINE_DOUBLE : PANGO_UNDERLINE_NONE)); } if(cell->attrs.italic != pt->pen.attrs.italic) { int italic = pt->pen.attrs.italic = cell->attrs.italic; flush_pending(pt); ADDATTR(pango_attr_style_new(italic ? PANGO_STYLE_ITALIC : PANGO_STYLE_NORMAL)); } if(cell->attrs.reverse != pt->pen.attrs.reverse) { flush_pending(pt); pt->pen.attrs.reverse = cell->attrs.reverse; } if(cell->attrs.strike != pt->pen.attrs.strike) { int strike = pt->pen.attrs.strike = cell->attrs.strike; flush_pending(pt); ADDATTR(pango_attr_strikethrough_new(strike)); } if(cell->attrs.font != pt->pen.attrs.font) { int font = pt->pen.attrs.font = cell->attrs.font; if(font >= pt->n_fonts) font = 0; flush_pending(pt); ADDATTR(pango_attr_family_new(pt->fonts[font])); } if(cell->attrs.dwl != pt->pen.attrs.dwl || cell->attrs.dhl != pt->pen.attrs.dhl) { pt->pen.attrs.dwl = cell->attrs.dwl; pt->pen.attrs.dhl = cell->attrs.dhl; flush_pending(pt); } col = GDK_COLOR_FROM_VTERM_COLOR(cell->fg); if(cursoroverride) { int grey = ((int)pt->cursor_col.red + pt->cursor_col.green + pt->cursor_col.blue)*2 > 65535*3 ? 0 : 65535; col.red = col.green = col.blue = grey; } if(col.red != pt->pen.fg_col.red || col.green != pt->pen.fg_col.green || col.blue != pt->pen.fg_col.blue) { flush_pending(pt); pt->pen.fg_col = col; } col = GDK_COLOR_FROM_VTERM_COLOR(cell->bg); if(cursoroverride) col = pt->cursor_col; if(col.red != pt->pen.bg_col.red || col.green != pt->pen.bg_col.green || col.blue != pt->pen.bg_col.blue) { flush_pending(pt); pt->pen.bg_col = col; } } static void repaint_phyrect(PangoTerm *pt, PhyRect ph_rect) { PhyPos ph_pos; for(ph_pos.prow = ph_rect.start_prow; ph_pos.prow < ph_rect.end_prow; ph_pos.prow++) { for(ph_pos.pcol = ph_rect.start_pcol; ph_pos.pcol < ph_rect.end_pcol; ) { VTermPos pos = VTERMPOS_FROM_PHYSPOS(pt, ph_pos); VTermScreenCell cell; fetch_cell(pt, pos, &cell); if(cell.attrs.dwl != pt->pending_dwl) flush_pending(pt); pt->pending_dwl = cell.attrs.dwl; /* Invert the RV attribute if this cell is selected */ if(pt->highlight_valid) { VTermPos start = pt->highlight_start, stop = pt->highlight_stop; int highlighted = (pos.row > start.row || (pos.row == start.row && pos.col >= start.col)) && (pos.row < stop.row || (pos.row == stop.row && pos.col <= stop.col)); if(highlighted) cell.attrs.reverse = !cell.attrs.reverse; } int cursor_here = pos.row == pt->cursorpos.row && pos.col == pt->cursorpos.col; int cursor_visible = CURSOR_ENABLED(pt) && (pt->cursor_blinkstate || !pt->has_focus); int draw_cursor = cursor_visible && cursor_here; chpen(&cell, pt, draw_cursor && pt->cursor_shape == VTERM_PROP_CURSORSHAPE_BLOCK); if(cell.chars[0] == 0) { put_erase(pt, cell.width, pos); } else { put_glyph(pt, cell.chars, cell.width, pos); } if(draw_cursor) { GdkRectangle cursor_area = GDKRECTANGLE_FROM_PHYPOS_CELLS(pt, ph_pos, 1); gtk_im_context_set_cursor_location(pt->im_context, &cursor_area); if (pt->cursor_shape != VTERM_PROP_CURSORSHAPE_BLOCK) { flush_pending(pt); cairo_t *gc = cairo_create(pt->buffer); gdk_cairo_rectangle(gc, &cursor_area); cairo_clip(gc); switch(pt->cursor_shape) { case VTERM_PROP_CURSORSHAPE_UNDERLINE: gdk_cairo_set_source_color(gc, &pt->cursor_col); cairo_rectangle(gc, cursor_area.x, cursor_area.y + (int)(cursor_area.height * 0.85), cursor_area.width, (int)(cursor_area.height * 0.15)); cairo_fill(gc); break; case VTERM_PROP_CURSORSHAPE_BAR_LEFT: gdk_cairo_set_source_color(gc, &pt->cursor_col); cairo_rectangle(gc, cursor_area.x, cursor_area.y, (cursor_area.width * 0.15), cursor_area.height); cairo_fill(gc); break; } cairo_destroy(gc); } } ph_pos.pcol += cell.width; } } } static void repaint_rect(PangoTerm *pt, VTermRect rect) { PhyRect ph_rect = PHYRECT_FROM_VTERMRECT(pt, rect); repaint_phyrect(pt, ph_rect); } static void repaint_cell(PangoTerm *pt, VTermPos pos) { VTermRect rect = { .start_col = pos.col, .end_col = pos.col + 1, .start_row = pos.row, .end_row = pos.row + 1, }; repaint_rect(pt, rect); } static void repaint_flow(PangoTerm *pt, VTermPos start, VTermPos stop) { VTermRect rect; if(start.row == stop.row) { rect.start_col = start.col; rect.start_row = start.row; rect.end_col = stop.col + 1; rect.end_row = start.row + 1; repaint_rect(pt, rect); } else { rect.start_col = start.col; rect.start_row = start.row; rect.end_col = pt->cols; rect.end_row = start.row + 1; repaint_rect(pt, rect); if(start.row + 1 < stop.row) { rect.start_col = 0; rect.start_row = start.row + 1; rect.end_col = pt->cols; rect.end_row = stop.row; repaint_rect(pt, rect); } rect.start_col = 0; rect.start_row = stop.row; rect.end_col = stop.col + 1; rect.end_row = stop.row + 1; repaint_rect(pt, rect); } } static gboolean cursor_blink(void *user_data) { PangoTerm *pt = user_data; pt->cursor_blinkstate = !pt->cursor_blinkstate; if(CURSOR_ENABLED(pt)) { repaint_cell(pt, pt->cursorpos); flush_pending(pt); blit_dirty(pt); } return TRUE; } static void cursor_start_blinking(PangoTerm *pt) { if(!CONF_cursor_blink_interval) return; pt->cursor_timer_id = g_timeout_add(CONF_cursor_blink_interval, cursor_blink, pt); /* Should start blinking in visible state */ pt->cursor_blinkstate = 1; if(CURSOR_ENABLED(pt)) repaint_cell(pt, pt->cursorpos); } static void cursor_stop_blinking(PangoTerm *pt) { g_source_remove(pt->cursor_timer_id); pt->cursor_timer_id = 0; /* Should always be in visible state */ pt->cursor_blinkstate = 1; if(CURSOR_ENABLED(pt)) repaint_cell(pt, pt->cursorpos); } static void store_clipboard(PangoTerm *pt) { VTermPos start = pt->highlight_start, stop = pt->highlight_stop; gchar *text = fetch_flow_text(pt, start, stop); gtk_clipboard_clear(pt->selection_primary); gtk_clipboard_set_text(pt->selection_primary, text, -1); free(text); } static void cancel_highlight(PangoTerm *pt) { if(!pt->highlight_valid) return; pt->highlight_valid = FALSE; repaint_flow(pt, pt->highlight_start, pt->highlight_stop); flush_pending(pt); blit_dirty(pt); } /* * VTerm event handlers */ static int term_damage(VTermRect rect, void *user_data) { PangoTerm *pt = user_data; if(pt->highlight_valid) { if((pt->highlight_start.row < rect.end_row - 1 || (pt->highlight_start.row == rect.end_row - 1 && pt->highlight_start.col < rect.end_col - 1)) && (pt->highlight_stop.row > rect.start_row || (pt->highlight_stop.row == rect.start_row && pt->highlight_stop.col > rect.start_col))) { /* Damage overlaps highlighted region */ cancel_highlight(pt); } } repaint_rect(pt, rect); return 1; } static int term_sb_pushline(int cols, const VTermScreenCell *cells, void *user_data) { PangoTerm *pt = user_data; PangoTermScrollbackLine *linebuffer = NULL; if(pt->scroll_current == pt->scroll_size) { /* Recycle old row if it's the right size */ if(pt->sb_buffer[pt->scroll_current-1]->cols == cols) linebuffer = pt->sb_buffer[pt->scroll_current-1]; else free(pt->sb_buffer[pt->scroll_current-1]); memmove(pt->sb_buffer + 1, pt->sb_buffer, sizeof(pt->sb_buffer[0]) * (pt->scroll_current - 1)); } else if(pt->scroll_current > 0) { memmove(pt->sb_buffer + 1, pt->sb_buffer, sizeof(pt->sb_buffer[0]) * pt->scroll_current); } if(!linebuffer) { linebuffer = g_malloc0(sizeof(PangoTermScrollbackLine) + cols * sizeof(linebuffer->cells[0])); linebuffer->cols = cols; } pt->sb_buffer[0] = linebuffer; if(pt->scroll_current < pt->scroll_size) pt->scroll_current++; memcpy(linebuffer->cells, cells, sizeof(cells[0]) * cols); return 1; } static int term_sb_popline(int cols, VTermScreenCell *cells, void *user_data) { PangoTerm *pt = user_data; if(!pt->scroll_current) return 0; PangoTermScrollbackLine *linebuffer = pt->sb_buffer[0]; pt->scroll_current--; memmove(pt->sb_buffer, pt->sb_buffer + 1, sizeof(pt->sb_buffer[0]) * (pt->scroll_current)); int cols_to_copy = cols; if(cols_to_copy > linebuffer->cols) cols_to_copy = linebuffer->cols; memcpy(cells, linebuffer->cells, sizeof(cells[0]) * cols_to_copy); for(int col = cols_to_copy; col < cols; col++) { cells[col] = (VTermScreenCell){ .chars = {0}, .width = 1, .attrs = {}, .fg = VTERM_COLOR_FROM_GDK_COLOR(pt->fg_col), .bg = VTERM_COLOR_FROM_GDK_COLOR(pt->bg_col), }; } free(linebuffer); return 1; } static int term_moverect(VTermRect dest, VTermRect src, void *user_data) { PangoTerm *pt = user_data; flush_pending(pt); blit_dirty(pt); if(pt->highlight_valid) { int start_inside = vterm_rect_contains(src, pt->highlight_start); int stop_inside = vterm_rect_contains(src, pt->highlight_stop); if(start_inside && stop_inside && (pt->highlight_start.row == pt->highlight_stop.row || (src.start_col == 0 && src.end_col == pt->cols))) { int delta_row = dest.start_row - src.start_row; int delta_col = dest.start_col - src.start_col; pt->highlight_start.row += delta_row; pt->highlight_start.col += delta_col; pt->highlight_stop.row += delta_row; pt->highlight_stop.col += delta_col; } else if(start_inside || stop_inside) { cancel_highlight(pt); } } PhyRect ph_dest = PHYRECT_FROM_VTERMRECT(pt, dest); if(ph_dest.end_prow < 0 || ph_dest.start_prow >= pt->rows) return 1; if(ph_dest.start_prow < 0) ph_dest.start_prow = 0; if(ph_dest.end_prow >= pt->rows) ph_dest.end_prow = pt->rows; GdkRectangle destarea = GDKRECTANGLE_FROM_PHYRECT(pt, ph_dest); cairo_surface_flush(pt->buffer); cairo_t* gc = cairo_create(pt->buffer); gdk_cairo_rectangle(gc, &destarea); cairo_clip(gc); cairo_set_source_surface(gc, pt->buffer, (dest.start_col - src.start_col) * pt->cell_width, (dest.start_row - src.start_row) * pt->cell_height); cairo_paint(gc); cairo_destroy(gc); blit_buffer(pt, &(GdkRectangle){ .x = destarea.x + CONF_border, .y = destarea.y + CONF_border, .width = destarea.width, .height = destarea.height, }); return 1; } static int term_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user_data) { PangoTerm *pt = user_data; pt->cursorpos = pos; pt->cursor_blinkstate = 1; return 1; } static int term_settermprop(VTermProp prop, VTermValue *val, void *user_data) { PangoTerm *pt = user_data; switch(prop) { case VTERM_PROP_CURSORVISIBLE: pt->cursor_visible = val->boolean; break; case VTERM_PROP_CURSORBLINK: if(val->boolean && !pt->cursor_timer_id) cursor_start_blinking(pt); else if(!val->boolean && pt->cursor_timer_id) cursor_stop_blinking(pt); break; case VTERM_PROP_CURSORSHAPE: pt->cursor_shape = val->number; break; case VTERM_PROP_ICONNAME: gdk_window_set_icon_name(GDK_WINDOW(gtk_widget_get_window(pt->termwin)), val->string); break; case VTERM_PROP_TITLE: gtk_window_set_title(GTK_WINDOW(pt->termwin), val->string); break; case VTERM_PROP_ALTSCREEN: pt->on_altscreen = val->boolean; break; case VTERM_PROP_MOUSE: pt->mousemode = val->number; break; default: return 0; } return 1; } static int term_bell(void *user_data) { PangoTerm *pt = user_data; gtk_widget_error_bell(GTK_WIDGET(pt->termwin)); return 1; } static VTermScreenCallbacks cb = { .damage = term_damage, .moverect = term_moverect, .movecursor = term_movecursor, .settermprop = term_settermprop, .bell = term_bell, .sb_pushline = term_sb_pushline, .sb_popline = term_sb_popline, }; static void altscreen_scroll(PangoTerm *pt, int delta, GtkOrientation orientation) { if (CONF_altscreen_scroll) { VTermKey which_arrow; if(delta > 0) { which_arrow = ((orientation == GTK_ORIENTATION_VERTICAL) ? VTERM_KEY_UP : VTERM_KEY_RIGHT); } else if(delta < 0) { which_arrow = ((orientation == GTK_ORIENTATION_VERTICAL) ? VTERM_KEY_DOWN : VTERM_KEY_LEFT); } for(int i=0; i < ((delta <= -1) ? -delta : delta); i++) { vterm_keyboard_key(pt->vt, which_arrow, 0); } term_flush_output(pt); } } static void hscroll_delta(PangoTerm *pt, int delta) { if(pt->on_altscreen) { altscreen_scroll(pt, delta, GTK_ORIENTATION_HORIZONTAL); } } static void vscroll_delta(PangoTerm *pt, int delta) { if(pt->on_altscreen) { altscreen_scroll(pt, delta, GTK_ORIENTATION_VERTICAL); return; } if(delta > 0) { if(pt->scroll_offs + delta > pt->scroll_current) delta = pt->scroll_current - pt->scroll_offs; } else if(delta < 0) { if(delta < -pt->scroll_offs) delta = -pt->scroll_offs; } if(!delta) return; pt->scroll_offs += delta; pt->cursor_hidden_for_redraw = 1; repaint_cell(pt, pt->cursorpos); PhyRect ph_repaint = { .start_pcol = 0, .end_pcol = pt->cols, .start_prow = 0, .end_prow = pt->rows, }; if(abs(delta) < pt->rows) { PhyRect ph_dest = { .start_pcol = 0, .end_pcol = pt->cols, .start_prow = 0, .end_prow = pt->rows, }; if(delta > 0) { ph_dest.start_prow = delta; ph_repaint.end_prow = delta; } else { ph_dest.end_prow = pt->rows + delta; ph_repaint.start_prow = pt->rows + delta; } GdkRectangle destarea = GDKRECTANGLE_FROM_PHYRECT(pt, ph_dest); cairo_surface_flush(pt->buffer); cairo_t *gc = cairo_create(pt->buffer); gdk_cairo_rectangle(gc, &destarea); cairo_clip(gc); cairo_set_source_surface(gc, pt->buffer, 0, delta * pt->cell_height); cairo_paint(gc); cairo_destroy(gc); } repaint_phyrect(pt, ph_repaint); pt->cursor_hidden_for_redraw = 0; repaint_cell(pt, pt->cursorpos); flush_pending(pt); GdkRectangle whole_screen = { .x = 0, .y = 0, .width = pt->cols * pt->cell_width + 2 * CONF_border, .height = pt->rows * pt->cell_height + 2 * CONF_border, }; blit_buffer(pt, &whole_screen); } /* * GTK widget event handlers */ static gboolean widget_keypress(GtkWidget *widget, GdkEventKey *event, gpointer user_data) { PangoTerm *pt = user_data; /* GtkIMContext will eat a Shift-Space and not tell us about shift. * Also don't let IME eat any GDK_KEY_KP_ events */ gboolean ret = (event->state & GDK_SHIFT_MASK && event->keyval == ' ') ? FALSE : (event->keyval >= GDK_KEY_KP_Space && event->keyval <= GDK_KEY_KP_Divide) ? FALSE : gtk_im_context_filter_keypress(pt->im_context, event); if(ret) return TRUE; // We don't need to track the state of modifier bits if(event->is_modifier) return FALSE; if((event->keyval == GDK_KEY_Insert && event->state & GDK_SHIFT_MASK) || ((event->keyval == 'v' || event->keyval == 'V') && event->state & GDK_CONTROL_MASK && event->state & GDK_SHIFT_MASK)) { /* Shift-Insert or Ctrl-Shift-V pastes clipboard */ gchar *str = gtk_clipboard_wait_for_text(event->keyval == GDK_KEY_Insert ? pt->selection_primary : pt->selection_clipboard); if(!str) return TRUE; lf_to_cr(str); term_push_string(pt, str, TRUE); return TRUE; } if((event->keyval == 'c' || event->keyval == 'C') && event->state & GDK_CONTROL_MASK && event->state & GDK_SHIFT_MASK) { /* Ctrl-Shift-C copies to clipboard */ if(!pt->highlight_valid) return TRUE; gchar *text = fetch_flow_text(pt, pt->highlight_start, pt->highlight_stop); if(!text) return TRUE; gtk_clipboard_clear(pt->selection_clipboard); gtk_clipboard_set_text(pt->selection_clipboard, text, -1); free(text); return TRUE; } if(event->keyval == GDK_KEY_Page_Down && event->state & GDK_SHIFT_MASK) { vscroll_delta(pt, -pt->rows / 2); return TRUE; } if(event->keyval == GDK_KEY_Page_Up && event->state & GDK_SHIFT_MASK) { vscroll_delta(pt, +pt->rows / 2); return TRUE; } VTermModifier mod = convert_modifier(event->state); VTermKey keyval = convert_keyval(event->keyval, &mod); /* * See also * /usr/include/gtk-2.0/gdk/gdkkeysyms.h */ if(keyval) { /* Shift-Enter and Shift-Backspace are too easy to mistype accidentally * Optionally remove shift if it's the only modifier */ if(mod == VTERM_MOD_SHIFT) switch(keyval) { case VTERM_KEY_ENTER: if(!CONF_chord_shift_enter) mod = 0; break; case VTERM_KEY_BACKSPACE: if(!CONF_chord_shift_backspace) mod = 0; break; default: break; } vterm_keyboard_key(pt->vt, keyval, mod); } else if(event->keyval >= 0x10000000) /* Extension key, not printable Unicode */ return FALSE; else if(event->keyval >= 0x01000000) /* Unicode shifted */ vterm_keyboard_unichar(pt->vt, event->keyval - 0x01000000, mod); else if(event->keyval < 0x0f00) { /* GDK key code; convert to Unicode */ guint32 unichar = gdk_keyval_to_unicode(event->keyval); if (unichar == 0) return FALSE; /* Shift-Space is too easy to mistype so optionally ignore that */ if(mod == VTERM_MOD_SHIFT && event->keyval == ' ') if(!CONF_chord_shift_space) mod = 0; vterm_keyboard_unichar(pt->vt, unichar, mod); } else if(event->keyval >= GDK_KEY_KP_0 && event->keyval <= GDK_KEY_KP_9) /* event->keyval is a keypad number; just treat it as Unicode */ vterm_keyboard_unichar(pt->vt, event->keyval - GDK_KEY_KP_0 + '0', mod); else return FALSE; if(CONF_unscroll_on_key && pt->scroll_offs) vscroll_delta(pt, -pt->scroll_offs); term_flush_output(pt); return FALSE; } static gboolean widget_keyrelease(GtkWidget *widget, GdkEventKey *event, gpointer user_data) { PangoTerm *pt = user_data; return gtk_im_context_filter_keypress(pt->im_context, event); } static gboolean widget_mousepress(GtkWidget *widget, GdkEventButton *event, gpointer user_data) { PangoTerm *pt = user_data; PhyPos ph_pos = { .pcol = (event->x - CONF_border) / pt->cell_width, .prow = (event->y - CONF_border) / pt->cell_height, }; /* If the mouse is being dragged, we'll get motion events even outside our * window */ int is_inside = (ph_pos.pcol >= 0 && ph_pos.pcol < pt->cols && ph_pos.prow >= 0 && ph_pos.prow < pt->rows); VTermPos pos = VTERMPOS_FROM_PHYSPOS(pt, ph_pos); /* Shift modifier bypasses terminal mouse handling */ if(pt->mousemode && !(event->state & GDK_SHIFT_MASK) && is_inside) { VTermModifier state = convert_modifier(event->state); int is_press; switch(event->type) { case GDK_BUTTON_PRESS: is_press = 1; break; case GDK_BUTTON_RELEASE: is_press = 0; break; default: return TRUE; } vterm_mouse_move(pt->vt, pos.row, pos.col, state); vterm_mouse_button(pt->vt, event->button, is_press, state); term_flush_output(pt); } else if(event->button == 2 && event->type == GDK_BUTTON_PRESS && is_inside) { /* Middle-click pastes primary selection */ gchar *str = gtk_clipboard_wait_for_text(pt->selection_primary); if(!str) return FALSE; lf_to_cr(str); term_push_string(pt, str, TRUE); } else if(event->button == 1 && event->type == GDK_BUTTON_PRESS && is_inside) { cancel_highlight(pt); pt->dragging = DRAG_PENDING; pt->drag_start = pos; } else if(event->button == 1 && event->type == GDK_BUTTON_RELEASE && pt->dragging != NO_DRAG) { /* Always accept a release even when outside */ pt->dragging = NO_DRAG; if(pt->highlight_valid) store_clipboard(pt); } else if(event->button == 1 && event->type == GDK_2BUTTON_PRESS && is_inside) { /* Highlight a word. start with the position, and extend it both sides * over word characters */ VTermPos start_pos = pos; while(start_pos.col > 0 || start_pos.row > 0) { VTermPos cellpos = start_pos; VTermScreenCell cell; pos_prev(pt, &cellpos); fetch_cell(pt, cellpos, &cell); if(!is_wordchar(cell.chars[0])) break; start_pos = cellpos; } VTermPos stop_pos = pos; while(stop_pos.col < pt->cols - 1 || stop_pos.row < pt->rows - 1) { VTermPos cellpos = stop_pos; VTermScreenCell cell; pos_next(pt, &cellpos); fetch_cell(pt, cellpos, &cell); if(!is_wordchar(cell.chars[0])) break; stop_pos = cellpos; } pt->highlight_valid = true; pt->highlight_start = start_pos; pt->highlight_stop = stop_pos; repaint_flow(pt, pt->highlight_start, pt->highlight_stop); flush_pending(pt); blit_dirty(pt); store_clipboard(pt); } else if(event->button == 1 && event->type == GDK_3BUTTON_PRESS && is_inside) { /* Highlight an entire line */ pt->highlight_valid = true; pt->highlight_start.row = pos.row; pt->highlight_start.col = 0; pt->highlight_stop.row = pos.row; pt->highlight_stop.col = pt->cols - 1; repaint_flow(pt, pt->highlight_start, pt->highlight_stop); flush_pending(pt); blit_dirty(pt); store_clipboard(pt); } return FALSE; } static gboolean widget_mousemove(GtkWidget *widget, GdkEventMotion *event, gpointer user_data) { PangoTerm *pt = user_data; PhyPos ph_pos = { .pcol = (event->x - CONF_border) / pt->cell_width, .prow = (event->y - CONF_border) / pt->cell_height, }; /* If the mouse is being dragged, we'll get motion events even outside our * window */ int is_inside = (ph_pos.pcol >= 0 && ph_pos.pcol < pt->cols && ph_pos.prow >= 0 && ph_pos.prow < pt->rows); if(ph_pos.pcol < 0) ph_pos.pcol = 0; if(ph_pos.pcol > pt->cols) ph_pos.pcol = pt->cols; /* allow off-by-1 */ if(ph_pos.prow < 0) ph_pos.prow = 0; if(ph_pos.prow >= pt->rows) ph_pos.prow = pt->rows - 1; VTermPos pos = VTERMPOS_FROM_PHYSPOS(pt, ph_pos); /* Shift modifier bypasses terminal mouse handling */ if(pt->mousemode > VTERM_PROP_MOUSE_CLICK && !(event->state & GDK_SHIFT_MASK) && is_inside) { if(pos.row < 0 || pos.row >= pt->rows) return TRUE; VTermModifier state = convert_modifier(event->state); vterm_mouse_move(pt->vt, pos.row, pos.col, state); term_flush_output(pt); } else if(event->state & GDK_BUTTON1_MASK) { VTermPos old_pos = pt->dragging == DRAGGING ? pt->drag_pos : pt->drag_start; if(pos.row == old_pos.row && pos.col == old_pos.col) /* Unchanged; stop here */ return FALSE; pt->dragging = DRAGGING; pt->drag_pos = pos; VTermPos pos_left1 = pt->drag_pos; if(pos_left1.col > 0) pos_left1.col--; pt->highlight_valid = true; VTermPos repaint_start = pt->highlight_start; VTermPos repaint_stop = pt->highlight_stop; if(vterm_pos_cmp(pt->drag_start, pt->drag_pos) > 0) { pt->highlight_start = pt->drag_pos; pt->highlight_stop = pt->drag_start; } else { pt->highlight_start = pt->drag_start; pt->highlight_stop = pt->drag_pos; if(pt->highlight_stop.col > 0) pt->highlight_stop.col--; /* exclude partial cell */ if(fetch_is_eol(pt, pt->highlight_stop)) pt->highlight_stop.col = pt->cols - 1; } if(vterm_pos_cmp(pt->highlight_start, repaint_start) < 0) repaint_start = pt->highlight_start; if(vterm_pos_cmp(pt->highlight_stop, repaint_stop) > 0) repaint_stop = pt->highlight_stop; repaint_flow(pt, repaint_start, repaint_stop); flush_pending(pt); blit_dirty(pt); } return FALSE; } static gboolean widget_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data) { PangoTerm *pt = user_data; PhyPos ph_pos = { .pcol = (event->x - CONF_border) / pt->cell_width, .prow = (event->y - CONF_border) / pt->cell_height, }; if(pt->mousemode && !(event->state & GDK_SHIFT_MASK)) { VTermPos pos = VTERMPOS_FROM_PHYSPOS(pt, ph_pos); if(pos.row < 0 || pos.row >= pt->rows) return TRUE; /* Translate scroll direction back into a button number */ int button; switch(event->direction) { case GDK_SCROLL_UP: button = 4; break; case GDK_SCROLL_DOWN: button = 5; break; default: return FALSE; } VTermModifier state = convert_modifier(event->state); vterm_mouse_move(pt->vt, pos.row, pos.col, state); vterm_mouse_button(pt->vt, button, 1, state); term_flush_output(pt); } else { switch(event->direction) { case GDK_SCROLL_UP: vscroll_delta(pt, +CONF_scroll_wheel_delta); break; case GDK_SCROLL_DOWN: vscroll_delta(pt, -CONF_scroll_wheel_delta); break; case GDK_SCROLL_RIGHT: hscroll_delta(pt, +1); break; case GDK_SCROLL_LEFT: hscroll_delta(pt, -1); break; default: return FALSE; } } return FALSE; } static gboolean widget_im_commit(GtkIMContext *context, gchar *str, gpointer user_data) { PangoTerm *pt = user_data; term_push_string(pt, str, FALSE); if(CONF_unscroll_on_key && pt->scroll_offs) vscroll_delta(pt, -pt->scroll_offs); return FALSE; } static gboolean widget_expose(GtkWidget *widget, GdkEventExpose *event, gpointer user_data) { PangoTerm *pt = user_data; /* GDK always sends resize events before expose events, so it's possible this * expose event is for a region that now doesn't exist. */ int right = 2 * CONF_border + pt->cols * pt->cell_width; int bottom = 2 * CONF_border + pt->rows * pt->cell_height; /* Trim to still-valid area, or ignore if there's nothing remaining */ if(event->area.x + event->area.width > right) event->area.width = right - event->area.x; if(event->area.y + event->area.height > bottom) event->area.height = bottom - event->area.y; if(event->area.height && event->area.width) blit_buffer(pt, &event->area); return TRUE; } static void widget_resize(GtkContainer* widget, gpointer user_data) { PangoTerm *pt = user_data; gint raw_width, raw_height; gtk_window_get_size(GTK_WINDOW(widget), &raw_width, &raw_height); raw_width -= 2 * CONF_border; raw_height -= 2 * CONF_border; int cols = raw_width / pt->cell_width; int rows = raw_height / pt->cell_height; if(cols == pt->cols && rows == pt->rows) return; // Clamp to a minimum 1x1 size because libvterm doesn't like zero if(!cols) cols = 1; if(!rows) rows = 1; pt->cols = cols; pt->rows = rows; if(pt->resizedfn) (*pt->resizedfn)(rows, cols, pt->resizedfn_data); cairo_surface_t* new_buffer = gdk_window_create_similar_surface(pt->termdraw, CAIRO_CONTENT_COLOR, cols * pt->cell_width, rows * pt->cell_height); cairo_t* gc = cairo_create(new_buffer); cairo_set_source_surface(gc, pt->buffer, 0, 0); cairo_paint(gc); cairo_destroy(gc); cairo_surface_destroy(pt->buffer); pt->buffer = new_buffer; vterm_set_size(pt->vt, rows, cols); vterm_screen_flush_damage(pt->vts); return; } static void widget_focus_in(GtkWidget *widget, GdkEventFocus *event, gpointer user_data) { PangoTerm *pt = user_data; pt->has_focus = 1; VTermState *state = vterm_obtain_state(pt->vt); vterm_state_focus_in(state); if(CURSOR_ENABLED(pt)) { repaint_cell(pt, pt->cursorpos); flush_pending(pt); blit_dirty(pt); } gtk_im_context_focus_in(pt->im_context); } static void widget_focus_out(GtkWidget *widget, GdkEventFocus *event, gpointer user_data) { PangoTerm *pt = user_data; pt->has_focus = 0; VTermState *state = vterm_obtain_state(pt->vt); vterm_state_focus_out(state); if(CURSOR_ENABLED(pt)) { repaint_cell(pt, pt->cursorpos); flush_pending(pt); blit_dirty(pt); } gtk_im_context_focus_out(pt->im_context); } static void widget_quit(GtkContainer* widget, gpointer unused_data) { gtk_main_quit(); } static GdkPixbuf *load_icon(GdkColor *background) { /* This technique stolen from * http://git.gnome.org/browse/gtk+/tree/gtk/gtkicontheme.c#n3180 * * Updated because rsvg no longer supports loading file: URL scheme, only * data:. */ gchar *icon; gsize icon_len; if(!g_file_get_contents(PANGOTERM_SHAREDIR "/pixmaps/pangoterm.svg", &icon, &icon_len, NULL)) return NULL; gchar *icon_base64 = g_base64_encode((guchar*)icon, icon_len); g_free(icon); gchar *str = g_strdup_printf( "\n" "\n" " \n" " \n" "", background->red / 255, background->green / 255, background->blue / 255, icon_base64); g_free(icon_base64); GInputStream *stream = g_memory_input_stream_new_from_data(str, -1, g_free); GdkPixbuf *ret = gdk_pixbuf_new_from_stream(stream, NULL, NULL); g_object_unref(stream); return ret; } PangoTerm *pangoterm_new(int rows, int cols) { PangoTerm *pt = g_new0(PangoTerm, 1); pt->rows = rows; pt->cols = cols; pt->writefn = NULL; pt->resizedfn = NULL; pt->n_fonts = 1; pt->fonts = malloc(sizeof(char *) * 2); pt->fonts[0] = g_strdup("Monospace"); pt->fonts[1] = NULL; pt->font_size = CONF_size; pt->cursor_col = (GdkColor){ 0xffff, 0xffff, 0xffff }; gdk_color_parse(CONF_cursor, &pt->cursor_col); /* Create VTerm */ pt->vt = vterm_new(rows, cols); vterm_set_utf8(pt->vt, 1); /* Set up state */ VTermState *state = vterm_obtain_state(pt->vt); vterm_state_set_bold_highbright(state, CONF_bold_highbright); for(int index = 0; index < sizeof(colours)/sizeof(colours[0]); index++) { if(!colours[index].is_set) continue; vterm_state_set_palette_color(state, index, &VTERM_COLOR_FROM_GDK_COLOR(colours[index].col)); } /* Set up screen */ pt->vts = vterm_obtain_screen(pt->vt); vterm_screen_enable_altscreen(pt->vts, CONF_altscreen); vterm_screen_set_callbacks(pt->vts, &cb, pt); vterm_screen_set_damage_merge(pt->vts, VTERM_DAMAGE_SCROLL); /* Set up GTK widget */ pt->termwin = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_widget_set_double_buffered(pt->termwin, FALSE); gtk_widget_modify_bg(pt->termwin, GTK_STATE_NORMAL, &pt->bg_col); pt->glyphs = g_string_sized_new(128); pt->glyph_widths = g_array_new(FALSE, FALSE, sizeof(int)); gtk_widget_realize(pt->termwin); pt->termdraw = gtk_widget_get_window(pt->termwin); gdk_window_set_cursor(GDK_WINDOW(pt->termdraw), gdk_cursor_new(GDK_XTERM)); cursor_start_blinking(pt); pt->cursor_shape = VTERM_PROP_CURSORSHAPE_BLOCK; GdkEventMask mask = gdk_window_get_events(pt->termdraw); gdk_window_set_events(pt->termdraw, mask|GDK_BUTTON_PRESS_MASK|GDK_BUTTON_RELEASE_MASK|GDK_POINTER_MOTION_MASK); g_signal_connect(G_OBJECT(pt->termwin), "expose-event", G_CALLBACK(widget_expose), pt); g_signal_connect(G_OBJECT(pt->termwin), "key-press-event", G_CALLBACK(widget_keypress), pt); g_signal_connect(G_OBJECT(pt->termwin), "key-release-event", G_CALLBACK(widget_keyrelease), pt); g_signal_connect(G_OBJECT(pt->termwin), "button-press-event", G_CALLBACK(widget_mousepress), pt); g_signal_connect(G_OBJECT(pt->termwin), "button-release-event", G_CALLBACK(widget_mousepress), pt); g_signal_connect(G_OBJECT(pt->termwin), "motion-notify-event", G_CALLBACK(widget_mousemove), pt); g_signal_connect(G_OBJECT(pt->termwin), "scroll-event", G_CALLBACK(widget_scroll), pt); g_signal_connect(G_OBJECT(pt->termwin), "focus-in-event", G_CALLBACK(widget_focus_in), pt); g_signal_connect(G_OBJECT(pt->termwin), "focus-out-event", G_CALLBACK(widget_focus_out), pt); g_signal_connect(G_OBJECT(pt->termwin), "destroy", G_CALLBACK(widget_quit), pt); pt->im_context = gtk_im_multicontext_new(); GdkWindow *gdkwin = gtk_widget_get_window(GTK_WIDGET(pt->termwin)); gtk_im_context_set_client_window(pt->im_context, gdkwin); gtk_im_context_set_use_preedit(pt->im_context, false); g_signal_connect(G_OBJECT(pt->im_context), "commit", G_CALLBACK(widget_im_commit), pt); g_signal_connect(G_OBJECT(pt->termwin), "check-resize", G_CALLBACK(widget_resize), pt); pt->dragging = NO_DRAG; pt->selection_primary = gtk_clipboard_get(GDK_SELECTION_PRIMARY); pt->selection_clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); pt->scroll_size = CONF_scrollback_size; pt->sb_buffer = g_new0(PangoTermScrollbackLine*, pt->scroll_size); return pt; } void pangoterm_free(PangoTerm *pt) { g_strfreev(pt->fonts); vterm_free(pt->vt); } void pangoterm_set_default_colors(PangoTerm *pt, GdkColor *fg_col, GdkColor *bg_col) { pt->fg_col = *fg_col; pt->bg_col = *bg_col; vterm_state_set_default_colors(vterm_obtain_state(pt->vt), &VTERM_COLOR_FROM_GDK_COLOR(*fg_col), &VTERM_COLOR_FROM_GDK_COLOR(*bg_col)); /* TODO: Do the equivalent using raw Xlib calls when using X backend, * as basically all these calls are deprecated. */ /* GdkColormap* colormap = gdk_colormap_get_system(); gdk_rgb_find_color(colormap, bg_col); gdk_window_set_background(pt->termdraw, bg_col); */ GdkPixbuf *icon = load_icon(bg_col); if(icon) { gtk_window_set_icon(GTK_WINDOW(pt->termwin), icon); g_object_unref(icon); } } void pangoterm_set_fonts(PangoTerm *pt, char *font, char **alt_fonts) { int n_fonts = 1; while(alt_fonts[n_fonts-1]) n_fonts++; g_strfreev(pt->fonts); pt->n_fonts = n_fonts; pt->fonts = malloc(sizeof(char*) * (n_fonts + 1)); pt->fonts[0] = g_strdup(font); for(int i = 1; i < n_fonts; i++) pt->fonts[i] = g_strdup(alt_fonts[i-1]); pt->fonts[n_fonts] = NULL; } void pangoterm_set_font_size(PangoTerm *pt, double size) { pt->font_size = size; } void pangoterm_set_title(PangoTerm *pt, const char *title) { gtk_window_set_title(GTK_WINDOW(pt->termwin), title); } void pangoterm_set_write_fn(PangoTerm *pt, PangoTermWriteFn *fn, void *user) { pt->writefn = fn; pt->writefn_data = user; } void pangoterm_set_resized_fn(PangoTerm *pt, PangoTermResizedFn *fn, void *user) { pt->resizedfn = fn; pt->resizedfn_data = user; } void pangoterm_start(PangoTerm *pt) { /* Finish the rest of the setup and start */ cairo_t *cctx = gdk_cairo_create(pt->termdraw); PangoContext *pctx = pango_cairo_create_context(cctx); PangoFontDescription *fontdesc = pango_font_description_from_string(pt->fonts[0]); if(pango_font_description_get_size(fontdesc) == 0) pango_font_description_set_size(fontdesc, pt->font_size * PANGO_SCALE); pango_context_set_font_description(pctx, fontdesc); pango_cairo_context_set_resolution(pctx, gdk_screen_get_resolution(gdk_screen_get_default())); pt->pen.pangoattrs = pango_attr_list_new(); pt->pen.layout = pango_layout_new(pctx); pango_layout_set_font_description(pt->pen.layout, fontdesc); PangoFontMetrics *metrics = pango_context_get_metrics(pctx, pango_context_get_font_description(pctx), pango_context_get_language(pctx)); int width = (pango_font_metrics_get_approximate_char_width(metrics) + pango_font_metrics_get_approximate_digit_width(metrics)) / 2; int height = pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics); pt->cell_width_pango = width; pt->cell_width = PANGO_PIXELS_CEIL(width); pt->cell_height = PANGO_PIXELS_CEIL(height); GdkColor fg_col = { 0xffff * 0.90, 0xffff * 0.90, 0xffff * 0.90 }; gdk_color_parse(CONF_foreground, &fg_col); GdkColor bg_col = { 0, 0, 0 }; gdk_color_parse(CONF_background, &bg_col); pangoterm_set_default_colors(pt, &fg_col, &bg_col); gtk_window_resize(GTK_WINDOW(pt->termwin), pt->cols * pt->cell_width + 2 * CONF_border, pt->rows * pt->cell_height + 2 * CONF_border); pt->buffer = gdk_window_create_similar_surface(pt->termdraw, CAIRO_CONTENT_COLOR, pt->cols * pt->cell_width, pt->rows * pt->cell_height); GdkGeometry hints; hints.min_width = pt->cell_width + 2 * CONF_border; hints.min_height = pt->cell_height + 2 * CONF_border; hints.width_inc = pt->cell_width; hints.height_inc = pt->cell_height; gtk_window_set_resizable(GTK_WINDOW(pt->termwin), TRUE); gtk_window_set_geometry_hints(GTK_WINDOW(pt->termwin), GTK_WIDGET(pt->termwin), &hints, GDK_HINT_RESIZE_INC | GDK_HINT_MIN_SIZE); vterm_screen_reset(pt->vts, 1); VTermState *state = vterm_obtain_state(pt->vt); vterm_state_set_termprop(state, VTERM_PROP_CURSORSHAPE, &(VTermValue){ .number = CONF_cursor_shape }); if(CONF_geometry && CONF_geometry[0]) gtk_window_parse_geometry(GTK_WINDOW(pt->termwin), CONF_geometry); gtk_widget_show_all(pt->termwin); } void pangoterm_push_bytes(PangoTerm *pt, const char *bytes, size_t len) { if(CONF_unscroll_on_output && pt->scroll_offs) vscroll_delta(pt, -pt->scroll_offs); vterm_input_write(pt->vt, bytes, len); } void pangoterm_begin_update(PangoTerm *pt) { /* Hide cursor during damage flush */ pt->cursor_hidden_for_redraw = 1; repaint_cell(pt, pt->cursorpos); } void pangoterm_end_update(PangoTerm *pt) { vterm_screen_flush_damage(pt->vts); pt->cursor_hidden_for_redraw = 0; repaint_cell(pt, pt->cursorpos); flush_pending(pt); blit_dirty(pt); term_flush_output(pt); } pangoterm-0~bzr607/pangoterm.cfg000066400000000000000000000025201320166567300170450ustar00rootroot00000000000000# Default pangoterm configuration # This file should be stored in $HOME/.config/pangoterm.cfg # == Colours == # foreground = "gray90" # background = "black" # cursor = "white" # colour:4 = "#0000CC" # == Font and size == # font = "DejaVu Sans Mono" # size = 9.0 # border = 2 (pixels) # == Window options == # lines = 25 # cols = 80 # title = "pangoterm" # == Cursor options == # cursor_shape = 1 (1=block 2=underbar 3=vertical bar) # cursor_blink_interval = 500 (in msec; 0 to disable) # == Scroll options == # scrollback_size = 1000 # scrollbar_width = 3 # scroll_wheel_delta = 3 # unscroll_on_output = true # unscroll_on_key = true # == Other options == # bold_highbright = true # - should bold attribute select high-brightness colours? # altscreen = true # - enable altscreen buffer # altscreen_scroll = false # - emulate scrolling as arrow-key presses when in altscreen # term = "xterm" # - override the $TERM environment variable # doubleclick_fullword = false # - doubleclick to select words will extend until whitespace # chord_shift_space = true # chord_shift_backspace = true # chord_shift_enter = true # - enable CSIu encoding of these commonly-mistyped modified keys # Options can be specific to profiles # [Profile green] # background=darkgreen # # Profile matches can use wildcards # [Profile *-large] # lines = 50 # cols = 120 pangoterm-0~bzr607/pangoterm.desktop000066400000000000000000000002701320166567300177570ustar00rootroot00000000000000[Desktop Entry] Version=1.0 Terminal=false Exec=pangoterm Icon=utilities-terminal Type=Application Categories=System;TerminalEmulator; Name=PangoTerm Comment=A minimalist GTK terminal pangoterm-0~bzr607/pangoterm.h000066400000000000000000000017651320166567300165470ustar00rootroot00000000000000#ifndef __PANGOTERM_H__ #define __PANGOTERM_H__ #include "vterm.h" #include typedef struct PangoTerm PangoTerm; PangoTerm *pangoterm_new(int rows, int cols); void pangoterm_free(PangoTerm *pt); void pangoterm_set_default_colors(PangoTerm *pt, GdkColor *fg_col, GdkColor *bg_col); void pangoterm_set_font_size(PangoTerm *pt, double size); void pangoterm_set_fonts(PangoTerm *pt, char *font, char **alt_fonts); // ptr not value void pangoterm_set_title(PangoTerm *pt, const char *title); void pangoterm_start(PangoTerm *pt); void pangoterm_begin_update(PangoTerm *pt); void pangoterm_push_bytes(PangoTerm *pt, const char *bytes, size_t len); void pangoterm_end_update(PangoTerm *pt); typedef size_t PangoTermWriteFn(const char *bytes, size_t len, void *user); void pangoterm_set_write_fn(PangoTerm *pt, PangoTermWriteFn *fn, void *user); typedef void PangoTermResizedFn(int rows, int cols, void *user); void pangoterm_set_resized_fn(PangoTerm *pt, PangoTermResizedFn *fn, void *user); #endif pangoterm-0~bzr607/pangoterm.svg000066400000000000000000000153011320166567300171060ustar00rootroot00000000000000