/*
* T.c
* T is a lean Terminal emulator.
*
* This file is part of T.
*
* T is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* T is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* T. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "config.h"
#define PROGRAM_NAME "T"
#define SOCKPATH CONFIG_SOCKDIR "/" CONFIG_SOCKNAME
#define SOCKPATH_MAXLEN sizeof((SOCKPATH))
#define MSGBUF_MAXLEN 64
#define ARG_DAEMON "-d"
#define ARG_NEWWIN "-n"
#define ARG_EXIT "-x"
typedef enum {
MSG_NEWWIN,
MSG_EXIT,
MSG_OK,
MSG_ERROR
} TMessage;
static int window_count = 0;
/* UI event callbacks */
static void window_destroy_callback(GtkWidget *widget, gpointer data);
static gboolean key_press_callback(GtkWidget *widget, GdkEvent *event, gpointer data);
static void terminal_child_exited_callback(VteTerminal *terminal, gpointer data);
static void terminal_window_title_changed_callback(VteTerminal *terminal, gpointer data);
/* UI helper functions */
static void change_font_size(VteTerminal *terminal, int delta);
static void set_preferences(VteTerminal *terminal);
static void setup_terminal(GtkWindow *window, VteTerminal *terminal);
/* (local) Networking & utility functions */
static int num_open_windows();
static void new_window();
static void wrn(const char *message);
static void err(const char *message, int ecode);
static int conn_socket();
static int bind_socket();
static void cleanup_socket(int sock_fd);
static TMessage handle_message(TMessage msg);
static gboolean socket_g_io_watch(GIOChannel *source, GIOCondition condition, gpointer data);
static int send_message(TMessage msg);
static int run_daemon();
static void usage();
static void
window_destroy_callback(GtkWidget *widget, gpointer data)
{
/* Keeping track of the open windows */
window_count--;
}
static gboolean
key_press_callback(GtkWidget *widget, GdkEvent *event, gpointer data)
{
GdkEventKey *key_event = (GdkEventKey *) event;
VteTerminal *terminal = VTE_TERMINAL(widget);
gboolean has_shift = (key_event->state & GDK_SHIFT_MASK) != 0;
gboolean has_control = (key_event->state & GDK_CONTROL_MASK) != 0;
if (!has_control)
return FALSE;
switch (key_event->keyval) {
case GDK_KEY_minus:
change_font_size(terminal, -1);
return TRUE;
case GDK_KEY_plus:
change_font_size(terminal, 1);
return TRUE;
}
if (!has_shift)
return FALSE;
/* Handles ctrl + shift + {c,v,n} */
switch (key_event->keyval) {
case GDK_KEY_C:
case GDK_KEY_c:
/* turn this event into a Control+Insert and delegate it */
key_event->keyval = GDK_KEY_Insert;
key_event->state &= ~GDK_SHIFT_MASK;
break;
case GDK_KEY_V:
case GDK_KEY_v:
/* turn this event into a Shift+Insert and delegate it */
key_event->keyval = GDK_KEY_Insert;
key_event->state &= ~GDK_CONTROL_MASK;
break;
case GDK_KEY_N:
case GDK_KEY_n:
new_window();
return TRUE;
}
return FALSE;
}
static void
terminal_child_exited_callback(VteTerminal *terminal, gpointer data)
{
/* The forked process exited; issue destruction of the terminal window */
gtk_widget_destroy(GTK_WIDGET(GTK_WINDOW(data)));
}
static void
terminal_window_title_changed_callback(VteTerminal *terminal, gpointer data)
{
GtkWindow *window = GTK_WINDOW(data);
gtk_window_set_title(window, vte_terminal_get_window_title(terminal));
}
static void
change_font_size(VteTerminal *terminal, int delta)
{
PangoFontDescription *desc = (PangoFontDescription *) vte_terminal_get_font(terminal);
gint font_size = pango_font_description_get_size(desc) + delta * PANGO_SCALE;
pango_font_description_set_size(desc, font_size);
vte_terminal_set_font(terminal, desc);
}
static void
set_preferences(VteTerminal *terminal)
{
/*
* Options set here can (and should) be configured in config.h
*/
/*
* These are static because this function is called every time a new window
* is created and we don't want to parse the colors all over again
*/
static gboolean colors_parsed = FALSE;
static GdkColor palette[CONFIG_PALETTE_SIZE], bg_color, fg_color;
static const char *CONFIG_COLOR_PALETTE[CONFIG_PALETTE_SIZE] = {
CONFIG_PALLETE_0,
CONFIG_PALLETE_1,
CONFIG_PALLETE_2,
CONFIG_PALLETE_3,
CONFIG_PALLETE_4,
CONFIG_PALLETE_5,
CONFIG_PALLETE_6,
CONFIG_PALLETE_7,
CONFIG_PALLETE_8,
CONFIG_PALLETE_9,
CONFIG_PALLETE_10,
CONFIG_PALLETE_11,
CONFIG_PALLETE_12,
CONFIG_PALLETE_13,
CONFIG_PALLETE_14,
CONFIG_PALLETE_15,
};
if (!colors_parsed) {
int i;
gdk_color_parse(CONFIG_FOREGROUND_COLOR, &fg_color);
gdk_color_parse(CONFIG_BACKGROUND_COLOR, &bg_color);
for (i = 0; i < CONFIG_PALETTE_SIZE; ++i)
gdk_color_parse(CONFIG_COLOR_PALETTE[i], &palette[i]);
colors_parsed = TRUE;
}
/* Set preferences */
vte_terminal_set_audible_bell(terminal, CONFIG_AUDIBLE_BELL);
vte_terminal_set_colors(terminal, &fg_color, &bg_color, palette, CONFIG_PALETTE_SIZE);
vte_terminal_set_cursor_blink_mode(terminal, CONFIG_CURSOR_BLINK_MODE);
vte_terminal_set_font_from_string(terminal, CONFIG_FONT_NAME);
vte_terminal_set_mouse_autohide(terminal, CONFIG_MOUSE_AUTOHIDE);
vte_terminal_set_scrollback_lines(terminal, CONFIG_SCROLLBACK_LINES);
vte_terminal_set_visible_bell(terminal, CONFIG_VISIBLE_BELL);
}
static void
setup_terminal(GtkWindow *window, VteTerminal *terminal)
{
char *argv[] = { vte_get_user_shell(), NULL };
GPid child_pid;
vte_terminal_fork_command_full(terminal,
VTE_PTY_DEFAULT,
NULL, /* wd; NULL for cwd */
argv, /* the program to fork into and its args */
NULL, /* env vars */
G_SPAWN_DO_NOT_REAP_CHILD,
NULL, /* setup func */
NULL, /* custom data to setup func */
&child_pid,
NULL); /* TODO: error handling */
g_signal_connect(terminal, "child-exited", G_CALLBACK(terminal_child_exited_callback), window);
g_signal_connect(terminal, "key-press-event", G_CALLBACK(key_press_callback), window);
g_signal_connect(terminal, "window-title-changed", G_CALLBACK(terminal_window_title_changed_callback), window);
set_preferences(terminal);
}
static int
num_open_windows()
{
return window_count;
}
static void
new_window()
{
GtkWindow *window = (GtkWindow *) gtk_window_new(GTK_WINDOW_TOPLEVEL);
VteTerminal *terminal = (VteTerminal *) vte_terminal_new();
setup_terminal(window, terminal);
GdkGeometry hints;
hints.base_width = vte_terminal_get_char_width(terminal);
hints.base_height = vte_terminal_get_char_height(terminal);
hints.min_width = hints.base_width * CONFIG_MIN_WIDTH;
hints.min_height = hints.base_height * CONFIG_MIN_HEIGHT;
hints.width_inc = hints.base_width;
hints.height_inc = hints.base_height;
gtk_window_set_geometry_hints(window, GTK_WIDGET(terminal), &hints, GDK_HINT_RESIZE_INC | GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE);
gtk_window_set_icon_name(window, "utilities-terminal");
g_signal_connect(window, "delete-event", G_CALLBACK(gtk_false), NULL);
g_signal_connect(window, "destroy", G_CALLBACK(window_destroy_callback), NULL);
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(terminal));
gtk_widget_show(GTK_WIDGET(terminal));
gtk_widget_show(GTK_WIDGET(window));
window_count++;
}
static void
wrn(const char *message)
{
fprintf(stderr, "%s: %s\n", PROGRAM_NAME, message);
}
static void
err(const char *message, int ecode)
{
wrn(message);
exit(ecode);
}
static int
conn_socket()
{
struct sockaddr_un sock_addr;
int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock_fd < 0)
err("Failed to create socket", EXIT_FAILURE);
memset(&sock_addr, 0, sizeof(sock_addr));
sock_addr.sun_family = AF_UNIX;
strncpy(sock_addr.sun_path, SOCKPATH, sizeof(sock_addr.sun_path) - 1);
if (connect(sock_fd, (struct sockaddr *) &sock_addr, sizeof(sock_addr)) < 0)
err("Failed to connect to daemon", EXIT_FAILURE);
return sock_fd;
}
static int
bind_socket()
{
struct sockaddr_un sock_addr;
memset(&sock_addr, 0, sizeof(sock_addr));
sock_addr.sun_family = AF_UNIX;
strncpy(sock_addr.sun_path, SOCKPATH, sizeof(sock_addr.sun_path) - 1);
/* Remove old socket */
if (!access(sock_addr.sun_path, F_OK) && unlink(sock_addr.sun_path) < 0)
err("Failed to remove socket file at " SOCKPATH, EXIT_FAILURE);
if (!access(CONFIG_SOCKDIR, F_OK) && rmdir(CONFIG_SOCKDIR) < 0)
err("Failed to remove socket directory at " CONFIG_SOCKDIR, EXIT_FAILURE);
/* Create a new directory */
if (mkdir(CONFIG_SOCKDIR, S_IRWXU) < 0)
err("Failed to create socket directory at " CONFIG_SOCKDIR, EXIT_FAILURE);
int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock_fd < 0)
err("Failed to create socket", EXIT_FAILURE);
if (bind(sock_fd, (struct sockaddr *) &sock_addr, sizeof(sock_addr)) < 0)
err("Failed to bind the socket to the file at " SOCKPATH, EXIT_FAILURE);
return sock_fd;
}
static void
cleanup_socket(int sock_fd)
{
if (close(sock_fd) < 0)
err("Failed to close socket", EXIT_FAILURE);
if (unlink(SOCKPATH) < 0)
err("Failed to remove socket file at " SOCKPATH, EXIT_FAILURE);
if (rmdir(CONFIG_SOCKDIR) < 0)
err("Failed to remove socket directory at " CONFIG_SOCKDIR, EXIT_FAILURE);
}
static TMessage
handle_message(TMessage msg)
{
switch (msg) {
case MSG_NEWWIN:
new_window();
return MSG_OK;
case MSG_EXIT:
if (num_open_windows() == 0) {
gtk_main_quit();
return MSG_OK;
}
return MSG_ERROR;
default:
return MSG_ERROR;
}
}
static gboolean
socket_g_io_watch(GIOChannel *source, GIOCondition condition, gpointer data)
{
int sock_fd = g_io_channel_unix_get_fd(source);
int client_fd = accept(sock_fd, NULL, NULL);
if (client_fd < 0) {
wrn("accept() failed");
return TRUE;
}
TMessage msg;
int len = recv(client_fd, &msg, sizeof(msg), 0);
if (len < 0) {
wrn("recv() failed");
goto socket_g_io_watch_cleanup;
}
msg = handle_message(msg);
write(client_fd, &msg, sizeof(msg));
socket_g_io_watch_cleanup:
close(client_fd);
return TRUE;
}
static void
usage()
{
puts("usage:");
puts("\t" PROGRAM_NAME " [ARG]");
puts("\nwhere ARG is one of:");
puts("\t" ARG_NEWWIN "\tRequests a new window; default if ommited");
puts("\t" ARG_EXIT "\tRequests daemon termination; only successful if there are no open windows");
puts("\t" ARG_DAEMON "\tRun in daemon mode");
}
static int
send_message(TMessage msg)
{
int sock_fd = conn_socket();
if (write(sock_fd, &msg, sizeof(msg)) < 0)
err("Failed to send data", EXIT_FAILURE);
if (recv(sock_fd, &msg, sizeof(msg), 0) < 0)
err("Failed to recv data", EXIT_FAILURE);
close(sock_fd);
return msg == MSG_OK ? EXIT_SUCCESS : EXIT_FAILURE;
}
static int
run_daemon(int *argc, char ***argv)
{
int sock_fd;
gtk_init(argc, argv);
sock_fd = bind_socket();
if (listen(sock_fd, SOMAXCONN) < 0)
err("Failed to listen to socket", EXIT_SUCCESS);
/* poll the socket in the GTK+ main loop */
GIOChannel *sock_chan = g_io_channel_unix_new(sock_fd);
g_io_add_watch(sock_chan, G_IO_IN, socket_g_io_watch, NULL);
gtk_main();
/* clean up */
cleanup_socket(sock_fd);
g_io_channel_unref(sock_chan);
return EXIT_SUCCESS;
}
int
main(int argc, char *argv[])
{
if (argc > 2) /* More than 1 arg means derp */
usage();
if (argc == 1 || strcmp(argv[1], ARG_NEWWIN) == 0)
return send_message(MSG_NEWWIN);
else if (strcmp(argv[1], ARG_EXIT) == 0)
return send_message(MSG_EXIT);
else if (strcmp(argv[1], ARG_DAEMON) == 0)
return run_daemon(&argc, &argv);
else /* derp */
usage();
return EXIT_FAILURE;
}