You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
14714 lines
406 KiB
14714 lines
406 KiB
/* |
|
* xC.c: a terminal-based IRC client |
|
* |
|
* Copyright (c) 2015 - 2021, Přemysl Eric Janouch <p@janouch.name> |
|
* |
|
* Permission to use, copy, modify, and/or distribute this software for any |
|
* purpose with or without fee is hereby granted. |
|
* |
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY |
|
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION |
|
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN |
|
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
|
* |
|
*/ |
|
|
|
// A table of all attributes we use for output |
|
#define ATTR_TABLE(XX) \ |
|
XX( PROMPT, prompt, Terminal attrs for the prompt ) \ |
|
XX( RESET, reset, String to reset terminal attributes ) \ |
|
XX( DATE_CHANGE, date_change, Terminal attrs for date change ) \ |
|
XX( READ_MARKER, read_marker, Terminal attrs for the read marker ) \ |
|
XX( WARNING, warning, Terminal attrs for warnings ) \ |
|
XX( ERROR, error, Terminal attrs for errors ) \ |
|
XX( EXTERNAL, external, Terminal attrs for external lines ) \ |
|
XX( TIMESTAMP, timestamp, Terminal attrs for timestamps ) \ |
|
XX( HIGHLIGHT, highlight, Terminal attrs for highlights ) \ |
|
XX( ACTION, action, Terminal attrs for user actions ) \ |
|
XX( USERHOST, userhost, Terminal attrs for user@host ) \ |
|
XX( JOIN, join, Terminal attrs for joins ) \ |
|
XX( PART, part, Terminal attrs for parts ) |
|
|
|
enum |
|
{ |
|
#define XX(x, y, z) ATTR_ ## x, |
|
ATTR_TABLE (XX) |
|
#undef XX |
|
ATTR_COUNT |
|
}; |
|
|
|
// User data for logger functions to enable formatted logging |
|
#define print_fatal_data ((void *) ATTR_ERROR) |
|
#define print_error_data ((void *) ATTR_ERROR) |
|
#define print_warning_data ((void *) ATTR_WARNING) |
|
|
|
#include "config.h" |
|
#define PROGRAM_NAME "xC" |
|
|
|
#include "common.c" |
|
#include "xD-replies.c" |
|
|
|
#include <math.h> |
|
#include <langinfo.h> |
|
#include <locale.h> |
|
#include <pwd.h> |
|
#include <sys/utsname.h> |
|
#include <wchar.h> |
|
|
|
#include <termios.h> |
|
#include <sys/ioctl.h> |
|
|
|
#include <curses.h> |
|
#include <term.h> |
|
|
|
// Literally cancer |
|
#undef lines |
|
#undef columns |
|
|
|
#include <ffi.h> |
|
|
|
#ifdef HAVE_LUA |
|
#include <lua.h> |
|
#include <lualib.h> |
|
#include <lauxlib.h> |
|
#endif // HAVE_LUA |
|
|
|
// --- Terminal information ---------------------------------------------------- |
|
|
|
static struct |
|
{ |
|
bool initialized; ///< Terminal is available |
|
bool stdout_is_tty; ///< `stdout' is a terminal |
|
bool stderr_is_tty; ///< `stderr' is a terminal |
|
|
|
struct termios termios; ///< Terminal attributes |
|
char *color_set_fg[256]; ///< Codes to set the foreground colour |
|
char *color_set_bg[256]; ///< Codes to set the background colour |
|
|
|
int lines; ///< Number of lines |
|
int columns; ///< Number of columns |
|
} |
|
g_terminal; |
|
|
|
static void |
|
update_screen_size (void) |
|
{ |
|
#ifdef TIOCGWINSZ |
|
if (!g_terminal.stdout_is_tty) |
|
return; |
|
|
|
struct winsize size; |
|
if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size)) |
|
{ |
|
char *row = getenv ("LINES"); |
|
char *col = getenv ("COLUMNS"); |
|
unsigned long tmp; |
|
g_terminal.lines = |
|
(row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row; |
|
g_terminal.columns = |
|
(col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col; |
|
} |
|
#endif // TIOCGWINSZ |
|
} |
|
|
|
static bool |
|
init_terminal (void) |
|
{ |
|
int tty_fd = -1; |
|
if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO))) |
|
tty_fd = STDERR_FILENO; |
|
if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO))) |
|
tty_fd = STDOUT_FILENO; |
|
|
|
int err; |
|
if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR) |
|
return false; |
|
|
|
// Make sure all terminal features used by us are supported |
|
if (!set_a_foreground || !set_a_background |
|
|| !enter_bold_mode || !exit_attribute_mode |
|
|| tcgetattr (tty_fd, &g_terminal.termios)) |
|
{ |
|
del_curterm (cur_term); |
|
return false; |
|
} |
|
|
|
// Make sure newlines are output correctly |
|
g_terminal.termios.c_oflag |= ONLCR; |
|
(void) tcsetattr (tty_fd, TCSADRAIN, &g_terminal.termios); |
|
|
|
g_terminal.lines = tigetnum ("lines"); |
|
g_terminal.columns = tigetnum ("cols"); |
|
update_screen_size (); |
|
|
|
int max = MIN (256, max_colors); |
|
for (int i = 0; i < max; i++) |
|
{ |
|
g_terminal.color_set_fg[i] = xstrdup (tparm (set_a_foreground, |
|
i, 0, 0, 0, 0, 0, 0, 0, 0)); |
|
g_terminal.color_set_bg[i] = xstrdup (tparm (set_a_background, |
|
i, 0, 0, 0, 0, 0, 0, 0, 0)); |
|
} |
|
return g_terminal.initialized = true; |
|
} |
|
|
|
static void |
|
free_terminal (void) |
|
{ |
|
if (!g_terminal.initialized) |
|
return; |
|
|
|
for (int i = 0; i < 256; i++) |
|
{ |
|
free (g_terminal.color_set_fg[i]); |
|
free (g_terminal.color_set_bg[i]); |
|
} |
|
del_curterm (cur_term); |
|
} |
|
|
|
// --- User interface ---------------------------------------------------------- |
|
|
|
// I'm not sure which one of these backends is worse: whether it's GNU Readline |
|
// or BSD Editline. They both have their own annoying problems. We use lots |
|
// of hacks to get the results we want and need. |
|
// |
|
// The abstraction is a necessary evil. It's still not 100%, though. |
|
|
|
/// Some arbitrary limit for the history |
|
#define HISTORY_LIMIT 10000 |
|
|
|
/// Characters that separate words |
|
#define WORD_BREAKING_CHARS " \f\n\r\t\v" |
|
|
|
struct input |
|
{ |
|
struct input_vtable *vtable; ///< Virtual methods |
|
void (*add_functions) (void *); ///< Define functions for binding |
|
void *user_data; ///< User data for callbacks |
|
}; |
|
|
|
typedef void *input_buffer_t; ///< Pointer alias for input buffers |
|
|
|
/// Named function that can be bound to a sequence of characters |
|
typedef bool (*input_fn) (int count, int key, void *user_data); |
|
|
|
// A little bit better than tons of forwarder functions in our case |
|
#define CALL(self, name) ((self)->vtable->name ((self))) |
|
#define CALL_(self, name, ...) ((self)->vtable->name ((self), __VA_ARGS__)) |
|
|
|
struct input_vtable |
|
{ |
|
/// Start the interface under the given program name |
|
void (*start) (void *input, const char *program_name); |
|
/// Stop the interface |
|
void (*stop) (void *input); |
|
/// Prepare or unprepare terminal for our needs |
|
void (*prepare) (void *input, bool enabled); |
|
/// Destroy the object |
|
void (*destroy) (void *input); |
|
|
|
/// Hide the prompt if shown |
|
void (*hide) (void *input); |
|
/// Show the prompt if hidden |
|
void (*show) (void *input); |
|
/// Retrieve current prompt string |
|
const char *(*get_prompt) (void *input); |
|
/// Change the prompt string; takes ownership |
|
void (*set_prompt) (void *input, char *prompt); |
|
/// Ring the terminal bell |
|
void (*ding) (void *input); |
|
|
|
/// Create a new input buffer |
|
input_buffer_t (*buffer_new) (void *input); |
|
/// Destroy an input buffer |
|
void (*buffer_destroy) (void *input, input_buffer_t buffer); |
|
/// Switch to a different input buffer |
|
void (*buffer_switch) (void *input, input_buffer_t buffer); |
|
|
|
/// Register a function that can be bound to character sequences |
|
void (*register_fn) (void *input, |
|
const char *name, const char *help, input_fn fn, void *user_data); |
|
/// Bind an arbitrary sequence of characters to the given named function |
|
void (*bind) (void *input, const char *seq, const char *fn); |
|
/// Bind Ctrl+key to the given named function |
|
void (*bind_control) (void *input, char key, const char *fn); |
|
/// Bind Alt+key to the given named function |
|
void (*bind_meta) (void *input, char key, const char *fn); |
|
|
|
/// Get the current line input and position within |
|
char *(*get_line) (void *input, int *position); |
|
/// Clear the current line input |
|
void (*clear_line) (void *input); |
|
/// Insert text at current position |
|
bool (*insert) (void *input, const char *text); |
|
|
|
/// Handle terminal resize |
|
void (*on_tty_resized) (void *input); |
|
/// Handle terminal input |
|
void (*on_tty_readable) (void *input); |
|
}; |
|
|
|
#define INPUT_VTABLE(XX) \ |
|
XX (start) XX (stop) XX (prepare) XX (destroy) \ |
|
XX (hide) XX (show) XX (get_prompt) XX (set_prompt) XX (ding) \ |
|
XX (buffer_new) XX (buffer_destroy) XX (buffer_switch) \ |
|
XX (register_fn) XX (bind) XX (bind_control) XX (bind_meta) \ |
|
XX (get_line) XX (clear_line) XX (insert) \ |
|
XX (on_tty_resized) XX (on_tty_readable) |
|
|
|
// --- GNU Readline ------------------------------------------------------------ |
|
|
|
#ifdef HAVE_READLINE |
|
|
|
#include <readline/readline.h> |
|
#include <readline/history.h> |
|
|
|
#define INPUT_START_IGNORE RL_PROMPT_START_IGNORE |
|
#define INPUT_END_IGNORE RL_PROMPT_END_IGNORE |
|
|
|
struct input_rl_fn |
|
{ |
|
ffi_closure closure; ///< Closure |
|
|
|
LIST_HEADER (struct input_rl_fn) |
|
input_fn callback; ///< Real callback |
|
void *user_data; ///< Real callback user data |
|
}; |
|
|
|
struct input_rl_buffer |
|
{ |
|
HISTORY_STATE *history; ///< Saved history state |
|
char *saved_line; ///< Saved line content |
|
int saved_point; ///< Saved cursor position |
|
int saved_mark; ///< Saved mark |
|
}; |
|
|
|
struct input_rl |
|
{ |
|
struct input super; ///< Parent class |
|
|
|
bool active; ///< Interface has been started |
|
char *prompt; ///< The prompt we use |
|
int prompt_shown; ///< Whether the prompt is shown now |
|
|
|
char *saved_line; ///< Saved line content |
|
int saved_point; ///< Saved cursor position |
|
int saved_mark; ///< Saved mark |
|
|
|
struct input_rl_fn *fns; ///< Named functions |
|
struct input_rl_buffer *current; ///< Current input buffer |
|
}; |
|
|
|
static void |
|
input_rl_ding (void *input) |
|
{ |
|
(void) input; |
|
rl_ding (); |
|
} |
|
|
|
static const char * |
|
input_rl_get_prompt (void *input) |
|
{ |
|
struct input_rl *self = input; |
|
return self->prompt; |
|
} |
|
|
|
static void |
|
input_rl_set_prompt (void *input, char *prompt) |
|
{ |
|
struct input_rl *self = input; |
|
cstr_set (&self->prompt, prompt); |
|
|
|
if (!self->active || self->prompt_shown <= 0) |
|
return; |
|
|
|
// First reset the prompt to work around a bug in readline |
|
rl_set_prompt (""); |
|
rl_redisplay (); |
|
|
|
rl_set_prompt (self->prompt); |
|
rl_redisplay (); |
|
} |
|
|
|
static void |
|
input_rl_clear_line (void *input) |
|
{ |
|
(void) input; |
|
rl_replace_line ("", false); |
|
rl_redisplay (); |
|
} |
|
|
|
static void |
|
input_rl__erase (struct input_rl *self) |
|
{ |
|
rl_set_prompt (""); |
|
input_rl_clear_line (self); |
|
} |
|
|
|
static bool |
|
input_rl_insert (void *input, const char *s) |
|
{ |
|
struct input_rl *self = input; |
|
rl_insert_text (s); |
|
if (self->prompt_shown > 0) |
|
rl_redisplay (); |
|
|
|
// GNU Readline, contrary to Editline, doesn't care about validity |
|
return true; |
|
} |
|
|
|
static char * |
|
input_rl_get_line (void *input, int *position) |
|
{ |
|
(void) input; |
|
if (position) *position = rl_point; |
|
return rl_copy_text (0, rl_end); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_rl_bind (void *input, const char *seq, const char *function_name) |
|
{ |
|
(void) input; |
|
rl_bind_keyseq (seq, rl_named_function (function_name)); |
|
} |
|
|
|
static void |
|
input_rl_bind_meta (void *input, char key, const char *function_name) |
|
{ |
|
// This one seems to actually work |
|
char keyseq[] = { '\\', 'e', key, 0 }; |
|
input_rl_bind (input, keyseq, function_name); |
|
#if 0 |
|
// While this one only fucks up UTF-8 |
|
// Tested with urxvt and xterm, on Debian Jessie/Arch, default settings |
|
// \M-<key> behaves exactly the same |
|
rl_bind_key (META (key), rl_named_function (function_name)); |
|
#endif |
|
} |
|
|
|
static void |
|
input_rl_bind_control (void *input, char key, const char *function_name) |
|
{ |
|
char keyseq[] = { '\\', 'C', '-', key, 0 }; |
|
input_rl_bind (input, keyseq, function_name); |
|
} |
|
|
|
static void |
|
input_rl__forward (ffi_cif *cif, void *ret, void **args, void *user_data) |
|
{ |
|
(void) cif; |
|
|
|
struct input_rl_fn *data = user_data; |
|
if (!data->callback |
|
(*(int *) args[0], UNMETA (*(int *) args[1]), data->user_data)) |
|
rl_ding (); |
|
*(int *) ret = 0; |
|
} |
|
|
|
static void |
|
input_rl_register_fn (void *input, |
|
const char *name, const char *help, input_fn callback, void *user_data) |
|
{ |
|
struct input_rl *self = input; |
|
(void) help; |
|
|
|
void *bound_fn = NULL; |
|
struct input_rl_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn); |
|
hard_assert (data); |
|
|
|
static ffi_cif cif; |
|
static ffi_type *args[2] = { &ffi_type_sint, &ffi_type_sint }; |
|
hard_assert (ffi_prep_cif |
|
(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_sint, args) == FFI_OK); |
|
|
|
data->prev = data->next = NULL; |
|
data->callback = callback; |
|
data->user_data = user_data; |
|
hard_assert (ffi_prep_closure_loc (&data->closure, |
|
&cif, input_rl__forward, data, bound_fn) == FFI_OK); |
|
|
|
rl_add_defun (name, (rl_command_func_t *) bound_fn, -1); |
|
LIST_PREPEND (self->fns, data); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static int app_readline_init (void); |
|
static void on_readline_input (char *line); |
|
static char **app_readline_completion (const char *text, int start, int end); |
|
|
|
static void |
|
input_rl_start (void *input, const char *program_name) |
|
{ |
|
struct input_rl *self = input; |
|
using_history (); |
|
// This can cause memory leaks, or maybe even a segfault. Funny, eh? |
|
stifle_history (HISTORY_LIMIT); |
|
|
|
const char *slash = strrchr (program_name, '/'); |
|
rl_readline_name = slash ? ++slash : program_name; |
|
rl_startup_hook = app_readline_init; |
|
rl_catch_sigwinch = false; |
|
rl_change_environment = false; |
|
|
|
rl_basic_word_break_characters = WORD_BREAKING_CHARS; |
|
rl_completer_word_break_characters = NULL; |
|
rl_attempted_completion_function = app_readline_completion; |
|
|
|
// We shouldn't produce any duplicates that the library would help us |
|
// autofilter, and we don't generally want alphabetic ordering at all |
|
rl_sort_completion_matches = false; |
|
|
|
hard_assert (self->prompt != NULL); |
|
// The inputrc is read before any callbacks are called, so we need to |
|
// register all functions that our user may want to map up front |
|
self->super.add_functions (self->super.user_data); |
|
rl_callback_handler_install (self->prompt, on_readline_input); |
|
|
|
self->prompt_shown = 1; |
|
self->active = true; |
|
} |
|
|
|
static void |
|
input_rl_stop (void *input) |
|
{ |
|
struct input_rl *self = input; |
|
if (self->prompt_shown > 0) |
|
input_rl__erase (self); |
|
|
|
// This is okay as long as we're not called from within readline |
|
rl_callback_handler_remove (); |
|
self->active = false; |
|
self->prompt_shown = false; |
|
} |
|
|
|
static void |
|
input_rl_prepare (void *input, bool enabled) |
|
{ |
|
(void) input; |
|
if (enabled) |
|
rl_prep_terminal (true); |
|
else |
|
rl_deprep_terminal (); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
// The following part shows you why it's not a good idea to use |
|
// GNU Readline for this kind of software. Or for anything else, really. |
|
|
|
static void |
|
input_rl__save_buffer (struct input_rl *self, struct input_rl_buffer *buffer) |
|
{ |
|
(void) self; |
|
|
|
buffer->history = history_get_history_state (); |
|
buffer->saved_line = rl_copy_text (0, rl_end); |
|
buffer->saved_point = rl_point; |
|
buffer->saved_mark = rl_mark; |
|
|
|
rl_replace_line ("", true); |
|
if (self->prompt_shown > 0) |
|
rl_redisplay (); |
|
} |
|
|
|
static void |
|
input_rl__restore_buffer (struct input_rl *self, struct input_rl_buffer *buffer) |
|
{ |
|
if (buffer->history) |
|
{ |
|
// history_get_history_state() just allocates a new HISTORY_STATE |
|
// and fills it with its current internal data. We don't need that |
|
// shell anymore after reviving it. |
|
history_set_history_state (buffer->history); |
|
free (buffer->history); |
|
buffer->history = NULL; |
|
} |
|
else |
|
{ |
|
// This should get us a clean history while keeping the flags. |
|
// Note that we've either saved the previous history entries, or we've |
|
// cleared them altogether, so there should be nothing to leak. |
|
HISTORY_STATE *state = history_get_history_state (); |
|
state->offset = state->length = state->size = 0; |
|
state->entries = NULL; |
|
history_set_history_state (state); |
|
free (state); |
|
} |
|
|
|
if (buffer->saved_line) |
|
{ |
|
rl_replace_line (buffer->saved_line, true); |
|
rl_point = buffer->saved_point; |
|
rl_mark = buffer->saved_mark; |
|
cstr_set (&buffer->saved_line, NULL); |
|
|
|
if (self->prompt_shown > 0) |
|
rl_redisplay (); |
|
} |
|
} |
|
|
|
static void |
|
input_rl_buffer_switch (void *input, input_buffer_t input_buffer) |
|
{ |
|
struct input_rl *self = input; |
|
struct input_rl_buffer *buffer = input_buffer; |
|
// There could possibly be occurences of the current undo list in some |
|
// history entry. We either need to free the undo list, or move it |
|
// somewhere else to load back later, as the buffer we're switching to |
|
// has its own history state. |
|
rl_free_undo_list (); |
|
|
|
// Save this buffer's history so that it's independent for each buffer |
|
if (self->current) |
|
input_rl__save_buffer (self, self->current); |
|
else |
|
// Just throw it away; there should always be an active buffer however |
|
#if RL_READLINE_VERSION >= 0x0603 |
|
rl_clear_history (); |
|
#else // RL_READLINE_VERSION < 0x0603 |
|
// At least something... this may leak undo entries |
|
clear_history (); |
|
#endif // RL_READLINE_VERSION < 0x0603 |
|
|
|
input_rl__restore_buffer (self, buffer); |
|
self->current = buffer; |
|
} |
|
|
|
static void |
|
input_rl__buffer_destroy_wo_history (struct input_rl_buffer *self) |
|
{ |
|
free (self->history); |
|
free (self->saved_line); |
|
free (self); |
|
} |
|
|
|
static void |
|
input_rl_buffer_destroy (void *input, input_buffer_t input_buffer) |
|
{ |
|
(void) input; |
|
struct input_rl_buffer *buffer = input_buffer; |
|
|
|
// rl_clear_history, being the only way I know of to get rid of the complete |
|
// history including attached data, is a pretty recent addition. *sigh* |
|
#if RL_READLINE_VERSION >= 0x0603 |
|
if (buffer->history) |
|
{ |
|
// See input_rl_buffer_switch() for why we need to do this BS |
|
rl_free_undo_list (); |
|
|
|
// This is probably the only way we can free the history fully |
|
HISTORY_STATE *state = history_get_history_state (); |
|
|
|
history_set_history_state (buffer->history); |
|
rl_clear_history (); |
|
// rl_clear_history just removes history entries, |
|
// we have to reclaim memory for their actual container ourselves |
|
free (buffer->history->entries); |
|
free (buffer->history); |
|
buffer->history = NULL; |
|
|
|
history_set_history_state (state); |
|
free (state); |
|
} |
|
#endif // RL_READLINE_VERSION |
|
|
|
input_rl__buffer_destroy_wo_history (buffer); |
|
} |
|
|
|
static input_buffer_t |
|
input_rl_buffer_new (void *input) |
|
{ |
|
(void) input; |
|
struct input_rl_buffer *self = xcalloc (1, sizeof *self); |
|
return self; |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
// Since {save,restore}_buffer() store history, we can't use them here like we |
|
// do with libedit, because then buffer_destroy() can free memory that's still |
|
// being used by readline. This situation is bound to happen on quit. |
|
|
|
static void |
|
input_rl__save (struct input_rl *self) |
|
{ |
|
hard_assert (!self->saved_line); |
|
|
|
self->saved_point = rl_point; |
|
self->saved_mark = rl_mark; |
|
self->saved_line = rl_copy_text (0, rl_end); |
|
} |
|
|
|
static void |
|
input_rl__restore (struct input_rl *self) |
|
{ |
|
hard_assert (self->saved_line); |
|
|
|
rl_set_prompt (self->prompt); |
|
rl_replace_line (self->saved_line, false); |
|
rl_point = self->saved_point; |
|
rl_mark = self->saved_mark; |
|
cstr_set (&self->saved_line, NULL); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_rl_hide (void *input) |
|
{ |
|
struct input_rl *self = input; |
|
if (!self->active || self->prompt_shown-- < 1) |
|
return; |
|
|
|
input_rl__save (self); |
|
input_rl__erase (self); |
|
} |
|
|
|
static void |
|
input_rl_show (void *input) |
|
{ |
|
struct input_rl *self = input; |
|
if (!self->active || ++self->prompt_shown < 1) |
|
return; |
|
|
|
input_rl__restore (self); |
|
rl_redisplay (); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_rl_on_tty_resized (void *input) |
|
{ |
|
(void) input; |
|
// This fucks up big time on terminals with automatic wrapping such as |
|
// rxvt-unicode or newer VTE when the current line overflows, however we |
|
// can't do much about that |
|
rl_resize_terminal (); |
|
} |
|
|
|
static void |
|
input_rl_on_tty_readable (void *input) |
|
{ |
|
(void) input; |
|
rl_callback_read_char (); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_rl_destroy (void *input) |
|
{ |
|
struct input_rl *self = input; |
|
free (self->saved_line); |
|
LIST_FOR_EACH (struct input_rl_fn, iter, self->fns) |
|
ffi_closure_free (iter); |
|
free (self->prompt); |
|
free (self); |
|
} |
|
|
|
#define XX(a) .a = input_rl_ ## a, |
|
static struct input_vtable input_rl_vtable = { INPUT_VTABLE (XX) }; |
|
#undef XX |
|
|
|
static struct input * |
|
input_rl_new (void) |
|
{ |
|
struct input_rl *self = xcalloc (1, sizeof *self); |
|
self->super.vtable = &input_rl_vtable; |
|
return &self->super; |
|
} |
|
|
|
#define input_new input_rl_new |
|
#endif // HAVE_READLINE |
|
|
|
// --- BSD Editline ------------------------------------------------------------ |
|
|
|
#ifdef HAVE_EDITLINE |
|
|
|
#include <histedit.h> |
|
|
|
#define INPUT_START_IGNORE '\x01' |
|
#define INPUT_END_IGNORE '\x01' |
|
|
|
struct input_el_fn |
|
{ |
|
ffi_closure closure; ///< Closure |
|
|
|
LIST_HEADER (struct input_el_fn) |
|
input_fn callback; ///< Real callback |
|
void *user_data; ///< Real callback user data |
|
|
|
wchar_t *name; ///< Function name |
|
wchar_t *help; ///< Function help |
|
}; |
|
|
|
struct input_el_buffer |
|
{ |
|
HistoryW *history; ///< The history object |
|
wchar_t *saved_line; ///< Saved line content |
|
int saved_len; ///< Length of the saved line |
|
int saved_point; ///< Saved cursor position |
|
}; |
|
|
|
struct input_el |
|
{ |
|
struct input super; ///< Parent class |
|
EditLine *editline; ///< The EditLine object |
|
|
|
bool active; ///< Are we a thing? |
|
char *prompt; ///< The prompt we use |
|
int prompt_shown; ///< Whether the prompt is shown now |
|
|
|
struct input_el_fn *fns; ///< Named functions |
|
struct input_el_buffer *current; ///< Current input buffer |
|
}; |
|
|
|
static void app_editline_init (struct input_el *self); |
|
|
|
static void |
|
input_el__redisplay (void *input) |
|
{ |
|
// See rl_redisplay(), however NetBSD editline's map.c v1.54 breaks VREPRINT |
|
// so we bind redisplay somewhere else in app_editline_init() |
|
struct input_el *self = input; |
|
char x[] = { 'q' & 31, 0 }; |
|
el_push (self->editline, x); |
|
|
|
// We have to do this or it gets stuck and nothing is done |
|
int count = 0; |
|
(void) el_wgets (self->editline, &count); |
|
} |
|
|
|
static char * |
|
input_el__make_prompt (EditLine *editline) |
|
{ |
|
struct input_el *self; |
|
el_get (editline, EL_CLIENTDATA, &self); |
|
if (!self->prompt) |
|
return ""; |
|
return self->prompt; |
|
} |
|
|
|
static char * |
|
input_el__make_empty_prompt (EditLine *editline) |
|
{ |
|
(void) editline; |
|
return ""; |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_el_ding (void *input) |
|
{ |
|
// XXX: this isn't probably very portable; |
|
// we could use "bell" from terminfo but that creates a dependency |
|
(void) input; |
|
write (STDOUT_FILENO, "\a", 1); |
|
} |
|
|
|
static const char * |
|
input_el_get_prompt (void *input) |
|
{ |
|
struct input_el *self = input; |
|
return self->prompt; |
|
} |
|
|
|
static void |
|
input_el_set_prompt (void *input, char *prompt) |
|
{ |
|
struct input_el *self = input; |
|
cstr_set (&self->prompt, prompt); |
|
|
|
if (self->prompt_shown > 0) |
|
input_el__redisplay (self); |
|
} |
|
|
|
static void |
|
input_el_clear_line (void *input) |
|
{ |
|
struct input_el *self = input; |
|
const LineInfoW *info = el_wline (self->editline); |
|
int len = info->lastchar - info->buffer; |
|
int point = info->cursor - info->buffer; |
|
el_cursor (self->editline, len - point); |
|
el_wdeletestr (self->editline, len); |
|
input_el__redisplay (self); |
|
} |
|
|
|
static void |
|
input_el__erase (struct input_el *self) |
|
{ |
|
el_set (self->editline, EL_PROMPT, input_el__make_empty_prompt); |
|
input_el_clear_line (self); |
|
} |
|
|
|
static bool |
|
input_el_insert (void *input, const char *s) |
|
{ |
|
struct input_el *self = input; |
|
bool success = !*s || !el_insertstr (self->editline, s); |
|
if (self->prompt_shown > 0) |
|
input_el__redisplay (self); |
|
return success; |
|
} |
|
|
|
static char * |
|
input_el_get_line (void *input, int *position) |
|
{ |
|
struct input_el *self = input; |
|
const LineInfo *info = el_line (self->editline); |
|
int point = info->cursor - info->buffer; |
|
if (position) *position = point; |
|
return xstrndup (info->buffer, info->lastchar - info->buffer); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_el_bind (void *input, const char *seq, const char *function_name) |
|
{ |
|
struct input_el *self = input; |
|
el_set (self->editline, EL_BIND, seq, function_name, NULL); |
|
} |
|
|
|
static void |
|
input_el_bind_meta (void *input, char key, const char *function_name) |
|
{ |
|
char keyseq[] = { 'M', '-', key, 0 }; |
|
input_el_bind (input, keyseq, function_name); |
|
} |
|
|
|
static void |
|
input_el_bind_control (void *input, char key, const char *function_name) |
|
{ |
|
char keyseq[] = { '^', key, 0 }; |
|
input_el_bind (input, keyseq, function_name); |
|
} |
|
|
|
static void |
|
input_el__forward (ffi_cif *cif, void *ret, void **args, void *user_data) |
|
{ |
|
(void) cif; |
|
|
|
struct input_el_fn *data = user_data; |
|
*(unsigned char *) ret = data->callback |
|
(1, *(int *) args[1], data->user_data) ? CC_NORM : CC_ERROR; |
|
} |
|
|
|
static wchar_t * |
|
ascii_to_wide (const char *ascii) |
|
{ |
|
size_t len = strlen (ascii) + 1; |
|
wchar_t *wide = xcalloc (sizeof *wide, len); |
|
while (len--) |
|
hard_assert ((wide[len] = (unsigned char) ascii[len]) < 0x80); |
|
return wide; |
|
} |
|
|
|
static void |
|
input_el_register_fn (void *input, |
|
const char *name, const char *help, input_fn callback, void *user_data) |
|
{ |
|
void *bound_fn = NULL; |
|
struct input_el_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn); |
|
hard_assert (data); |
|
|
|
static ffi_cif cif; |
|
static ffi_type *args[2] = { &ffi_type_pointer, &ffi_type_sint }; |
|
hard_assert (ffi_prep_cif |
|
(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_uchar, args) == FFI_OK); |
|
|
|
data->user_data = user_data; |
|
data->callback = callback; |
|
data->name = ascii_to_wide (name); |
|
data->help = ascii_to_wide (help); |
|
hard_assert (ffi_prep_closure_loc (&data->closure, |
|
&cif, input_el__forward, data, bound_fn) == FFI_OK); |
|
|
|
struct input_el *self = input; |
|
el_wset (self->editline, EL_ADDFN, data->name, data->help, bound_fn); |
|
LIST_PREPEND (self->fns, data); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_el_start (void *input, const char *program_name) |
|
{ |
|
struct input_el *self = input; |
|
self->editline = el_init (program_name, stdin, stdout, stderr); |
|
el_set (self->editline, EL_CLIENTDATA, self); |
|
el_set (self->editline, EL_PROMPT_ESC, |
|
input_el__make_prompt, INPUT_START_IGNORE); |
|
el_set (self->editline, EL_SIGNAL, false); |
|
el_set (self->editline, EL_UNBUFFERED, true); |
|
el_set (self->editline, EL_EDITOR, "emacs"); |
|
|
|
app_editline_init (self); |
|
|
|
self->prompt_shown = 1; |
|
self->active = true; |
|
} |
|
|
|
static void |
|
input_el_stop (void *input) |
|
{ |
|
struct input_el *self = input; |
|
if (self->prompt_shown > 0) |
|
input_el__erase (self); |
|
|
|
el_end (self->editline); |
|
self->editline = NULL; |
|
self->active = false; |
|
self->prompt_shown = false; |
|
} |
|
|
|
static void |
|
input_el_prepare (void *input, bool enabled) |
|
{ |
|
struct input_el *self = input; |
|
el_set (self->editline, EL_PREP_TERM, enabled); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_el__save_buffer (struct input_el *self, struct input_el_buffer *buffer) |
|
{ |
|
const LineInfoW *info = el_wline (self->editline); |
|
int len = info->lastchar - info->buffer; |
|
int point = info->cursor - info->buffer; |
|
|
|
wchar_t *line = calloc (sizeof *info->buffer, len + 1); |
|
memcpy (line, info->buffer, sizeof *info->buffer * len); |
|
el_cursor (self->editline, len - point); |
|
el_wdeletestr (self->editline, len); |
|
|
|
buffer->saved_line = line; |
|
buffer->saved_point = point; |
|
buffer->saved_len = len; |
|
} |
|
|
|
static void |
|
input_el__save (struct input_el *self) |
|
{ |
|
if (self->current) |
|
input_el__save_buffer (self, self->current); |
|
} |
|
|
|
static void |
|
input_el__restore_buffer (struct input_el *self, struct input_el_buffer *buffer) |
|
{ |
|
if (buffer->saved_line) |
|
{ |
|
el_winsertstr (self->editline, buffer->saved_line); |
|
el_cursor (self->editline, |
|
-(buffer->saved_len - buffer->saved_point)); |
|
free (buffer->saved_line); |
|
buffer->saved_line = NULL; |
|
} |
|
} |
|
|
|
static void |
|
input_el__restore (struct input_el *self) |
|
{ |
|
if (self->current) |
|
input_el__restore_buffer (self, self->current); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_el_buffer_switch (void *input, input_buffer_t input_buffer) |
|
{ |
|
struct input_el *self = input; |
|
struct input_el_buffer *buffer = input_buffer; |
|
|
|
if (self->current) |
|
input_el__save_buffer (self, self->current); |
|
|
|
input_el__restore_buffer (self, buffer); |
|
el_wset (self->editline, EL_HIST, history, buffer->history); |
|
self->current = buffer; |
|
} |
|
|
|
static void |
|
input_el_buffer_destroy (void *input, input_buffer_t input_buffer) |
|
{ |
|
(void) input; |
|
struct input_el_buffer *buffer = input_buffer; |
|
|
|
history_wend (buffer->history); |
|
free (buffer->saved_line); |
|
free (buffer); |
|
} |
|
|
|
static input_buffer_t |
|
input_el_buffer_new (void *input) |
|
{ |
|
(void) input; |
|
struct input_el_buffer *self = xcalloc (1, sizeof *self); |
|
self->history = history_winit (); |
|
|
|
HistEventW ev; |
|
history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT); |
|
return self; |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_el_hide (void *input) |
|
{ |
|
struct input_el *self = input; |
|
if (!self->active || self->prompt_shown-- < 1) |
|
return; |
|
|
|
input_el__save (self); |
|
input_el__erase (self); |
|
} |
|
|
|
static void |
|
input_el_show (void *input) |
|
{ |
|
struct input_el *self = input; |
|
if (!self->active || ++self->prompt_shown < 1) |
|
return; |
|
|
|
input_el__restore (self); |
|
el_set (self->editline, |
|
EL_PROMPT_ESC, input_el__make_prompt, INPUT_START_IGNORE); |
|
input_el__redisplay (self); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_el_on_tty_resized (void *input) |
|
{ |
|
struct input_el *self = input; |
|
el_resize (self->editline); |
|
} |
|
|
|
static void |
|
input_el_on_tty_readable (void *input) |
|
{ |
|
// We bind the return key to process it how we need to |
|
struct input_el *self = input; |
|
|
|
// el_gets() with EL_UNBUFFERED doesn't work with UTF-8, |
|
// we must use the wide-character interface |
|
int count = 0; |
|
const wchar_t *buf = el_wgets (self->editline, &count); |
|
if (!buf || count-- <= 0) |
|
return; |
|
|
|
if (count == 0 && buf[0] == ('D' - 0x40) /* hardcoded VEOF in editline */) |
|
{ |
|
el_deletestr (self->editline, 1); |
|
input_el__redisplay (self); |
|
input_el_ding (self); |
|
} |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static void |
|
input_el_destroy (void *input) |
|
{ |
|
struct input_el *self = input; |
|
LIST_FOR_EACH (struct input_el_fn, iter, self->fns) |
|
{ |
|
free (iter->name); |
|
free (iter->help); |
|
ffi_closure_free (iter); |
|
} |
|
free (self->prompt); |
|
free (self); |
|
} |
|
|
|
#define XX(a) .a = input_el_ ## a, |
|
static struct input_vtable input_el_vtable = { INPUT_VTABLE (XX) }; |
|
#undef XX |
|
|
|
static struct input * |
|
input_el_new (void) |
|
{ |
|
struct input_el *self = xcalloc (1, sizeof *self); |
|
self->super.vtable = &input_el_vtable; |
|
return &self->super; |
|
} |
|
|
|
#define input_new input_el_new |
|
#endif // HAVE_EDITLINE |
|
|
|
// --- Application data -------------------------------------------------------- |
|
|
|
// All text stored in our data structures is encoded in UTF-8. Or at least |
|
// should be--our only ways of retrieving strings are: via the command line |
|
// (converted from locale, no room for errors), via the configuration file |
|
// (restrictive ASCII grammar for bare words and an internal check for strings), |
|
// and via plugins (meticulously validated). |
|
// |
|
// The only exception is IRC identifiers. |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
// We need a few reference countable objects with support for both strong |
|
// and weak references (mainly used for scripted plugins). |
|
// |
|
// Beware that if you don't own the object, you will most probably want |
|
// to keep the weak reference link so that you can get rid of it later. |
|
// Also note that you have to make sure the user_data don't leak resources. |
|
// |
|
// Having a callback is more versatile than just nulling out a pointer. |
|
|
|
/// Callback just before a reference counted object is destroyed |
|
typedef void (*destroy_cb_fn) (void *object, void *user_data); |
|
|
|
struct weak_ref_link |
|
{ |
|
LIST_HEADER (struct weak_ref_link) |
|
|
|
destroy_cb_fn on_destroy; ///< Called when object is destroyed |
|
void *user_data; ///< User data |
|
}; |
|
|
|
static struct weak_ref_link * |
|
weak_ref (struct weak_ref_link **list, destroy_cb_fn cb, void *user_data) |
|
{ |
|
struct weak_ref_link *link = xcalloc (1, sizeof *link); |
|
link->on_destroy = cb; |
|
link->user_data = user_data; |
|
LIST_PREPEND (*list, link); |
|
return link; |
|
} |
|
|
|
static void |
|
weak_unref (struct weak_ref_link **list, struct weak_ref_link **link) |
|
{ |
|
if (*link) |
|
LIST_UNLINK (*list, *link); |
|
free (*link); |
|
*link = NULL; |
|
} |
|
|
|
#define REF_COUNTABLE_HEADER \ |
|
size_t ref_count; /**< Reference count */ \ |
|
struct weak_ref_link *weak_refs; /**< To remove any weak references */ |
|
|
|
#define REF_COUNTABLE_METHODS(name) \ |
|
static struct name * \ |
|
name ## _ref (struct name *self) \ |
|
{ \ |
|
self->ref_count++; \ |
|
return self; \ |
|
} \ |
|
\ |
|
static void \ |
|
name ## _unref (struct name *self) \ |
|
{ \ |
|
if (--self->ref_count) \ |
|
return; \ |
|
LIST_FOR_EACH (struct weak_ref_link, iter, self->weak_refs) \ |
|
{ \ |
|
iter->on_destroy (self, iter->user_data); \ |
|
free (iter); \ |
|
} \ |
|
name ## _destroy (self); \ |
|
} \ |
|
\ |
|
static struct weak_ref_link * \ |
|
name ## _weak_ref (struct name *self, destroy_cb_fn cb, void *user_data) \ |
|
{ return weak_ref (&self->weak_refs, cb, user_data); } \ |
|
\ |
|
static void \ |
|
name ## _weak_unref (struct name *self, struct weak_ref_link **link) \ |
|
{ weak_unref (&self->weak_refs, link); } |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
// Simple introspection framework to simplify exporting stuff to Lua, since |
|
// there is a lot of it. While not fully automated, at least we have control |
|
// over which fields are exported. |
|
|
|
enum ispect_type |
|
{ |
|
ISPECT_BOOL, ISPECT_INT, ISPECT_UINT, ISPECT_SIZE, ISPECT_STRING, |
|
|
|
ISPECT_STR, ///< "struct str" |
|
ISPECT_STR_MAP, ///< "struct str_map" |
|
ISPECT_REF ///< Weakly referenced object |
|
}; |
|
|
|
struct ispect_field |
|
{ |
|
const char *name; ///< Name of the field |
|
size_t offset; ///< Offset in the structure |
|
enum ispect_type type; ///< Type of the field |
|
|
|
enum ispect_type subtype; ///< STR_MAP subtype |
|
struct ispect_field *fields; ///< REF target fields |
|
bool is_list; ///< REF target is a list |
|
}; |
|
|
|
#define ISPECT(object, field, type) \ |
|
{ #field, offsetof (struct object, field), ISPECT_##type, 0, NULL, false }, |
|
#define ISPECT_REF(object, field, is_list, ref_type) \ |
|
{ #field, offsetof (struct object, field), ISPECT_REF, 0, \ |
|
g_##ref_type##_ispect, is_list }, |
|
#define ISPECT_MAP(object, field, subtype) \ |
|
{ #field, offsetof (struct object, field), ISPECT_STR_MAP, \ |
|
ISPECT_##subtype, NULL, false }, |
|
#define ISPECT_MAP_REF(object, field, is_list, ref_type) \ |
|
{ #field, offsetof (struct object, field), ISPECT_STR_MAP, \ |
|
ISPECT_REF, g_##ref_type##_ispect, is_list }, |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
struct user_channel |
|
{ |
|
LIST_HEADER (struct user_channel) |
|
|
|
struct channel *channel; ///< Reference to channel |
|
}; |
|
|
|
static struct user_channel * |
|
user_channel_new (struct channel *channel) |
|
{ |
|
struct user_channel *self = xcalloc (1, sizeof *self); |
|
self->channel = channel; |
|
return self; |
|
} |
|
|
|
static void |
|
user_channel_destroy (struct user_channel *self) |
|
{ |
|
// The "channel" reference is weak and this object should get |
|
// destroyed whenever the user stops being in the channel. |
|
free (self); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
// We keep references to user information in channels and buffers, |
|
// and weak references in the name lookup table. |
|
|
|
struct user |
|
{ |
|
REF_COUNTABLE_HEADER |
|
|
|
char *nickname; ///< Literal nickname |
|
bool away; ///< User is away |
|
|
|
struct user_channel *channels; ///< Channels the user is on (with us) |
|
}; |
|
|
|
static struct ispect_field g_user_ispect[] = |
|
{ |
|
ISPECT( user, nickname, STRING ) |
|
ISPECT( user, away, BOOL ) |
|
{} |
|
}; |
|
|
|
static struct user * |
|
user_new (char *nickname) |
|
{ |
|
struct user *self = xcalloc (1, sizeof *self); |
|
self->ref_count = 1; |
|
self->nickname = nickname; |
|
return self; |
|
} |
|
|
|
static void |
|
user_destroy (struct user *self) |
|
{ |
|
free (self->nickname); |
|
LIST_FOR_EACH (struct user_channel, iter, self->channels) |
|
user_channel_destroy (iter); |
|
free (self); |
|
} |
|
|
|
REF_COUNTABLE_METHODS (user) |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
struct channel_user |
|
{ |
|
LIST_HEADER (struct channel_user) |
|
|
|
struct user *user; ///< Reference to user |
|
char *prefixes; ///< Ordered @+... characters |
|
}; |
|
|
|
static struct channel_user * |
|
channel_user_new (struct user *user, const char *prefixes) |
|
{ |
|
struct channel_user *self = xcalloc (1, sizeof *self); |
|
self->user = user; |
|
self->prefixes = xstrdup (prefixes); |
|
return self; |
|
} |
|
|
|
static void |
|
channel_user_destroy (struct channel_user *self) |
|
{ |
|
user_unref (self->user); |
|
free (self->prefixes); |
|
free (self); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
// We keep references to channels in their buffers, |
|
// and weak references in their users and the name lookup table. |
|
|
|
struct channel |
|
{ |
|
REF_COUNTABLE_HEADER |
|
|
|
struct server *s; ///< Server |
|
|
|
char *name; ///< Channel name |
|
char *topic; ///< Channel topic |
|
|
|
// XXX: write something like an ordered set of characters object? |
|
struct str no_param_modes; ///< No parameter channel modes |
|
struct str_map param_modes; ///< Parametrized channel modes |
|
|
|
struct channel_user *users; ///< Channel users |
|
struct strv names_buf; ///< Buffer for RPL_NAMREPLY |
|
size_t users_len; ///< User count |
|
|
|
bool left_manually; ///< Don't rejoin on reconnect |
|
bool show_names_after_who; ///< RPL_ENDOFWHO delays RPL_ENDOFNAMES |
|
}; |
|
|
|
static struct ispect_field g_channel_ispect[] = |
|
{ |
|
ISPECT( channel, name, STRING ) |
|
ISPECT( channel, topic, STRING ) |
|
ISPECT( channel, no_param_modes, STR ) |
|
ISPECT_MAP( channel, param_modes, STRING ) |
|
ISPECT( channel, users_len, SIZE ) |
|
ISPECT( channel, left_manually, BOOL ) |
|
{} |
|
}; |
|
|
|
static struct channel * |
|
channel_new (struct server *s, char *name) |
|
{ |
|
struct channel *self = xcalloc (1, sizeof *self); |
|
self->ref_count = 1; |
|
self->s = s; |
|
self->name = name; |
|
self->no_param_modes = str_make (); |
|
self->param_modes = str_map_make (free); |
|
self->names_buf = strv_make (); |
|
return self; |
|
} |
|
|
|
static void |
|
channel_destroy (struct channel *self) |
|
{ |
|
free (self->name); |
|
free (self->topic); |
|
str_free (&self->no_param_modes); |
|
str_map_free (&self->param_modes); |
|
// Owner has to make sure we have no users by now |
|
hard_assert (!self->users && !self->users_len); |
|
strv_free (&self->names_buf); |
|
free (self); |
|
} |
|
|
|
REF_COUNTABLE_METHODS (channel) |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
enum formatter_item_type |
|
{ |
|
FORMATTER_ITEM_END, ///< Sentinel value for arrays |
|
FORMATTER_ITEM_TEXT, ///< Text |
|
FORMATTER_ITEM_ATTR, ///< Formatting attributes |
|
FORMATTER_ITEM_FG_COLOR, ///< Foreground colour |
|
FORMATTER_ITEM_BG_COLOR, ///< Background colour |
|
FORMATTER_ITEM_SIMPLE, ///< Toggle IRC formatting |
|
FORMATTER_ITEM_IGNORE_ATTR ///< Un/set attribute ignoration |
|
}; |
|
|
|
struct formatter_item |
|
{ |
|
enum formatter_item_type type : 16; ///< Type of this item |
|
int attribute : 16; ///< Attribute ID |
|
int color; ///< Colour |
|
char *text; ///< String |
|
}; |
|
|
|
static void |
|
formatter_item_free (struct formatter_item *self) |
|
{ |
|
free (self->text); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
struct formatter |
|
{ |
|
struct app_context *ctx; ///< Application context |
|
struct server *s; ///< Server |
|
|
|
struct formatter_item *items; ///< Items |
|
size_t items_len; ///< Items used |
|
size_t items_alloc; ///< Items allocated |
|
}; |
|
|
|
static struct formatter |
|
formatter_make (struct app_context *ctx, struct server *s) |
|
{ |
|
struct formatter self = { .ctx = ctx, .s = s }; |
|
self.items = xcalloc (sizeof *self.items, (self.items_alloc = 16)); |
|
return self; |
|
} |
|
|
|
static void |
|
formatter_free (struct formatter *self) |
|
{ |
|
for (size_t i = 0; i < self->items_len; i++) |
|
formatter_item_free (&self->items[i]); |
|
free (self->items); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
enum buffer_line_flags |
|
{ |
|
BUFFER_LINE_STATUS = 1 << 0, ///< Status message |
|
BUFFER_LINE_ERROR = 1 << 1, ///< Error message |
|
BUFFER_LINE_HIGHLIGHT = 1 << 2, ///< The user was highlighted by this |
|
BUFFER_LINE_SKIP_FILE = 1 << 3, ///< Don't log this to file |
|
BUFFER_LINE_INDENT = 1 << 4, ///< Just indent the line |
|
BUFFER_LINE_UNIMPORTANT = 1 << 5 ///< Joins, parts, similar spam |
|
}; |
|
|
|
struct buffer_line |
|
{ |
|
LIST_HEADER (struct buffer_line) |
|
|
|
int flags; ///< Flags |
|
time_t when; ///< Time of the event |
|
struct formatter_item items[]; ///< Line data |
|
}; |
|
|
|
/// Create a new buffer line stealing all data from the provided formatter |
|
struct buffer_line * |
|
buffer_line_new (struct formatter *f) |
|
{ |
|
// We make space for one more item that gets initialized to all zeros, |
|
// meaning FORMATTER_ITEM_END (because it's the first value in the enum) |
|
size_t items_size = f->items_len * sizeof *f->items; |
|
struct buffer_line *self = |
|
xcalloc (1, sizeof *self + items_size + sizeof *self->items); |
|
memcpy (self->items, f->items, items_size); |
|
|
|
// We've stolen pointers from the formatter, let's destroy it altogether |
|
free (f->items); |
|
memset (f, 0, sizeof *f); |
|
return self; |
|
} |
|
|
|
static void |
|
buffer_line_destroy (struct buffer_line *self) |
|
{ |
|
for (struct formatter_item *iter = self->items; iter->type; iter++) |
|
formatter_item_free (iter); |
|
free (self); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
enum buffer_type |
|
{ |
|
BUFFER_GLOBAL, ///< Global information |
|
BUFFER_SERVER, ///< Server-related messages |
|
BUFFER_CHANNEL, ///< Channels |
|
BUFFER_PM ///< Private messages (query) |
|
}; |
|
|
|
struct buffer |
|
{ |
|
LIST_HEADER (struct buffer) |
|
REF_COUNTABLE_HEADER |
|
|
|
enum buffer_type type; ///< Type of the buffer |
|
char *name; ///< The name of the buffer |
|
|
|
struct input *input; ///< API for "input_data" |
|
input_buffer_t input_data; ///< User interface data |
|
|
|
// Buffer contents: |
|
|
|
struct buffer_line *lines; ///< All lines in this buffer |
|
struct buffer_line *lines_tail; ///< The tail of buffer lines |
|
unsigned lines_count; ///< How many lines we have |
|
|
|
unsigned new_messages_count; ///< # messages since last left |
|
unsigned new_unimportant_count; ///< How much of that is unimportant |
|
bool highlighted; ///< We've been highlighted |
|
bool hide_unimportant; ///< Hide unimportant messages |
|
|
|
FILE *log_file; ///< Log file |
|
|
|
// Origin information: |
|
|
|
struct server *server; ///< Reference to server |
|
struct channel *channel; ///< Reference to channel |
|
struct user *user; ///< Reference to user |
|
}; |
|
|
|
static struct ispect_field g_server_ispect[]; |
|
static struct ispect_field g_buffer_ispect[] = |
|
{ |
|
ISPECT( buffer, name, STRING ) |
|
ISPECT( buffer, new_messages_count, UINT ) |
|
ISPECT( buffer, new_unimportant_count, UINT ) |
|
ISPECT( buffer, highlighted, BOOL ) |
|
ISPECT( buffer, hide_unimportant, BOOL ) |
|
ISPECT_REF( buffer, server, false, server ) |
|
ISPECT_REF( buffer, channel, false, channel ) |
|
ISPECT_REF( buffer, user, false, user ) |
|
{} |
|
}; |
|
|
|
static struct buffer * |
|
buffer_new (struct input *input, enum buffer_type type, char *name) |
|
{ |
|
struct buffer *self = xcalloc (1, sizeof *self); |
|
self->ref_count = 1; |
|
self->input = input; |
|
self->input_data = CALL (input, buffer_new); |
|
self->type = type; |
|
self->name = name; |
|
return self; |
|
} |
|
|
|
static void |
|
buffer_destroy (struct buffer *self) |
|
{ |
|
free (self->name); |
|
if (self->input_data) |
|
{ |
|
#ifdef HAVE_READLINE |
|
// FIXME: can't really free "history" contents from here, as we cannot |
|
// be sure that the user interface pointer is valid and usable |
|
input_rl__buffer_destroy_wo_history (self->input_data); |
|
#else // ! HAVE_READLINE |
|
CALL_ (self->input, buffer_destroy, self->input_data); |
|
#endif // ! HAVE_READLINE |
|
} |
|
LIST_FOR_EACH (struct buffer_line, iter, self->lines) |
|
buffer_line_destroy (iter); |
|
if (self->log_file) |
|
(void) fclose (self->log_file); |
|
if (self->user) |
|
user_unref (self->user); |
|
if (self->channel) |
|
channel_unref (self->channel); |
|
free (self); |
|
} |
|
|
|
REF_COUNTABLE_METHODS (buffer) |
|
#define buffer_ref do_not_use_dangerous |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
// The only real purpose of this is to abstract away TLS |
|
struct transport |
|
{ |
|
/// Initialize the transport |
|
bool (*init) (struct server *s, const char *hostname, struct error **e); |
|
/// Destroy the user data pointer |
|
void (*cleanup) (struct server *s); |
|
|
|
/// The underlying socket may have become readable, update `read_buffer' |
|
enum socket_io_result (*try_read) (struct server *s); |
|
/// The underlying socket may have become writeable, flush `write_buffer' |
|
enum socket_io_result (*try_write) (struct server *s); |
|
/// Return event mask to use in the poller |
|
int (*get_poll_events) (struct server *s); |
|
|
|
/// Called just before closing the connection from our side |
|
void (*in_before_shutdown) (struct server *s); |
|
}; |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
enum server_state |
|
{ |
|
IRC_DISCONNECTED, ///< Not connected |
|
IRC_CONNECTING, ///< Connecting to the server |
|
IRC_CONNECTED, ///< Trying to register |
|
IRC_REGISTERED, ///< We can chat now |
|
IRC_CLOSING, ///< Flushing output before shutdown |
|
IRC_HALF_CLOSED ///< Connection shutdown from our side |
|
}; |
|
|
|
/// Convert an IRC identifier character to lower-case |
|
typedef int (*irc_tolower_fn) (int); |
|
|
|
/// Key conversion function for hashmap lookups |
|
typedef size_t (*irc_strxfrm_fn) (char *, const char *, size_t); |
|
|
|
struct server |
|
{ |
|
REF_COUNTABLE_HEADER |
|
struct app_context *ctx; ///< Application context |
|
|
|
char *name; ///< Server identifier |
|
struct buffer *buffer; ///< The buffer for this server |
|
struct config_item *config; ///< Configuration root |
|
|
|
// Connection: |
|
|
|
enum server_state state; ///< Connection state |
|
struct connector *connector; ///< Connection establisher |
|
struct socks_connector *socks_conn; ///< SOCKS connection establisher |
|
unsigned reconnect_attempt; ///< Number of reconnect attempt |
|
bool manual_disconnect; ///< Don't reconnect after disconnect |
|
|
|
int socket; ///< Socket FD of the server |
|
struct str read_buffer; ///< Input yet to be processed |
|
struct str write_buffer; ///< Outut yet to be be sent out |
|
struct poller_fd socket_event; ///< We can read from the socket |
|
|
|
struct transport *transport; ///< Transport method |
|
void *transport_data; ///< Transport data |
|
|
|
// Events: |
|
|
|
struct poller_timer ping_tmr; ///< We should send a ping |
|
struct poller_timer timeout_tmr; ///< Connection seems to be dead |
|
struct poller_timer reconnect_tmr; ///< We should reconnect now |
|
struct poller_timer autojoin_tmr; ///< Re/join channels as appropriate |
|
|
|
// IRC: |
|
|
|
// TODO: an output queue to prevent excess floods (this will be needed |
|
// especially for away status polling) |
|
|
|
bool rehashing; ///< Rehashing IRC identifiers |
|
|
|
struct str_map irc_users; ///< IRC user data |
|
struct str_map irc_channels; ///< IRC channel data |
|
struct str_map irc_buffer_map; ///< Maps IRC identifiers to buffers |
|
|
|
struct user *irc_user; ///< Our own user |
|
int nick_counter; ///< Iterates "nicks" when registering |
|
struct str irc_user_mode; ///< Our current user modes |
|
char *irc_user_host; ///< Our current user@host |
|
bool autoaway_active; ///< Autoaway is currently active |
|
|
|
struct strv cap_ls_buf; ///< Buffer for IRCv3.2 CAP LS |
|
bool cap_echo_message; ///< Whether the server echoes messages |
|
bool cap_away_notify; ///< Whether we get AWAY notifications |
|
bool cap_sasl; ///< Whether SASL is available |
|
|
|
// Server-specific information (from RPL_ISUPPORT): |
|
|
|
irc_tolower_fn irc_tolower; ///< Server tolower() |
|
irc_strxfrm_fn irc_strxfrm; ///< Server strxfrm() |
|
|
|
char *irc_chantypes; ///< Channel types (name prefixes) |
|
char *irc_idchan_prefixes; ///< Prefixes for "safe channels" |
|
char *irc_statusmsg; ///< Prefixes for channel targets |
|
|
|
char irc_extban_prefix; ///< EXTBAN prefix or \0 |
|
char *irc_extban_types; ///< EXTBAN types |
|
|
|
char *irc_chanmodes_list; ///< Channel modes for lists |
|
char *irc_chanmodes_param_always; ///< Channel modes with mandatory param |
|
char *irc_chanmodes_param_when_set; ///< Channel modes with param when set |
|
char *irc_chanmodes_param_never; ///< Channel modes without param |
|
|
|
char *irc_chanuser_prefixes; ///< Channel user prefixes |
|
char *irc_chanuser_modes; ///< Channel user modes |
|
|
|
unsigned irc_max_modes; ///< Max parametrized modes per command |
|
}; |
|
|
|
static struct ispect_field g_server_ispect[] = |
|
{ |
|
ISPECT( server, name, STRING ) |
|
ISPECT( server, state, INT ) |
|
ISPECT( server, reconnect_attempt, UINT ) |
|
ISPECT( server, manual_disconnect, BOOL ) |
|
ISPECT( server, irc_user_host, STRING ) |
|
ISPECT( server, autoaway_active, BOOL ) |
|
ISPECT( server, cap_echo_message, BOOL ) |
|
ISPECT_REF( server, buffer, false, buffer ) |
|
|
|
// TODO: either rename the underlying field or fix the plugins |
|
{ "user", offsetof (struct server, irc_user), |
|
ISPECT_REF, 0, g_user_ispect, false }, |
|
{ "user_mode", offsetof (struct server, irc_user_mode), |
|
ISPECT_STR, 0, NULL, false }, |
|
|
|
{} |
|
}; |
|
|
|
static void on_irc_timeout (void *user_data); |
|
static void on_irc_ping_timeout (void *user_data); |
|
static void on_irc_autojoin_timeout (void *user_data); |
|
static void irc_initiate_connect (struct server *s); |
|
|
|
static void |
|
server_init_specifics (struct server *self) |
|
{ |
|
// Defaults as per the RPL_ISUPPORT drafts, or RFC 1459 |
|
|
|
self->irc_tolower = irc_tolower; |
|
self->irc_strxfrm = irc_strxfrm; |
|
|
|
self->irc_chantypes = xstrdup ("#&"); |
|
self->irc_idchan_prefixes = xstrdup (""); |
|
self->irc_statusmsg = xstrdup (""); |
|
|
|
self->irc_extban_prefix = 0; |
|
self->irc_extban_types = xstrdup (""); |
|
|
|
self->irc_chanmodes_list = xstrdup ("b"); |
|
self->irc_chanmodes_param_always = xstrdup ("k"); |
|
self->irc_chanmodes_param_when_set = xstrdup ("l"); |
|
self->irc_chanmodes_param_never = xstrdup ("imnpst"); |
|
|
|
self->irc_chanuser_prefixes = xstrdup ("@+"); |
|
self->irc_chanuser_modes = xstrdup ("ov"); |
|
|
|
self->irc_max_modes = 3; |
|
} |
|
|
|
static void |
|
server_free_specifics (struct server *self) |
|
{ |
|
free (self->irc_chantypes); |
|
free (self->irc_idchan_prefixes); |
|
free (self->irc_statusmsg); |
|
|
|
free (self->irc_extban_types); |
|
|
|
free (self->irc_chanmodes_list); |
|
free (self->irc_chanmodes_param_always); |
|
free (self->irc_chanmodes_param_when_set); |
|
free (self->irc_chanmodes_param_never); |
|
|
|
free (self->irc_chanuser_prefixes); |
|
free (self->irc_chanuser_modes); |
|
} |
|
|
|
static struct server * |
|
server_new (struct poller *poller) |
|
{ |
|
struct server *self = xcalloc (1, sizeof *self); |
|
self->ref_count = 1; |
|
|
|
self->socket = -1; |
|
self->read_buffer = str_make (); |
|
self->write_buffer = str_make (); |
|
self->state = IRC_DISCONNECTED; |
|
|
|
self->timeout_tmr = poller_timer_make (poller); |
|
self->timeout_tmr.dispatcher = on_irc_timeout; |
|
self->timeout_tmr.user_data = self; |
|
|
|
self->ping_tmr = poller_timer_make (poller); |
|
self->ping_tmr.dispatcher = on_irc_ping_timeout; |
|
self->ping_tmr.user_data = self; |
|
|
|
self->reconnect_tmr = poller_timer_make (poller); |
|
self->reconnect_tmr.dispatcher = (poller_timer_fn) irc_initiate_connect; |
|
self->reconnect_tmr.user_data = self; |
|
|
|
self->autojoin_tmr = poller_timer_make (poller); |
|
self->autojoin_tmr.dispatcher = on_irc_autojoin_timeout; |
|
self->autojoin_tmr.user_data = self; |
|
|
|
self->irc_users = str_map_make (NULL); |
|
self->irc_users.key_xfrm = irc_strxfrm; |
|
self->irc_channels = str_map_make (NULL); |
|
self->irc_channels.key_xfrm = irc_strxfrm; |
|
self->irc_buffer_map = str_map_make (NULL); |
|
self->irc_buffer_map.key_xfrm = irc_strxfrm; |
|
|
|
self->irc_user_mode = str_make (); |
|
|
|
self->cap_ls_buf = strv_make (); |
|
server_init_specifics (self); |
|
return self; |
|
} |
|
|
|
static void |
|
server_destroy (struct server *self) |
|
{ |
|
free (self->name); |
|
|
|
if (self->connector) |
|
{ |
|
connector_free (self->connector); |
|
free (self->connector); |
|
} |
|
if (self->socks_conn) |
|
{ |
|
socks_connector_free (self->socks_conn); |
|
free (self->socks_conn); |
|
} |
|
|
|
if (self->transport |
|
&& self->transport->cleanup) |
|
self->transport->cleanup (self); |
|
|
|
if (self->socket != -1) |
|
{ |
|
poller_fd_reset (&self->socket_event); |
|
xclose (self->socket); |
|
} |
|
str_free (&self->read_buffer); |
|
str_free (&self->write_buffer); |
|
|
|
poller_timer_reset (&self->ping_tmr); |
|
poller_timer_reset (&self->timeout_tmr); |
|
poller_timer_reset (&self->reconnect_tmr); |
|
poller_timer_reset (&self->autojoin_tmr); |
|
|
|
str_map_free (&self->irc_users); |
|
str_map_free (&self->irc_channels); |
|
str_map_free (&self->irc_buffer_map); |
|
|
|
if (self->irc_user) |
|
user_unref (self->irc_user); |
|
str_free (&self->irc_user_mode); |
|
free (self->irc_user_host); |
|
|
|
strv_free (&self->cap_ls_buf); |
|
server_free_specifics (self); |
|
free (self); |
|
} |
|
|
|
REF_COUNTABLE_METHODS (server) |
|
#define server_ref do_not_use_dangerous |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
struct plugin |
|
{ |
|
LIST_HEADER (struct plugin) |
|
|
|
char *name; ///< Name of the plugin |
|
struct plugin_vtable *vtable; ///< Methods |
|
}; |
|
|
|
struct plugin_vtable |
|
{ |
|
/// Collect garbage |
|
void (*gc) (struct plugin *self); |
|
/// Unregister and free the plugin including all relevant resources |
|
void (*free) (struct plugin *self); |
|
}; |
|
|
|
static void |
|
plugin_destroy (struct plugin *self) |
|
{ |
|
self->vtable->free (self); |
|
free (self->name); |
|
free (self); |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
// This is a bit ugly since insertion is O(n) and the need to get rid of the |
|
// specific type because of list macros, however I don't currently posses any |
|
// strictly better, ordered data structure |
|
|
|
struct hook |
|
{ |
|
LIST_HEADER (struct hook) |
|
int priority; ///< The lesser the sooner |
|
}; |
|
|
|
static struct hook * |
|
hook_insert (struct hook *list, struct hook *item) |
|
{ |
|
// Corner cases: list is empty or we precede everything |
|
if (!list || item->priority < list->priority) |
|
{ |
|
LIST_PREPEND (list, item); |
|
return list; |
|
} |
|
|
|
// Otherwise fast-forward to the last entry that precedes us |
|
struct hook *before = list; |
|
while (before->next && before->next->priority < item->priority) |
|
before = before->next; |
|
|
|
// And link ourselves in between it and its successor |
|
if ((item->next = before->next)) |
|
item->next->prev = item; |
|
before->next = item; |
|
item->prev = before; |
|
return list; |
|
} |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
struct input_hook |
|
{ |
|
struct hook super; ///< Common hook fields |
|
|
|
/// Takes over the ownership of "input", returns either NULL if input |
|
/// was thrown away, or a possibly modified version of it |
|
char *(*filter) (struct input_hook *self, |
|
struct buffer *buffer, char *input); |
|
}; |
|
|
|
struct irc_hook |
|
{ |
|
struct hook super; ///< Common hook fields |
|
|
|
/// Takes over the ownership of "message", returns either NULL if message |
|
/// was thrown away, or a possibly modified version of it |
|
char *(*filter) (struct irc_hook *self, |
|
struct server *server, char *message); |
|
}; |
|
|
|
struct prompt_hook |
|
{ |
|
struct hook super; ///< Common hook fields |
|
|
|
/// Returns what the prompt should look like right now based on other state |
|
char *(*make) (struct prompt_hook *self); |
|
}; |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
struct completion_word |
|
{ |
|
size_t start; ///< Offset to start of word |
|
size_t end; ///< Offset to end of word |
|
}; |
|
|
|
struct completion |
|
{ |
|
char *line; ///< The line which is being completed |
|
|
|
struct completion_word *words; ///< Word locations |
|
size_t words_len; ///< Number of words |
|
size_t words_alloc; ///< Number of words allocated |
|
|
|
size_t location; ///< Which word is being completed |
|
}; |
|
|
|
struct completion_hook |
|
{ |
|
struct hook super; ///< Common hook fields |
|
|
|
/// Tries to add possible completions of "word" to "output" |
|
void (*complete) (struct completion_hook *self, |
|
struct completion *data, const char *word, struct strv *output); |
|
}; |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
struct app_context |
|
{ |
|
char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes |
|
|
|
// Configuration: |
|
|
|
struct config config; ///< Program configuration |
|
char *attrs[ATTR_COUNT]; ///< Terminal attributes |
|
bool isolate_buffers; ///< Isolate global/server buffers |
|
bool beep_on_highlight; ///< Beep on highlight |
|
bool logging; ///< Logging to file enabled |
|
bool show_all_prefixes; ///< Show all prefixes before nicks |
|
bool word_wrapping; ///< Enable simple word wrapping |
|
|
|
struct str_map servers; ///< Our servers |
|
|
|
// Events: |
|
|
|
struct poller_fd tty_event; ///< Terminal input event |
|
struct poller_fd signal_event; ///< Signal FD event |
|
|
|
struct poller_timer flush_timer; ///< Flush all open files (e.g. logs) |
|
struct poller_timer date_chg_tmr; ///< Print a date change |
|
struct poller_timer autoaway_tmr; ///< Autoaway timer |
|
|
|
struct poller poller; ///< Manages polled descriptors |
|
bool quitting; ///< User requested quitting |
|
bool polling; ///< The event loop is running |
|
|
|
// Buffers: |
|
|
|
struct buffer *buffers; ///< All our buffers in order |
|
struct buffer *buffers_tail; ///< The tail of our buffers |
|
|
|
struct buffer *global_buffer; ///< The global buffer |
|
struct buffer *current_buffer; ///< The current buffer |
|
struct buffer *last_buffer; ///< Last used buffer |
|
|
|
struct str_map buffers_by_name; ///< Buffers by name |
|
|
|
unsigned backlog_limit; ///< Limit for buffer lines |
|
time_t last_displayed_msg_time; ///< Time of last displayed message |
|
|
|
// Terminal: |
|
|
|
iconv_t term_to_utf8; ///< Terminal encoding to UTF-8 |
|
iconv_t term_from_utf8; ///< UTF-8 to terminal encoding |
|
|
|
struct input *input; ///< User interface |
|
|
|
struct poller_idle prompt_event; ///< Deferred prompt refresh |
|
struct poller_idle input_event; ///< Pending input event |
|
struct strv pending_input; ///< Pending input lines |
|
|
|
int *nick_palette; ///< A 256-colour palette for nicknames |
|
size_t nick_palette_len; ///< Number of entries in nick_palette |
|
|
|
bool awaiting_formatting_escape; ///< Awaiting an IRC formatting escape |
|
bool in_bracketed_paste; ///< User is pasting some content |
|
struct str input_buffer; ///< Buffered pasted content |
|
|
|
bool running_backlog_helper; ///< Running a backlog helper |
|
bool running_editor; ///< Running editor for the input |
|
char *editor_filename; ///< The file being edited by user |
|
int terminal_suspended; ///< Terminal suspension level |
|
|
|
struct plugin *plugins; ///< Loaded plugins |
|
struct hook *input_hooks; ///< Input hooks |
|
struct hook *irc_hooks; ///< IRC hooks |
|
struct hook *prompt_hooks; ///< Prompt hooks |
|
struct hook *completion_hooks; ///< Autocomplete hooks |
|
} |
|
*g_ctx; |
|
|
|
static struct ispect_field g_ctx_ispect[] = |
|
{ |
|
ISPECT_MAP_REF( app_context, servers, false, server ) |
|
ISPECT_REF( app_context, buffers, true, buffer ) |
|
ISPECT_REF( app_context, global_buffer, false, buffer ) |
|
ISPECT_REF( app_context, current_buffer, false, buffer ) |
|
{} |
|
}; |
|
|
|
static int * |
|
filter_color_cube_for_acceptable_nick_colors (size_t *len) |
|
{ |
|
// This is a pure function and we don't use threads, static storage is fine |
|
static int table[6 * 6 * 6]; |
|
size_t len_counter = 0; |
|
for (int x = 0; x < (int) N_ELEMENTS (table); x++) |
|
{ |
|
int r = x / 36; |
|
int g = (x / 6) % 6; |
|
int b = (x % 6); |
|
|
|
// The first step is 95/255, the rest are 40/255, |
|
// as an approximation we can double the first step |
|
double linear_R = pow ((r + !!r) / 6., 2.2); |
|
double linear_G = pow ((g + !!g) / 6., 2.2); |
|
double linear_B = pow ((b + !!b) / 6., 2.2); |
|
|
|
// Use the relative luminance of colours within the cube to filter |
|
// colours that look okay-ish on terminals with both black and white |
|
// backgrounds (use the test-nick-colors script to calibrate) |
|
double Y = 0.2126 * linear_R + 0.7152 * linear_G + 0.0722 * linear_B; |
|
if (Y >= .25 && Y <= .4) |
|
table[len_counter++] = 16 + x; |
|
} |
|
*len = len_counter; |
|
return table; |
|
} |
|
|
|
static bool |
|
app_iconv_open (iconv_t *target, const char *to, const char *from) |
|
{ |
|
if (ICONV_ACCEPTS_TRANSLIT) |
|
{ |
|
char *to_real = xstrdup_printf ("%s//TRANSLIT", to); |
|
*target = iconv_open (to_real, from); |
|
free (to_real); |
|
} |
|
else |
|
*target = iconv_open (to, from); |
|
return *target != (iconv_t) -1; |
|
} |
|
|
|
static void |
|
app_context_init (struct app_context *self) |
|
{ |
|
memset (self, 0, sizeof *self); |
|
|
|
self->config = config_make (); |
|
poller_init (&self->poller); |
|
|
|
self->servers = str_map_make ((str_map_free_fn) server_unref); |
|
self->servers.key_xfrm = tolower_ascii_strxfrm; |
|
|
|
self->buffers_by_name = str_map_make (NULL); |
|
self->buffers_by_name.key_xfrm = tolower_ascii_strxfrm; |
|
|
|
// So that we don't lose the logo shortly after startup |
|
self->backlog_limit = 1000; |
|
self->last_displayed_msg_time = time (NULL); |
|
|
|
char *native = nl_langinfo (CODESET); |
|
if (!app_iconv_open (&self->term_from_utf8, native, "UTF-8") |
|
|| !app_iconv_open (&self->term_to_utf8, "UTF-8", native)) |
|
exit_fatal ("creating the UTF-8 conversion object failed: %s", |
|
strerror (errno)); |
|
|
|
self->input = input_new (); |
|
self->input->user_data = self; |
|
self->pending_input = strv_make (); |
|
self->input_buffer = str_make (); |
|
|
|
self->nick_palette = |
|
filter_color_cube_for_acceptable_nick_colors (&self->nick_palette_len); |
|
} |
|
|
|
static void |
|
app_context_free (struct app_context *self) |
|
{ |
|
// Plugins can try to use of the other fields when destroyed |
|
LIST_FOR_EACH (struct plugin, iter, self->plugins) |
|
plugin_destroy (iter); |
|
|
|
config_free (&self->config); |
|
for (size_t i = 0; i < ATTR_COUNT; i++) |
|
{ |
|
free (self->attrs_defaults[i]); |
|
free (self->attrs[i]); |
|
} |
|
|
|
LIST_FOR_EACH (struct buffer, iter, self->buffers) |
|
{ |
|
#ifdef HAVE_READLINE |
|
// We can use the user interface here; see buffer_destroy() |
|
CALL_ (self->input, buffer_destroy, iter->input_data); |
|
iter->input_data = NULL; |
|
#endif // HAVE_READLINE |
|
buffer_unref (iter); |
|
} |
|
str_map_free (&self->buffers_by_name); |
|
|
|
str_map_free (&self->servers); |
|
poller_free (&self->poller); |
|
|
|
iconv_close (self->term_from_utf8); |
|
iconv_close (self->term_to_utf8); |
|
|
|
CALL (self->input, destroy); |
|
strv_free (&self->pending_input); |
|
str_free (&self->input_buffer); |
|
|
|
free (self->editor_filename); |
|
} |
|
|
|
static void |
|
refresh_prompt (struct app_context *ctx) |
|
{ |
|
// XXX: the need for this conditional could probably be resolved |
|
// by some clever reordering |
|
if (ctx->prompt_event.poller) |
|
poller_idle_set (&ctx->prompt_event); |
|
} |
|
|
|
// --- Configuration ----------------------------------------------------------- |
|
|
|
static void |
|
on_config_debug_mode_change (struct config_item *item) |
|
{ |
|
g_debug_mode = item->value.boolean; |
|
} |
|
|
|
static void |
|
on_config_show_all_prefixes_change (struct config_item *item) |
|
{ |
|
struct app_context *ctx = item->user_data; |
|
ctx->show_all_prefixes = item->value.boolean; |
|
refresh_prompt (ctx); |
|
} |
|
|
|
static void on_config_backlog_limit_change (struct config_item *item); |
|
static void on_config_attribute_change (struct config_item *item); |
|
static void on_config_logging_change (struct config_item *item); |
|
|
|
#define TRIVIAL_BOOLEAN_ON_CHANGE(name) \ |
|
static void \ |
|
on_config_ ## name ## _change (struct config_item *item) \ |
|
{ \ |
|
struct app_context *ctx = item->user_data; \ |
|
ctx->name = item->value.boolean; \ |
|
} |
|
|
|
TRIVIAL_BOOLEAN_ON_CHANGE (isolate_buffers) |
|
TRIVIAL_BOOLEAN_ON_CHANGE (beep_on_highlight) |
|
TRIVIAL_BOOLEAN_ON_CHANGE (word_wrapping) |
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
|
|
|
static bool |
|
config_validate_nonjunk_string |
|
(const struct config_item *item, struct error **e) |
|
{ |
|
if (item->type == CONFIG_ITEM_NULL) |
|
return true; |
|
|
|
hard_assert (config_item_type_is_string (item->type)); |
|
for (size_t i = 0; i < item->value.string.len; i++) |
|
{ |
|
// Not even a tabulator |
|
unsigned char c = item->value.string.str[i]; |
|
if (iscntrl_ascii (c)) |
|
{ |
|
error_set (e, "control characters are not allowed"); |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
static bool |
|
config_validate_addresses |
|
(const struct config_item *item, struct error **e) |
|
{ |
|
if (item->type == CONFIG_ITEM_NULL) |
|
return true; |
|
if (!config_validate_nonjunk_string (item, e)) |
|
return false; |
|
|
|
// Comma-separated list of "host[:port]" pairs |
|
regex_t re; |
|
int err = regcomp (&re, "^([^/:,]+(:[^/:,]+)?)?" |
|
"(,([^/:,]+(:[^/:,]+)?)?)*$", REG_EXTENDED | REG_NOSUB); |
|
hard_assert (!err); |
|
|
|
bool result = !regexec (&re, item->value.string.str, 0, NULL, 0); |
|
if (!result) |
|
error_set (e, "invalid address list string"); |
|
|
|
regfree (&re); |
|
return result; |
|
} |
|
|
|
static bool |
|
config_validate_nonnegative |
|
(const struct config_item *item, struct error **e) |
|
{ |
|
if (item->type == CONFIG_ITEM_NULL) |
|
return true; |
|
|
|
hard_assert (item->type == CONFIG_ITEM_INTEGER); |
|
if (item->value.integer >= 0) |
|
return true; |
|
|
|
error_set (e, "must be non-negative"); |
|
return false; |
|
} |
|
|
|
static struct config_schema g_config_server[] = |
|
{ |
|
{ .name = "nicks", |
|
.comment = "IRC nickname", |
|
.type = CONFIG_ITEM_STRING_ARRAY, |
|
.validate = config_validate_nonjunk_string }, |
|
{ .name = "username", |
|
.comment = "IRC user name", |
|
.type = CONFIG_ITEM_STRING, |
|
.validate = config_validate_nonjunk_string }, |
|
{ .name = "realname", |
|
.comment = "IRC real name/e-mail", |
|
.type = CONFIG_ITEM_STRING, |
|
.validate = config_validate_nonjunk_string }, |
|
|
|
{ .name = "addresses", |
|
.comment = "Addresses of the IRC network (e.g. \"irc.net:6667\")", |
|
.type = CONFIG_ITEM_STRING_ARRAY, |
|
.validate = config_validate_addresses }, |
|
{ .name = "password", |
|
.comment = "Password to connect to the server, if any", |
|
.type = CONFIG_ITEM_STRING, |
|
.validate = config_validate_nonjunk_string }, |
|
// XXX: if we add support for new capabilities, the value stays unchanged |
|
{ .name = "capabilities", |
|
.comment = "Capabilities to use if supported by server", |
|
.type = CONFIG_ITEM_STRING_ARRAY, |
|
.validate = config_validate_nonjunk_string, |
|
.default_ = "\"multi-prefix,invite-notify,server-time,echo-message," |
|
"message-tags,away-notify,cap-notify,chghost\"" }, |
|
|
|
{ .name = "tls", |
|
.comment = "Whether to use TLS", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "off" }, |
|
{ .name = "tls_cert", |
|
.comment = "Client TLS certificate (PEM)", |
|
.type = CONFIG_ITEM_STRING }, |
|
{ .name = "tls_verify", |
|
.comment = "Whether to verify certificates", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "on" }, |
|
{ .name = "tls_ca_file", |
|
.comment = "OpenSSL CA bundle file", |
|
.type = CONFIG_ITEM_STRING }, |
|
{ .name = "tls_ca_path", |
|
.comment = "OpenSSL CA bundle path", |
|
.type = CONFIG_ITEM_STRING }, |
|
{ .name = "tls_ciphers", |
|
.comment = "OpenSSL cipher preference list", |
|
.type = CONFIG_ITEM_STRING, |
|
.default_ = "\"DEFAULT:!MEDIUM:!LOW\"" }, |
|
|
|
{ .name = "autoconnect", |
|
.comment = "Connect automatically on startup", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "on" }, |
|
{ .name = "autojoin", |
|
.comment = "Channels to join on start (e.g. \"#abc,#def key,#ghi\")", |
|
.type = CONFIG_ITEM_STRING_ARRAY, |
|
.validate = config_validate_nonjunk_string }, |
|
{ .name = "command", |
|
.comment = "Command to execute after a successful connect", |
|
.type = CONFIG_ITEM_STRING }, |
|
{ .name = "command_delay", |
|
.comment = "Delay between executing \"command\" and joining channels", |
|
.type = CONFIG_ITEM_INTEGER, |
|
.validate = config_validate_nonnegative, |
|
.default_ = "0" }, |
|
{ .name = "reconnect", |
|
.comment = "Whether to reconnect on error", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "on" }, |
|
{ .name = "reconnect_delay", |
|
.comment = "Time between reconnecting", |
|
.type = CONFIG_ITEM_INTEGER, |
|
.validate = config_validate_nonnegative, |
|
.default_ = "5" }, |
|
|
|
{ .name = "socks_host", |
|
.comment = "Address of a SOCKS 4a/5 proxy", |
|
.type = CONFIG_ITEM_STRING, |
|
.validate = config_validate_nonjunk_string }, |
|
{ .name = "socks_port", |
|
.comment = "SOCKS port number", |
|
.type = CONFIG_ITEM_INTEGER, |
|
.validate = config_validate_nonnegative, |
|
.default_ = "1080" }, |
|
{ .name = "socks_username", |
|
.comment = "SOCKS auth. username", |
|
.type = CONFIG_ITEM_STRING }, |
|
{ .name = "socks_password", |
|
.comment = "SOCKS auth. password", |
|
.type = CONFIG_ITEM_STRING }, |
|
{} |
|
}; |
|
|
|
static struct config_schema g_config_behaviour[] = |
|
{ |
|
{ .name = "isolate_buffers", |
|
.comment = "Don't leak messages from the server and global buffers", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "off", |
|
.on_change = on_config_isolate_buffers_change }, |
|
{ .name = "beep_on_highlight", |
|
.comment = "Beep when highlighted or on a new invisible PM", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "on", |
|
.on_change = on_config_beep_on_highlight_change }, |
|
{ .name = "show_all_prefixes", |
|
.comment = "Show all prefixes in front of nicknames", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "off", |
|
.on_change = on_config_show_all_prefixes_change }, |
|
{ .name = "word_wrapping", |
|
.comment = "Enable simple word wrapping in buffers", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "on", |
|
.on_change = on_config_word_wrapping_change }, |
|
{ .name = "editor_command", |
|
.comment = "VIM: \"vim +%Bgo %F\", Emacs: \"emacs -nw +%L:%C %F\"", |
|
.type = CONFIG_ITEM_STRING }, |
|
{ .name = "process_pasted_text", |
|
.comment = "Normalize newlines and quote the command prefix in pastes", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "on" }, |
|
{ .name = "date_change_line", |
|
.comment = "Input to strftime(3) for the date change line", |
|
.type = CONFIG_ITEM_STRING, |
|
.default_ = "\"%F\"" }, |
|
{ .name = "read_marker_char", |
|
.comment = "The character to use for the read marker line", |
|
.type = CONFIG_ITEM_STRING, |
|
.default_ = "\"-\"", |
|
.validate = config_validate_nonjunk_string }, |
|
{ .name = "logging", |
|
.comment = "Log buffer contents to file", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "off", |
|
.on_change = on_config_logging_change }, |
|
{ .name = "save_on_quit", |
|
.comment = "Save configuration before quitting", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "on" }, |
|
{ .name = "debug_mode", |
|
.comment = "Produce some debugging output", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "off", |
|
.on_change = on_config_debug_mode_change }, |
|
|
|
{ .name = "backlog_limit", |
|
.comment = "Maximum number of lines stored in the backlog", |
|
.type = CONFIG_ITEM_INTEGER, |
|
.validate = config_validate_nonnegative, |
|
.default_ = "1000", |
|
.on_change = on_config_backlog_limit_change }, |
|
{ .name = "backlog_helper", |
|
.comment = "Shell command to display a buffer's history", |
|
.type = CONFIG_ITEM_STRING, |
|
.default_ = "\"LESSSECURE=1 less -M -R +Gb\"" }, |
|
{ .name = "backlog_helper_strip_formatting", |
|
.comment = "Strip formatting from backlog helper input", |
|
.type = CONFIG_ITEM_BOOLEAN, |
|
.default_ = "off" }, |
|
|
|
{ .name = "reconnect_delay_growing", |
|
.comment = "Growing factor for reconnect delay", |
|
.type = CONFIG_ITEM_INTEGER, |
|
.validate = config_validate_nonnegative, |
|
.default_ = "2" }, |
|
{ .name = "reconnect_delay_max", |
|
.comment = "Maximum reconnect delay in seconds", |
|
.type = |