aboutsummaryrefslogtreecommitdiff
/*
 * 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 <http://www.gnu.org/licenses/>.
 */

#include <glib.h>
#include <gdk/gdk.h>
#include <gdk/gdkx.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
#include <vte/vte.h>

#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#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;
}