2015-04-12 04:52:39 +02:00
|
|
|
/*
|
2020-10-31 13:42:56 +01:00
|
|
|
* degesch.c: a terminal-based IRC client
|
2015-04-12 04:52:39 +02:00
|
|
|
*
|
2020-09-02 19:36:30 +02:00
|
|
|
* Copyright (c) 2015 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
2015-04-12 04:52:39 +02:00
|
|
|
*
|
|
|
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
2018-06-21 22:42:05 +02:00
|
|
|
* purpose with or without fee is hereby granted.
|
2015-04-12 04:52:39 +02:00
|
|
|
*
|
|
|
|
|
* 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.
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
|
2015-04-29 21:25:57 +02:00
|
|
|
// A table of all attributes we use for output
|
2020-09-20 13:24:02 +02:00
|
|
|
#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 )
|
2015-04-29 20:56:26 +02:00
|
|
|
|
|
|
|
|
enum
|
|
|
|
|
{
|
|
|
|
|
#define XX(x, y, z) ATTR_ ## x,
|
|
|
|
|
ATTR_TABLE (XX)
|
|
|
|
|
#undef XX
|
|
|
|
|
ATTR_COUNT
|
|
|
|
|
};
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
// User data for logger functions to enable formatted logging
|
2015-04-29 20:56:26 +02:00
|
|
|
#define print_fatal_data ((void *) ATTR_ERROR)
|
|
|
|
|
#define print_error_data ((void *) ATTR_ERROR)
|
|
|
|
|
#define print_warning_data ((void *) ATTR_WARNING)
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
#define PROGRAM_NAME "degesch"
|
|
|
|
|
|
|
|
|
|
#include "common.c"
|
2015-04-21 22:08:18 +02:00
|
|
|
#include "kike-replies.c"
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
|
#include <langinfo.h>
|
|
|
|
|
#include <locale.h>
|
|
|
|
|
#include <pwd.h>
|
2015-04-27 01:36:33 +02:00
|
|
|
#include <sys/utsname.h>
|
2015-05-07 05:19:13 +02:00
|
|
|
#include <wchar.h>
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-05 03:42:40 +02:00
|
|
|
#include <termios.h>
|
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
#include <curses.h>
|
|
|
|
|
#include <term.h>
|
2015-04-13 00:06:08 +02:00
|
|
|
|
|
|
|
|
// Literally cancer
|
|
|
|
|
#undef lines
|
|
|
|
|
#undef columns
|
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
#include <ffi.h>
|
|
|
|
|
|
2015-11-19 15:45:32 +01:00
|
|
|
#ifdef HAVE_LUA
|
|
|
|
|
#include <lua.h>
|
|
|
|
|
#include <lualib.h>
|
|
|
|
|
#include <lauxlib.h>
|
|
|
|
|
#endif // HAVE_LUA
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// --- Terminal information ----------------------------------------------------
|
2015-05-06 22:20:02 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static struct
|
|
|
|
|
{
|
|
|
|
|
bool initialized; ///< Terminal is available
|
|
|
|
|
bool stdout_is_tty; ///< `stdout' is a terminal
|
|
|
|
|
bool stderr_is_tty; ///< `stderr' is a terminal
|
2015-05-06 22:20:02 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
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
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
int lines; ///< Number of lines
|
|
|
|
|
int columns; ///< Number of columns
|
|
|
|
|
}
|
|
|
|
|
g_terminal;
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static void
|
|
|
|
|
update_screen_size (void)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
#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
|
|
|
|
|
}
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static bool
|
|
|
|
|
init_terminal (void)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
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;
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
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;
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
free_terminal (void)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
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);
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// --- User interface ----------------------------------------------------------
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// 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.
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
/// Some arbitrary limit for the history
|
|
|
|
|
#define HISTORY_LIMIT 10000
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
/// Characters that separate words
|
|
|
|
|
#define WORD_BREAKING_CHARS " \f\n\r\t\v"
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
struct input
|
|
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_vtable *vtable; ///< Virtual methods
|
2016-03-10 00:06:28 +01:00
|
|
|
void (*add_functions) (void *); ///< Define functions for binding
|
2016-03-07 01:08:52 +01:00
|
|
|
void *user_data; ///< User data for callbacks
|
|
|
|
|
};
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
typedef void *input_buffer_t; ///< Pointer alias for input buffers
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
/// Named function that can be bound to a sequence of characters
|
|
|
|
|
typedef bool (*input_fn) (int count, int key, void *user_data);
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// 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);
|
2016-03-07 23:15:57 +01:00
|
|
|
/// Retrieve current prompt string
|
2016-03-07 01:08:52 +01:00
|
|
|
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
|
|
|
|
|
char *(*get_line) (void *input);
|
|
|
|
|
/// 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);
|
2015-05-05 03:23:53 +02:00
|
|
|
};
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#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)
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2015-05-05 19:35:51 +02:00
|
|
|
// --- GNU Readline ------------------------------------------------------------
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
#ifdef HAVE_READLINE
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#include <readline/readline.h>
|
|
|
|
|
#include <readline/history.h>
|
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
#define INPUT_START_IGNORE RL_PROMPT_START_IGNORE
|
|
|
|
|
#define INPUT_END_IGNORE RL_PROMPT_END_IGNORE
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl_fn
|
|
|
|
|
{
|
|
|
|
|
ffi_closure closure; ///< Closure
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
LIST_HEADER (struct input_rl_fn)
|
|
|
|
|
input_fn callback; ///< Real callback
|
|
|
|
|
void *user_data; ///< Real callback user data
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct input_rl_buffer
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
HISTORY_STATE *history; ///< Saved history state
|
|
|
|
|
char *saved_line; ///< Saved line content
|
|
|
|
|
int saved_point; ///< Saved cursor position
|
|
|
|
|
int saved_mark; ///< Saved mark
|
|
|
|
|
};
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
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
|
|
|
|
|
};
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_ding (void *input)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
(void) input;
|
|
|
|
|
rl_ding ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const char *
|
|
|
|
|
input_rl_get_prompt (void *input)
|
|
|
|
|
{
|
|
|
|
|
struct input_rl *self = input;
|
|
|
|
|
return self->prompt;
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_set_prompt (void *input, char *prompt)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&self->prompt, prompt);
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2020-10-06 13:37:07 +02:00
|
|
|
if (!self->active || self->prompt_shown <= 0)
|
2015-05-12 07:07:02 +02:00
|
|
|
return;
|
|
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
// First reset the prompt to work around a bug in readline
|
|
|
|
|
rl_set_prompt ("");
|
2020-10-06 13:37:07 +02:00
|
|
|
rl_redisplay ();
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
|
rl_set_prompt (self->prompt);
|
2020-10-06 13:37:07 +02:00
|
|
|
rl_redisplay ();
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_clear_line (void *input)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
(void) input;
|
2015-11-24 03:04:14 +01:00
|
|
|
rl_replace_line ("", false);
|
2015-05-05 03:23:53 +02:00
|
|
|
rl_redisplay ();
|
|
|
|
|
}
|
|
|
|
|
|
2015-12-25 04:21:18 +01:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__erase (struct input_rl *self)
|
2015-12-25 04:21:18 +01:00
|
|
|
{
|
|
|
|
|
rl_set_prompt ("");
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_clear_line (self);
|
2015-12-25 04:21:18 +01:00
|
|
|
}
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
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)
|
|
|
|
|
{
|
|
|
|
|
(void) input;
|
|
|
|
|
return rl_copy_text (0, rl_end);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-05-08 06:23:38 +02:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_bind (void *input, const char *seq, const char *function_name)
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
(void) input;
|
2015-05-08 06:23:38 +02:00
|
|
|
rl_bind_keyseq (seq, rl_named_function (function_name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_bind_meta (void *input, char key, const char *function_name)
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
|
|
|
|
// This one seems to actually work
|
|
|
|
|
char keyseq[] = { '\\', 'e', key, 0 };
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_bind (input, keyseq, function_name);
|
2015-05-08 06:23:38 +02:00
|
|
|
#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
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_bind_control (void *input, char key, const char *function_name)
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
|
|
|
|
char keyseq[] = { '\\', 'C', '-', key, 0 };
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_bind (input, keyseq, function_name);
|
2015-12-25 04:21:18 +01:00
|
|
|
}
|
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__forward (ffi_cif *cif, void *ret, void **args, void *user_data)
|
2016-03-06 17:59:45 +01:00
|
|
|
{
|
|
|
|
|
(void) cif;
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl_fn *data = user_data;
|
2016-03-06 17:59:45 +01:00
|
|
|
if (!data->callback
|
|
|
|
|
(*(int *) args[0], UNMETA (*(int *) args[1]), data->user_data))
|
|
|
|
|
rl_ding ();
|
|
|
|
|
*(int *) ret = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_register_fn (void *input,
|
2016-03-06 17:59:45 +01:00
|
|
|
const char *name, const char *help, input_fn callback, void *user_data)
|
|
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
2016-03-06 17:59:45 +01:00
|
|
|
(void) help;
|
|
|
|
|
|
|
|
|
|
void *bound_fn = NULL;
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
|
2016-03-06 17:59:45 +01:00
|
|
|
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,
|
2016-03-07 01:08:52 +01:00
|
|
|
&cif, input_rl__forward, data, bound_fn) == FFI_OK);
|
2016-03-06 17:59:45 +01:00
|
|
|
|
|
|
|
|
rl_add_defun (name, (rl_command_func_t *) bound_fn, -1);
|
|
|
|
|
LIST_PREPEND (self->fns, data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
static int app_readline_init (void);
|
|
|
|
|
static void on_readline_input (char *line);
|
2015-05-06 17:32:29 +02:00
|
|
|
static char **app_readline_completion (const char *text, int start, int end);
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_start (void *input, const char *program_name)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
using_history ();
|
|
|
|
|
// This can cause memory leaks, or maybe even a segfault. Funny, eh?
|
|
|
|
|
stifle_history (HISTORY_LIMIT);
|
|
|
|
|
|
2015-05-06 22:01:48 +02:00
|
|
|
const char *slash = strrchr (program_name, '/');
|
|
|
|
|
rl_readline_name = slash ? ++slash : program_name;
|
2015-05-05 03:23:53 +02:00
|
|
|
rl_startup_hook = app_readline_init;
|
|
|
|
|
rl_catch_sigwinch = false;
|
2020-03-21 22:02:02 +01:00
|
|
|
rl_change_environment = false;
|
2015-05-06 22:01:48 +02:00
|
|
|
|
2015-05-06 22:20:02 +02:00
|
|
|
rl_basic_word_break_characters = WORD_BREAKING_CHARS;
|
2015-05-06 22:01:48 +02:00
|
|
|
rl_completer_word_break_characters = NULL;
|
2015-05-06 17:32:29 +02:00
|
|
|
rl_attempted_completion_function = app_readline_completion;
|
2015-05-06 22:01:48 +02:00
|
|
|
|
|
|
|
|
hard_assert (self->prompt != NULL);
|
2016-03-10 00:06:28 +01:00
|
|
|
// 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);
|
2015-05-06 22:01:48 +02:00
|
|
|
rl_callback_handler_install (self->prompt, on_readline_input);
|
|
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
self->prompt_shown = 1;
|
|
|
|
|
self->active = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_stop (void *input)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
2015-05-05 03:23:53 +02:00
|
|
|
if (self->prompt_shown > 0)
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__erase (self);
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
|
// This is okay as long as we're not called from within readline
|
|
|
|
|
rl_callback_handler_remove ();
|
|
|
|
|
self->active = false;
|
2015-05-12 07:07:02 +02:00
|
|
|
self->prompt_shown = false;
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static void
|
|
|
|
|
input_rl_prepare (void *input, bool enabled)
|
|
|
|
|
{
|
|
|
|
|
(void) input;
|
|
|
|
|
if (enabled)
|
|
|
|
|
rl_prep_terminal (true);
|
|
|
|
|
else
|
|
|
|
|
rl_deprep_terminal ();
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-05 03:23:53 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
// 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
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__save_buffer (struct input_rl *self, struct input_rl_buffer *buffer)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
|
|
|
|
(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;
|
2015-11-24 03:04:14 +01:00
|
|
|
|
|
|
|
|
rl_replace_line ("", true);
|
|
|
|
|
if (self->prompt_shown > 0)
|
|
|
|
|
rl_redisplay ();
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__restore_buffer (struct input_rl *self, struct input_rl_buffer *buffer)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
|
|
|
|
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;
|
2015-07-04 01:06:51 +02:00
|
|
|
state->entries = NULL;
|
2015-05-05 03:23:53 +02:00
|
|
|
history_set_history_state (state);
|
|
|
|
|
free (state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (buffer->saved_line)
|
|
|
|
|
{
|
2015-11-24 03:04:14 +01:00
|
|
|
rl_replace_line (buffer->saved_line, true);
|
2015-05-05 03:23:53 +02:00
|
|
|
rl_point = buffer->saved_point;
|
|
|
|
|
rl_mark = buffer->saved_mark;
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&buffer->saved_line, NULL);
|
2015-05-05 03:23:53 +02:00
|
|
|
|
2015-07-05 17:29:44 +02:00
|
|
|
if (self->prompt_shown > 0)
|
2015-05-05 03:23:53 +02:00
|
|
|
rl_redisplay ();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_buffer_switch (void *input, input_buffer_t input_buffer)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
|
|
|
|
struct input_rl_buffer *buffer = input_buffer;
|
2015-05-05 03:23:53 +02:00
|
|
|
// 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)
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__save_buffer (self, self->current);
|
2015-05-05 03:23:53 +02:00
|
|
|
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
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__restore_buffer (self, buffer);
|
2015-05-05 03:23:53 +02:00
|
|
|
self->current = buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__buffer_destroy_wo_history (struct input_rl_buffer *self)
|
2015-05-05 03:23:53 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
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;
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
// See input_rl_buffer_switch() for why we need to do this BS
|
2015-05-05 03:23:53 +02:00
|
|
|
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 ();
|
2015-06-28 23:00:46 +02:00
|
|
|
// rl_clear_history just removes history entries,
|
|
|
|
|
// we have to reclaim memory for their actual container ourselves
|
|
|
|
|
free (buffer->history->entries);
|
2015-06-20 19:50:45 +02:00
|
|
|
free (buffer->history);
|
|
|
|
|
buffer->history = NULL;
|
2015-05-05 03:23:53 +02:00
|
|
|
|
|
|
|
|
history_set_history_state (state);
|
|
|
|
|
free (state);
|
|
|
|
|
}
|
|
|
|
|
#endif // RL_READLINE_VERSION
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
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;
|
2015-05-05 03:23:53 +02:00
|
|
|
}
|
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// 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.
|
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__save (struct input_rl *self)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
|
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
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl__restore (struct input_rl *self)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
|
hard_assert (self->saved_line);
|
|
|
|
|
|
|
|
|
|
rl_set_prompt (self->prompt);
|
2015-11-24 03:04:14 +01:00
|
|
|
rl_replace_line (self->saved_line, false);
|
2015-05-05 08:46:59 +02:00
|
|
|
rl_point = self->saved_point;
|
|
|
|
|
rl_mark = self->saved_mark;
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&self->saved_line, NULL);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_rl_hide (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_rl *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
if (!self->active || self->prompt_shown-- < 1)
|
|
|
|
|
return;
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
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);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#define XX(a) .a = input_rl_ ## a,
|
|
|
|
|
static struct input_vtable input_rl_vtable = { INPUT_VTABLE (XX) };
|
|
|
|
|
#undef XX
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static struct input *
|
|
|
|
|
input_rl_new (void)
|
|
|
|
|
{
|
|
|
|
|
struct input_rl *self = xcalloc (1, sizeof *self);
|
|
|
|
|
self->super.vtable = &input_rl_vtable;
|
|
|
|
|
return &self->super;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#define input_new input_rl_new
|
2015-05-05 08:46:59 +02:00
|
|
|
#endif // HAVE_READLINE
|
|
|
|
|
|
2015-05-05 19:35:51 +02:00
|
|
|
// --- BSD Editline ------------------------------------------------------------
|
2015-05-05 08:46:59 +02:00
|
|
|
|
|
|
|
|
#ifdef HAVE_EDITLINE
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#include <histedit.h>
|
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
#define INPUT_START_IGNORE '\x01'
|
|
|
|
|
#define INPUT_END_IGNORE '\x01'
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el_fn
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
ffi_closure closure; ///< Closure
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
LIST_HEADER (struct input_el_fn)
|
|
|
|
|
input_fn callback; ///< Real callback
|
|
|
|
|
void *user_data; ///< Real callback user data
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
wchar_t *name; ///< Function name
|
|
|
|
|
wchar_t *help; ///< Function help
|
|
|
|
|
};
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el_buffer
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
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
|
|
|
|
|
};
|
2015-05-08 06:23:38 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input super; ///< Parent class
|
|
|
|
|
EditLine *editline; ///< The EditLine object
|
2015-05-08 06:23:38 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
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 int
|
|
|
|
|
input_el__get_termios (int character, int fallback)
|
2015-05-08 06:23:38 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
if (!g_terminal.initialized)
|
|
|
|
|
return fallback;
|
|
|
|
|
|
|
|
|
|
cc_t value = g_terminal.termios.c_cc[character];
|
|
|
|
|
if (value == _POSIX_VDISABLE)
|
|
|
|
|
return fallback;
|
|
|
|
|
return value;
|
2015-05-08 06:23:38 +02:00
|
|
|
}
|
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__redisplay (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
|
// See rl_redisplay()
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
|
|
|
|
char x[] = { input_el__get_termios (VREPRINT, 'R' - 0x40), 0 };
|
2015-05-05 08:46:59 +02:00
|
|
|
el_push (self->editline, x);
|
|
|
|
|
|
|
|
|
|
// We have to do this or it gets stuck and nothing is done
|
2020-09-02 19:11:10 +02:00
|
|
|
int count = 0;
|
|
|
|
|
(void) el_wgets (self->editline, &count);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static char *
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__make_prompt (EditLine *editline)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self;
|
2015-05-05 08:46:59 +02:00
|
|
|
el_get (editline, EL_CLIENTDATA, &self);
|
2015-05-05 22:35:51 +02:00
|
|
|
if (!self->prompt)
|
|
|
|
|
return "";
|
2015-05-05 08:46:59 +02:00
|
|
|
return self->prompt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static char *
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__make_empty_prompt (EditLine *editline)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
|
(void) editline;
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
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;
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&self->prompt, prompt);
|
2016-03-07 01:08:52 +01:00
|
|
|
|
|
|
|
|
if (self->prompt_shown > 0)
|
|
|
|
|
input_el__redisplay (self);
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_clear_line (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
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);
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__redisplay (self);
|
2015-12-25 04:21:18 +01:00
|
|
|
}
|
2015-05-05 08:46:59 +02:00
|
|
|
|
2015-12-25 04:21:18 +01:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__erase (struct input_el *self)
|
2015-12-25 04:21:18 +01:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
el_set (self->editline, EL_PROMPT, input_el__make_empty_prompt);
|
|
|
|
|
input_el_clear_line (self);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
2015-11-15 00:59:11 +01:00
|
|
|
static bool
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_insert (void *input, const char *s)
|
2015-05-12 03:48:52 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-12-25 04:21:18 +01:00
|
|
|
bool success = !*s || !el_insertstr (self->editline, s);
|
2015-07-06 01:54:02 +02:00
|
|
|
if (self->prompt_shown > 0)
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__redisplay (self);
|
2015-11-15 00:59:11 +01:00
|
|
|
return success;
|
2015-05-12 03:48:52 +02:00
|
|
|
}
|
|
|
|
|
|
2015-12-25 04:21:18 +01:00
|
|
|
static char *
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_get_line (void *input)
|
2015-12-25 04:21:18 +01:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-12-25 04:21:18 +01:00
|
|
|
const LineInfo *info = el_line (self->editline);
|
|
|
|
|
return xstrndup (info->buffer, info->lastchar - info->buffer);
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2016-03-06 17:59:45 +01:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
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)
|
2016-03-06 17:59:45 +01:00
|
|
|
{
|
|
|
|
|
(void) cif;
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el_fn *data = user_data;
|
2016-03-06 17:59:45 +01:00
|
|
|
*(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
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_register_fn (void *input,
|
2016-03-06 17:59:45 +01:00
|
|
|
const char *name, const char *help, input_fn callback, void *user_data)
|
|
|
|
|
{
|
|
|
|
|
void *bound_fn = NULL;
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
|
2016-03-06 17:59:45 +01:00
|
|
|
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,
|
2016-03-07 01:08:52 +01:00
|
|
|
&cif, input_el__forward, data, bound_fn) == FFI_OK);
|
2016-03-06 17:59:45 +01:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2016-03-06 17:59:45 +01:00
|
|
|
el_wset (self->editline, EL_ADDFN, data->name, data->help, bound_fn);
|
|
|
|
|
LIST_PREPEND (self->fns, data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_start (void *input, const char *program_name)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
self->editline = el_init (program_name, stdin, stdout, stderr);
|
|
|
|
|
el_set (self->editline, EL_CLIENTDATA, self);
|
|
|
|
|
el_set (self->editline, EL_PROMPT_ESC,
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__make_prompt, INPUT_START_IGNORE);
|
2015-05-05 08:46:59 +02:00
|
|
|
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);
|
2015-05-08 06:23:38 +02:00
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
self->prompt_shown = 1;
|
|
|
|
|
self->active = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_stop (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
if (self->prompt_shown > 0)
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__erase (self);
|
2015-05-05 08:46:59 +02:00
|
|
|
|
|
|
|
|
el_end (self->editline);
|
|
|
|
|
self->editline = NULL;
|
|
|
|
|
self->active = false;
|
2015-05-12 07:07:02 +02:00
|
|
|
self->prompt_shown = false;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
static void
|
|
|
|
|
input_el_prepare (void *input, bool enabled)
|
|
|
|
|
{
|
|
|
|
|
struct input_el *self = input;
|
|
|
|
|
el_set (self->editline, EL_PREP_TERM, enabled);
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-05 08:46:59 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__save_buffer (struct input_el *self, struct input_el_buffer *buffer)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
|
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
|
2016-03-07 01:08:52 +01:00
|
|
|
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)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
|
if (buffer->saved_line)
|
|
|
|
|
{
|
|
|
|
|
el_winsertstr (self->editline, buffer->saved_line);
|
|
|
|
|
el_cursor (self->editline,
|
|
|
|
|
-(buffer->saved_len - buffer->saved_point));
|
2020-09-02 19:01:31 +02:00
|
|
|
free (buffer->saved_line);
|
|
|
|
|
buffer->saved_line = NULL;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__restore (struct input_el *self)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
|
if (self->current)
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__restore_buffer (self, self->current);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_buffer_switch (void *input, input_buffer_t input_buffer)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
|
|
|
|
struct input_el_buffer *buffer = input_buffer;
|
|
|
|
|
|
2015-05-05 22:35:51 +02:00
|
|
|
if (self->current)
|
2016-03-07 01:08:52 +01:00
|
|
|
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;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_buffer_destroy (void *input, input_buffer_t input_buffer)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
(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;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_hide (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
if (!self->active || self->prompt_shown-- < 1)
|
|
|
|
|
return;
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__save (self);
|
|
|
|
|
input_el__erase (self);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el_show (void *input)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
if (!self->active || ++self->prompt_shown < 1)
|
|
|
|
|
return;
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
input_el__restore (self);
|
2015-05-05 08:46:59 +02:00
|
|
|
el_set (self->editline,
|
2016-03-07 01:08:52 +01:00
|
|
|
EL_PROMPT_ESC, input_el__make_prompt, INPUT_START_IGNORE);
|
|
|
|
|
input_el__redisplay (self);
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
static void
|
2016-03-07 01:08:52 +01:00
|
|
|
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)
|
2015-05-05 08:46:59 +02:00
|
|
|
{
|
|
|
|
|
// We bind the return key to process it how we need to
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input_el *self = input;
|
2015-05-05 08:46:59 +02:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
if (count == 0 && buf[0] == ('D' - 0x40) /* hardcoded VEOF in editline */)
|
2015-05-05 20:18:41 +02:00
|
|
|
{
|
|
|
|
|
el_deletestr (self->editline, 1);
|
2016-03-07 01:08:52 +01:00
|
|
|
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);
|
2015-05-05 20:18:41 +02:00
|
|
|
}
|
2016-03-07 01:08:52 +01:00
|
|
|
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;
|
2015-05-05 08:46:59 +02:00
|
|
|
}
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
#define input_new input_el_new
|
2015-05-05 08:46:59 +02:00
|
|
|
#endif // HAVE_EDITLINE
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
// --- Application data --------------------------------------------------------
|
|
|
|
|
|
2020-10-02 07:09:58 +02:00
|
|
|
// 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.
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2015-04-21 00:29:07 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-11-18 23:03:21 +01:00
|
|
|
// 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.
|
2015-04-19 02:12:59 +02:00
|
|
|
|
2015-04-21 00:04:34 +02:00
|
|
|
/// Callback just before a reference counted object is destroyed
|
|
|
|
|
typedef void (*destroy_cb_fn) (void *object, void *user_data);
|
|
|
|
|
|
2015-11-18 23:03:21 +01:00
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
|
2015-11-21 15:15:49 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-21 00:29:07 +02:00
|
|
|
#define REF_COUNTABLE_HEADER \
|
|
|
|
|
size_t ref_count; /**< Reference count */ \
|
2015-11-18 23:03:21 +01:00
|
|
|
struct weak_ref_link *weak_refs; /**< To remove any weak references */
|
2015-04-21 00:29:07 +02:00
|
|
|
|
|
|
|
|
#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; \
|
2015-11-18 23:03:21 +01:00
|
|
|
LIST_FOR_EACH (struct weak_ref_link, iter, self->weak_refs) \
|
|
|
|
|
{ \
|
|
|
|
|
iter->on_destroy (self, iter->user_data); \
|
|
|
|
|
free (iter); \
|
|
|
|
|
} \
|
2015-04-21 00:29:07 +02:00
|
|
|
name ## _destroy (self); \
|
2015-11-18 23:03:21 +01:00
|
|
|
} \
|
|
|
|
|
\
|
|
|
|
|
static struct weak_ref_link * \
|
|
|
|
|
name ## _weak_ref (struct name *self, destroy_cb_fn cb, void *user_data) \
|
2016-10-27 11:48:48 +02:00
|
|
|
{ return weak_ref (&self->weak_refs, cb, user_data); } \
|
2015-11-18 23:03:21 +01:00
|
|
|
\
|
|
|
|
|
static void \
|
|
|
|
|
name ## _weak_unref (struct name *self, struct weak_ref_link **link) \
|
2015-11-21 15:15:49 +01:00
|
|
|
{ weak_unref (&self->weak_refs, link); }
|
2015-04-21 00:29:07 +02:00
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2016-10-27 17:03:53 +02:00
|
|
|
// 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
|
|
|
|
|
{
|
2016-10-27 19:10:35 +02:00
|
|
|
ISPECT_BOOL, ISPECT_INT, ISPECT_UINT, ISPECT_SIZE, ISPECT_STRING,
|
|
|
|
|
|
|
|
|
|
ISPECT_STR, ///< "struct str"
|
|
|
|
|
ISPECT_STR_MAP, ///< "struct str_map"
|
2016-10-28 00:07:49 +02:00
|
|
|
ISPECT_REF ///< Weakly referenced object
|
2016-10-27 17:03:53 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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
|
2016-10-28 00:07:49 +02:00
|
|
|
|
|
|
|
|
enum ispect_type subtype; ///< STR_MAP subtype
|
|
|
|
|
struct ispect_field *fields; ///< REF target fields
|
|
|
|
|
bool is_list; ///< REF target is a list
|
2016-10-27 17:03:53 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#define ISPECT(object, field, type) \
|
2016-10-28 00:07:49 +02:00
|
|
|
{ #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 },
|
2016-10-27 17:03:53 +02:00
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
struct user_channel
|
|
|
|
|
{
|
|
|
|
|
LIST_HEADER (struct user_channel)
|
|
|
|
|
|
|
|
|
|
struct channel *channel; ///< Reference to channel
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static struct user_channel *
|
2020-10-04 08:32:15 +02:00
|
|
|
user_channel_new (struct channel *channel)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
|
struct user_channel *self = xcalloc (1, sizeof *self);
|
2020-10-04 08:32:15 +02:00
|
|
|
self->channel = channel;
|
2015-04-19 02:12:59 +02:00
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2015-04-21 00:29:07 +02:00
|
|
|
user_channel_destroy (struct user_channel *self)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
2015-04-25 00:36:02 +02:00
|
|
|
// The "channel" reference is weak and this object should get
|
|
|
|
|
// destroyed whenever the user stops being in the channel.
|
2015-04-19 02:12:59 +02:00
|
|
|
free (self);
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
// We keep references to user information in channels and buffers,
|
2015-04-21 00:04:34 +02:00
|
|
|
// and weak references in the name lookup table.
|
2015-04-19 02:12:59 +02:00
|
|
|
|
|
|
|
|
struct user
|
2015-04-18 22:09:05 +02:00
|
|
|
{
|
2015-04-21 00:29:07 +02:00
|
|
|
REF_COUNTABLE_HEADER
|
2015-04-21 00:04:34 +02:00
|
|
|
|
2015-04-18 22:09:05 +02:00
|
|
|
char *nickname; ///< Literal nickname
|
|
|
|
|
bool away; ///< User is away
|
|
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
struct user_channel *channels; ///< Channels the user is on (with us)
|
2015-04-18 22:09:05 +02:00
|
|
|
};
|
|
|
|
|
|
2016-10-28 00:07:49 +02:00
|
|
|
static struct ispect_field g_user_ispect[] =
|
2016-10-27 17:03:53 +02:00
|
|
|
{
|
|
|
|
|
ISPECT( user, nickname, STRING )
|
|
|
|
|
ISPECT( user, away, BOOL )
|
|
|
|
|
{}
|
|
|
|
|
};
|
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
static struct user *
|
2020-10-04 08:32:15 +02:00
|
|
|
user_new (char *nickname)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
|
struct user *self = xcalloc (1, sizeof *self);
|
|
|
|
|
self->ref_count = 1;
|
2020-10-04 08:32:15 +02:00
|
|
|
self->nickname = nickname;
|
2015-04-19 02:12:59 +02:00
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-18 22:09:05 +02:00
|
|
|
static void
|
2015-04-19 02:12:59 +02:00
|
|
|
user_destroy (struct user *self)
|
2015-04-18 22:09:05 +02:00
|
|
|
{
|
|
|
|
|
free (self->nickname);
|
2015-04-19 02:12:59 +02:00
|
|
|
LIST_FOR_EACH (struct user_channel, iter, self->channels)
|
|
|
|
|
user_channel_destroy (iter);
|
|
|
|
|
free (self);
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-21 00:29:07 +02:00
|
|
|
REF_COUNTABLE_METHODS (user)
|
2015-04-19 02:12:59 +02:00
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
struct channel_user
|
|
|
|
|
{
|
|
|
|
|
LIST_HEADER (struct channel_user)
|
|
|
|
|
|
|
|
|
|
struct user *user; ///< Reference to user
|
2020-10-04 08:22:20 +02:00
|
|
|
char *prefixes; ///< Ordered @+... characters
|
2015-04-19 02:12:59 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static struct channel_user *
|
2020-10-04 08:22:20 +02:00
|
|
|
channel_user_new (struct user *user, const char *prefixes)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
|
struct channel_user *self = xcalloc (1, sizeof *self);
|
2020-10-04 08:22:20 +02:00
|
|
|
self->user = user;
|
|
|
|
|
self->prefixes = xstrdup (prefixes);
|
2015-04-19 02:12:59 +02:00
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2015-04-21 00:29:07 +02:00
|
|
|
channel_user_destroy (struct channel_user *self)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
|
user_unref (self->user);
|
2020-10-04 08:22:20 +02:00
|
|
|
free (self->prefixes);
|
2015-04-19 02:12:59 +02:00
|
|
|
free (self);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-04-25 00:36:02 +02:00
|
|
|
// We keep references to channels in their buffers,
|
|
|
|
|
// and weak references in their users and the name lookup table.
|
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
struct channel
|
|
|
|
|
{
|
2015-04-21 00:29:07 +02:00
|
|
|
REF_COUNTABLE_HEADER
|
2015-04-21 00:04:34 +02:00
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
struct server *s; ///< Server
|
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
char *name; ///< Channel name
|
|
|
|
|
char *topic; ///< Channel topic
|
|
|
|
|
|
2015-06-04 22:19:32 +02:00
|
|
|
// 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
|
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
struct channel_user *users; ///< Channel users
|
2017-01-23 23:50:27 +01:00
|
|
|
struct strv names_buf; ///< Buffer for RPL_NAMREPLY
|
2016-10-23 16:29:55 +02:00
|
|
|
size_t users_len; ///< User count
|
2015-07-12 00:30:10 +02:00
|
|
|
|
|
|
|
|
bool left_manually; ///< Don't rejoin on reconnect
|
2020-10-16 16:45:40 +02:00
|
|
|
bool show_names_after_who; ///< RPL_ENDOFWHO delays RPL_ENDOFNAMES
|
2015-04-19 02:12:59 +02:00
|
|
|
};
|
|
|
|
|
|
2016-10-28 00:07:49 +02:00
|
|
|
static struct ispect_field g_channel_ispect[] =
|
2016-10-27 17:03:53 +02:00
|
|
|
{
|
2016-10-28 00:07:49 +02:00
|
|
|
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 )
|
2016-10-27 17:03:53 +02:00
|
|
|
{}
|
|
|
|
|
};
|
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
static struct channel *
|
2020-10-16 16:45:40 +02:00
|
|
|
channel_new (struct server *s, char *name)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
|
struct channel *self = xcalloc (1, sizeof *self);
|
|
|
|
|
self->ref_count = 1;
|
2020-10-16 16:45:40 +02:00
|
|
|
self->s = s;
|
2020-10-04 08:32:15 +02:00
|
|
|
self->name = name;
|
2017-06-22 22:39:39 +02:00
|
|
|
self->no_param_modes = str_make ();
|
|
|
|
|
self->param_modes = str_map_make (free);
|
|
|
|
|
self->names_buf = strv_make ();
|
2015-04-19 02:12:59 +02:00
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2015-04-21 00:04:34 +02:00
|
|
|
channel_destroy (struct channel *self)
|
2015-04-19 02:12:59 +02:00
|
|
|
{
|
|
|
|
|
free (self->name);
|
|
|
|
|
free (self->topic);
|
2015-06-04 22:19:32 +02:00
|
|
|
str_free (&self->no_param_modes);
|
|
|
|
|
str_map_free (&self->param_modes);
|
2015-04-25 00:36:02 +02:00
|
|
|
// Owner has to make sure we have no users by now
|
2016-10-23 16:29:55 +02:00
|
|
|
hard_assert (!self->users && !self->users_len);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&self->names_buf);
|
2015-04-18 22:09:05 +02:00
|
|
|
free (self);
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-21 00:29:07 +02:00
|
|
|
REF_COUNTABLE_METHODS (channel)
|
2015-04-19 02:12:59 +02:00
|
|
|
|
2015-04-18 22:09:05 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-06-28 20:48:43 +02:00
|
|
|
enum formatter_item_type
|
|
|
|
|
{
|
2016-01-31 20:07:20 +01:00
|
|
|
FORMATTER_ITEM_END, ///< Sentinel value for arrays
|
2015-06-28 20:48:43 +02:00
|
|
|
FORMATTER_ITEM_TEXT, ///< Text
|
|
|
|
|
FORMATTER_ITEM_ATTR, ///< Formatting attributes
|
2020-10-11 17:36:21 +02:00
|
|
|
FORMATTER_ITEM_FG_COLOR, ///< Foreground colour
|
|
|
|
|
FORMATTER_ITEM_BG_COLOR, ///< Background colour
|
2015-10-01 21:05:32 +02:00
|
|
|
FORMATTER_ITEM_SIMPLE, ///< Toggle mIRC formatting
|
2015-06-28 20:48:43 +02:00
|
|
|
FORMATTER_ITEM_IGNORE_ATTR ///< Un/set attribute ignoration
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct formatter_item
|
|
|
|
|
{
|
2016-01-31 20:07:20 +01:00
|
|
|
enum formatter_item_type type : 16; ///< Type of this item
|
|
|
|
|
int attribute : 16; ///< Attribute ID
|
2020-10-11 17:36:21 +02:00
|
|
|
int color; ///< Colour
|
2016-01-31 20:07:20 +01:00
|
|
|
char *text; ///< String
|
2015-06-28 20:48:43 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void
|
2016-01-31 20:07:20 +01:00
|
|
|
formatter_item_free (struct formatter_item *self)
|
2015-06-28 20:48:43 +02:00
|
|
|
{
|
|
|
|
|
free (self->text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
struct formatter
|
|
|
|
|
{
|
|
|
|
|
struct app_context *ctx; ///< Application context
|
|
|
|
|
struct server *s; ///< Server
|
|
|
|
|
|
|
|
|
|
struct formatter_item *items; ///< Items
|
2016-01-31 20:07:20 +01:00
|
|
|
size_t items_len; ///< Items used
|
|
|
|
|
size_t items_alloc; ///< Items allocated
|
2015-06-28 20:48:43 +02:00
|
|
|
};
|
|
|
|
|
|
2017-06-22 22:45:25 +02:00
|
|
|
static struct formatter
|
|
|
|
|
formatter_make (struct app_context *ctx, struct server *s)
|
2015-06-28 20:48:43 +02:00
|
|
|
{
|
2017-06-22 22:45:25 +02:00
|
|
|
struct formatter self = { .ctx = ctx, .s = s };
|
|
|
|
|
self.items = xcalloc (sizeof *self.items, (self.items_alloc = 16));
|
|
|
|
|
return self;
|
2015-06-28 20:48:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
formatter_free (struct formatter *self)
|
|
|
|
|
{
|
2016-01-31 20:07:20 +01:00
|
|
|
for (size_t i = 0; i < self->items_len; i++)
|
|
|
|
|
formatter_item_free (&self->items[i]);
|
|
|
|
|
free (self->items);
|
2015-06-28 20:48:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
enum buffer_line_flags
|
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
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
|
2015-06-28 19:56:05 +02:00
|
|
|
BUFFER_LINE_SKIP_FILE = 1 << 3, ///< Don't log this to file
|
2015-08-08 21:17:32 +02:00
|
|
|
BUFFER_LINE_INDENT = 1 << 4, ///< Just indent the line
|
|
|
|
|
BUFFER_LINE_UNIMPORTANT = 1 << 5 ///< Joins, parts, similar spam
|
2015-04-25 13:41:10 +02:00
|
|
|
};
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
struct buffer_line
|
|
|
|
|
{
|
|
|
|
|
LIST_HEADER (struct buffer_line)
|
|
|
|
|
|
|
|
|
|
int flags; ///< Flags
|
|
|
|
|
time_t when; ///< Time of the event
|
2016-01-31 20:07:20 +01:00
|
|
|
struct formatter_item items[]; ///< Line data
|
2015-04-12 04:52:39 +02:00
|
|
|
};
|
|
|
|
|
|
2016-01-31 20:07:20 +01:00
|
|
|
/// Create a new buffer line stealing all data from the provided formatter
|
2015-04-13 00:06:08 +02:00
|
|
|
struct buffer_line *
|
2016-01-17 22:15:48 +01:00
|
|
|
buffer_line_new (struct formatter *f)
|
2015-04-13 00:06:08 +02:00
|
|
|
{
|
2016-01-31 20:07:20 +01:00
|
|
|
// 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);
|
2015-04-13 00:06:08 +02:00
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
buffer_line_destroy (struct buffer_line *self)
|
|
|
|
|
{
|
2016-01-31 20:07:20 +01:00
|
|
|
for (struct formatter_item *iter = self->items; iter->type; iter++)
|
|
|
|
|
formatter_item_free (iter);
|
2015-04-13 00:06:08 +02:00
|
|
|
free (self);
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
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)
|
2015-11-18 23:49:09 +01:00
|
|
|
REF_COUNTABLE_HEADER
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
|
enum buffer_type type; ///< Type of the buffer
|
|
|
|
|
char *name; ///< The name of the buffer
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input *input; ///< API for "input_data"
|
|
|
|
|
input_buffer_t input_data; ///< User interface data
|
2015-04-15 02:10:21 +02:00
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
// Buffer contents:
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
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
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2016-03-26 03:09:29 +01:00
|
|
|
unsigned new_messages_count; ///< # messages since last left
|
|
|
|
|
unsigned new_unimportant_count; ///< How much of that is unimportant
|
2015-06-21 04:00:20 +02:00
|
|
|
bool highlighted; ///< We've been highlighted
|
2016-10-23 16:53:31 +02:00
|
|
|
bool hide_unimportant; ///< Hide unimportant messages
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2015-07-04 15:24:08 +02:00
|
|
|
FILE *log_file; ///< Log file
|
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
// Origin information:
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
struct server *server; ///< Reference to server
|
2015-04-19 02:12:59 +02:00
|
|
|
struct channel *channel; ///< Reference to channel
|
2015-04-30 00:02:14 +02:00
|
|
|
struct user *user; ///< Reference to user
|
2015-04-12 04:52:39 +02:00
|
|
|
};
|
|
|
|
|
|
2016-10-28 00:07:49 +02:00
|
|
|
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 )
|
2016-10-27 17:03:53 +02:00
|
|
|
{}
|
|
|
|
|
};
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
static struct buffer *
|
2016-03-07 01:08:52 +01:00
|
|
|
buffer_new (struct input *input)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
|
|
|
|
struct buffer *self = xcalloc (1, sizeof *self);
|
2015-11-18 23:49:09 +01:00
|
|
|
self->ref_count = 1;
|
2016-03-07 01:08:52 +01:00
|
|
|
self->input = input;
|
|
|
|
|
self->input_data = CALL (input, buffer_new);
|
2015-04-12 04:52:39 +02:00
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
buffer_destroy (struct buffer *self)
|
|
|
|
|
{
|
|
|
|
|
free (self->name);
|
2015-05-05 03:23:53 +02:00
|
|
|
if (self->input_data)
|
2016-03-07 01:08:52 +01:00
|
|
|
{
|
|
|
|
|
#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
|
|
|
|
|
}
|
2015-04-19 02:12:59 +02:00
|
|
|
LIST_FOR_EACH (struct buffer_line, iter, self->lines)
|
|
|
|
|
buffer_line_destroy (iter);
|
2015-07-04 15:24:08 +02:00
|
|
|
if (self->log_file)
|
|
|
|
|
(void) fclose (self->log_file);
|
2015-04-19 02:12:59 +02:00
|
|
|
if (self->user)
|
|
|
|
|
user_unref (self->user);
|
|
|
|
|
if (self->channel)
|
|
|
|
|
channel_unref (self->channel);
|
2015-04-12 04:52:39 +02:00
|
|
|
free (self);
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-18 23:49:09 +01:00
|
|
|
REF_COUNTABLE_METHODS (buffer)
|
|
|
|
|
#define buffer_ref do_not_use_dangerous
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-07-15 23:34:36 +02:00
|
|
|
// The only real purpose of this is to abstract away TLS
|
2015-07-03 20:32:31 +02:00
|
|
|
struct transport
|
|
|
|
|
{
|
|
|
|
|
/// Initialize the transport
|
2015-12-09 00:53:56 +01:00
|
|
|
bool (*init) (struct server *s, const char *hostname, struct error **e);
|
2015-07-03 20:32:31 +02:00
|
|
|
/// Destroy the user data pointer
|
|
|
|
|
void (*cleanup) (struct server *s);
|
|
|
|
|
|
|
|
|
|
/// The underlying socket may have become readable, update `read_buffer'
|
2016-01-06 23:37:30 +01:00
|
|
|
enum socket_io_result (*try_read) (struct server *s);
|
2015-07-03 20:32:31 +02:00
|
|
|
/// The underlying socket may have become writeable, flush `write_buffer'
|
2016-01-06 23:37:30 +01:00
|
|
|
enum socket_io_result (*try_write) (struct server *s);
|
2015-07-03 20:32:31 +02:00
|
|
|
/// 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);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-05-08 17:39:26 +02:00
|
|
|
enum server_state
|
|
|
|
|
{
|
|
|
|
|
IRC_DISCONNECTED, ///< Not connected
|
2015-05-09 22:10:58 +02:00
|
|
|
IRC_CONNECTING, ///< Connecting to the server
|
2015-05-08 17:39:26 +02:00
|
|
|
IRC_CONNECTED, ///< Trying to register
|
2015-07-03 20:32:31 +02:00
|
|
|
IRC_REGISTERED, ///< We can chat now
|
|
|
|
|
IRC_CLOSING, ///< Flushing output before shutdown
|
|
|
|
|
IRC_HALF_CLOSED ///< Connection shutdown from our side
|
2015-05-08 17:39:26 +02:00
|
|
|
};
|
|
|
|
|
|
2015-06-20 22:42:38 +02:00
|
|
|
/// 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);
|
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
struct server
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-11-18 23:49:09 +01:00
|
|
|
REF_COUNTABLE_HEADER
|
2015-04-30 00:02:14 +02:00
|
|
|
struct app_context *ctx; ///< Application context
|
2015-05-09 23:30:04 +02:00
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
char *name; ///< Server identifier
|
|
|
|
|
struct buffer *buffer; ///< The buffer for this server
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *config; ///< Configuration root
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
// Connection:
|
|
|
|
|
|
2015-05-08 17:39:26 +02:00
|
|
|
enum server_state state; ///< Connection state
|
2015-05-09 22:10:58 +02:00
|
|
|
struct connector *connector; ///< Connection establisher
|
2015-07-20 23:31:26 +02:00
|
|
|
struct socks_connector *socks_conn; ///< SOCKS connection establisher
|
2015-07-17 21:18:05 +02:00
|
|
|
unsigned reconnect_attempt; ///< Number of reconnect attempt
|
2015-05-16 12:45:39 +02:00
|
|
|
bool manual_disconnect; ///< Don't reconnect after disconnect
|
2015-05-08 17:39:26 +02:00
|
|
|
|
|
|
|
|
int socket; ///< Socket FD of the server
|
2015-04-12 04:52:39 +02:00
|
|
|
struct str read_buffer; ///< Input yet to be processed
|
2015-07-03 20:32:31 +02:00
|
|
|
struct str write_buffer; ///< Outut yet to be be sent out
|
|
|
|
|
struct poller_fd socket_event; ///< We can read from the socket
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-07-03 20:32:31 +02:00
|
|
|
struct transport *transport; ///< Transport method
|
|
|
|
|
void *transport_data; ///< Transport data
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2015-06-20 21:10:50 +02:00
|
|
|
// 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
|
2015-07-18 14:12:34 +02:00
|
|
|
struct poller_timer autojoin_tmr; ///< Re/join channels as appropriate
|
2015-04-19 02:12:59 +02:00
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
// IRC:
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2015-06-20 21:10:50 +02:00
|
|
|
// TODO: an output queue to prevent excess floods (this will be needed
|
|
|
|
|
// especially for away status polling)
|
|
|
|
|
|
2015-06-23 22:10:16 +02:00
|
|
|
bool rehashing; ///< Rehashing IRC identifiers
|
|
|
|
|
|
2015-04-19 02:12:59 +02:00
|
|
|
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
|
2015-04-18 22:09:05 +02:00
|
|
|
|
2015-04-20 22:09:56 +02:00
|
|
|
struct user *irc_user; ///< Our own user
|
2015-07-09 02:46:31 +02:00
|
|
|
int nick_counter; ///< Iterates "nicks" when registering
|
2015-06-07 04:20:39 +02:00
|
|
|
struct str irc_user_mode; ///< Our current user modes
|
2015-04-20 22:09:56 +02:00
|
|
|
char *irc_user_host; ///< Our current user@host
|
2015-11-22 23:04:51 +01:00
|
|
|
bool autoaway_active; ///< Autoaway is currently active
|
2015-04-15 16:54:05 +02:00
|
|
|
|
2020-10-16 16:45:40 +02:00
|
|
|
bool cap_echo_message; ///< Whether the server echoes messages
|
|
|
|
|
bool cap_away_notify; ///< Whether we get AWAY notifications
|
2015-06-20 21:38:04 +02:00
|
|
|
|
2015-06-01 21:45:04 +02:00
|
|
|
// Server-specific information (from RPL_ISUPPORT):
|
|
|
|
|
|
2015-06-20 22:42:38 +02:00
|
|
|
irc_tolower_fn irc_tolower; ///< Server tolower()
|
|
|
|
|
irc_strxfrm_fn irc_strxfrm; ///< Server strxfrm()
|
2015-06-01 21:45:04 +02:00
|
|
|
|
|
|
|
|
char *irc_chantypes; ///< Channel types (name prefixes)
|
|
|
|
|
char *irc_idchan_prefixes; ///< Prefixes for "safe channels"
|
|
|
|
|
char *irc_statusmsg; ///< Prefixes for channel targets
|
|
|
|
|
|
2015-06-03 23:17:10 +02:00
|
|
|
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
|
|
|
|
|
|
2015-06-01 21:45:04 +02:00
|
|
|
char *irc_chanuser_prefixes; ///< Channel user prefixes
|
|
|
|
|
char *irc_chanuser_modes; ///< Channel user modes
|
|
|
|
|
|
2015-06-17 21:19:09 +02:00
|
|
|
unsigned irc_max_modes; ///< Max parametrized modes per command
|
2015-04-30 00:02:14 +02:00
|
|
|
};
|
|
|
|
|
|
2016-10-28 00:07:49 +02:00
|
|
|
static struct ispect_field g_server_ispect[] =
|
2016-10-27 17:03:53 +02:00
|
|
|
{
|
2016-10-28 00:07:49 +02:00
|
|
|
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 )
|
2016-10-27 18:46:27 +02:00
|
|
|
|
|
|
|
|
// TODO: either rename the underlying field or fix the plugins
|
2016-10-27 20:58:14 +02:00
|
|
|
{ "user", offsetof (struct server, irc_user),
|
2016-10-28 00:07:49 +02:00
|
|
|
ISPECT_REF, 0, g_user_ispect, false },
|
2016-10-27 20:58:14 +02:00
|
|
|
{ "user_mode", offsetof (struct server, irc_user_mode),
|
2016-10-28 00:07:49 +02:00
|
|
|
ISPECT_STR, 0, NULL, false },
|
2016-10-27 18:46:27 +02:00
|
|
|
|
2016-10-27 17:03:53 +02:00
|
|
|
{}
|
|
|
|
|
};
|
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
static void on_irc_timeout (void *user_data);
|
2015-05-09 22:10:58 +02:00
|
|
|
static void on_irc_ping_timeout (void *user_data);
|
2015-07-18 14:12:34 +02:00
|
|
|
static void on_irc_autojoin_timeout (void *user_data);
|
2015-05-09 22:10:58 +02:00
|
|
|
static void irc_initiate_connect (struct server *s);
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2015-06-20 22:42:38 +02:00
|
|
|
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_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_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);
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-18 23:49:09 +01:00
|
|
|
static struct server *
|
|
|
|
|
server_new (struct poller *poller)
|
2015-04-30 00:02:14 +02:00
|
|
|
{
|
2015-11-18 23:49:09 +01:00
|
|
|
struct server *self = xcalloc (1, sizeof *self);
|
|
|
|
|
self->ref_count = 1;
|
2015-05-16 12:33:59 +02:00
|
|
|
|
2015-05-08 17:39:26 +02:00
|
|
|
self->socket = -1;
|
2017-06-22 22:39:39 +02:00
|
|
|
self->read_buffer = str_make ();
|
|
|
|
|
self->write_buffer = str_make ();
|
2015-05-08 17:39:26 +02:00
|
|
|
self->state = IRC_DISCONNECTED;
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->timeout_tmr = poller_timer_make (poller);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->timeout_tmr.dispatcher = on_irc_timeout;
|
|
|
|
|
self->timeout_tmr.user_data = self;
|
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->ping_tmr = poller_timer_make (poller);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->ping_tmr.dispatcher = on_irc_ping_timeout;
|
|
|
|
|
self->ping_tmr.user_data = self;
|
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->reconnect_tmr = poller_timer_make (poller);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->reconnect_tmr.dispatcher = (poller_timer_fn) irc_initiate_connect;
|
|
|
|
|
self->reconnect_tmr.user_data = self;
|
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->autojoin_tmr = poller_timer_make (poller);
|
2015-07-18 14:12:34 +02:00
|
|
|
self->autojoin_tmr.dispatcher = on_irc_autojoin_timeout;
|
|
|
|
|
self->autojoin_tmr.user_data = self;
|
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->irc_users = str_map_make (NULL);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->irc_users.key_xfrm = irc_strxfrm;
|
2017-06-22 22:39:39 +02:00
|
|
|
self->irc_channels = str_map_make (NULL);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->irc_channels.key_xfrm = irc_strxfrm;
|
2017-06-22 22:39:39 +02:00
|
|
|
self->irc_buffer_map = str_map_make (NULL);
|
2015-06-20 21:10:50 +02:00
|
|
|
self->irc_buffer_map.key_xfrm = irc_strxfrm;
|
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->irc_user_mode = str_make ();
|
2015-06-07 04:20:39 +02:00
|
|
|
|
2015-06-20 22:42:38 +02:00
|
|
|
server_init_specifics (self);
|
2015-11-18 23:49:09 +01:00
|
|
|
return self;
|
2015-04-30 00:02:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2015-11-18 23:49:09 +01:00
|
|
|
server_destroy (struct server *self)
|
2015-04-30 00:02:14 +02:00
|
|
|
{
|
2015-06-20 21:10:50 +02:00
|
|
|
free (self->name);
|
|
|
|
|
|
2015-05-09 22:10:58 +02:00
|
|
|
if (self->connector)
|
|
|
|
|
{
|
|
|
|
|
connector_free (self->connector);
|
|
|
|
|
free (self->connector);
|
|
|
|
|
}
|
2015-07-20 23:31:26 +02:00
|
|
|
if (self->socks_conn)
|
|
|
|
|
{
|
|
|
|
|
socks_connector_free (self->socks_conn);
|
|
|
|
|
free (self->socks_conn);
|
|
|
|
|
}
|
2015-07-03 20:32:31 +02:00
|
|
|
|
|
|
|
|
if (self->transport
|
|
|
|
|
&& self->transport->cleanup)
|
|
|
|
|
self->transport->cleanup (self);
|
|
|
|
|
|
2015-05-08 17:39:26 +02:00
|
|
|
if (self->socket != -1)
|
2015-04-30 00:02:14 +02:00
|
|
|
{
|
2015-07-03 20:32:31 +02:00
|
|
|
poller_fd_reset (&self->socket_event);
|
2017-05-06 21:35:44 +02:00
|
|
|
xclose (self->socket);
|
2015-04-30 00:02:14 +02:00
|
|
|
}
|
|
|
|
|
str_free (&self->read_buffer);
|
2015-07-03 20:32:31 +02:00
|
|
|
str_free (&self->write_buffer);
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2015-07-14 06:50:39 +02:00
|
|
|
poller_timer_reset (&self->ping_tmr);
|
|
|
|
|
poller_timer_reset (&self->timeout_tmr);
|
|
|
|
|
poller_timer_reset (&self->reconnect_tmr);
|
2015-07-18 14:12:34 +02:00
|
|
|
poller_timer_reset (&self->autojoin_tmr);
|
2015-07-14 06:50:39 +02:00
|
|
|
|
2015-06-20 21:10:50 +02:00
|
|
|
str_map_free (&self->irc_users);
|
|
|
|
|
str_map_free (&self->irc_channels);
|
|
|
|
|
str_map_free (&self->irc_buffer_map);
|
2015-05-15 20:05:27 +02:00
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
if (self->irc_user)
|
|
|
|
|
user_unref (self->irc_user);
|
2015-06-07 04:20:39 +02:00
|
|
|
str_free (&self->irc_user_mode);
|
2015-04-30 00:02:14 +02:00
|
|
|
free (self->irc_user_host);
|
|
|
|
|
|
2015-06-20 22:42:38 +02:00
|
|
|
server_free_specifics (self);
|
2015-05-16 12:33:59 +02:00
|
|
|
free (self);
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-18 23:49:09 +01:00
|
|
|
REF_COUNTABLE_METHODS (server)
|
|
|
|
|
#define server_ref do_not_use_dangerous
|
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-11-19 14:23:10 +01:00
|
|
|
struct plugin
|
|
|
|
|
{
|
|
|
|
|
LIST_HEADER (struct plugin)
|
|
|
|
|
|
|
|
|
|
char *name; ///< Name of the plugin
|
|
|
|
|
struct plugin_vtable *vtable; ///< Methods
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct plugin_vtable
|
|
|
|
|
{
|
2020-10-20 02:02:09 +02:00
|
|
|
/// Collect garbage
|
|
|
|
|
void (*gc) (struct plugin *self);
|
2015-11-19 14:23:10 +01:00
|
|
|
/// 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-11-21 18:40:07 +01:00
|
|
|
// 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
|
2015-11-19 19:09:05 +01:00
|
|
|
{
|
2015-11-21 18:40:07 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2015-11-19 19:09:05 +01:00
|
|
|
|
2015-11-21 18:40:07 +01:00
|
|
|
// Otherwise fast-forward to the last entry that precedes us
|
|
|
|
|
struct hook *before = list;
|
2015-11-24 02:32:11 +01:00
|
|
|
while (before->next && before->next->priority < item->priority)
|
2015-11-21 18:40:07 +01:00
|
|
|
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
|
2015-11-19 19:09:05 +01:00
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
|
{
|
2015-11-21 18:40:07 +01:00
|
|
|
struct hook super; ///< Common hook fields
|
2015-11-19 19:09:05 +01:00
|
|
|
|
|
|
|
|
/// 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);
|
|
|
|
|
};
|
|
|
|
|
|
2016-10-28 01:01:27 +02:00
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2016-01-15 01:44:35 +01:00
|
|
|
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,
|
2017-01-23 23:50:27 +01:00
|
|
|
struct completion *data, const char *word, struct strv *output);
|
2016-01-15 01:44:35 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
struct app_context
|
|
|
|
|
{
|
2015-07-05 15:57:53 +02:00
|
|
|
char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes
|
|
|
|
|
|
2015-04-30 00:02:14 +02:00
|
|
|
// Configuration:
|
|
|
|
|
|
2015-05-02 23:00:34 +02:00
|
|
|
struct config config; ///< Program configuration
|
2015-04-30 00:02:14 +02:00
|
|
|
char *attrs[ATTR_COUNT]; ///< Terminal attributes
|
|
|
|
|
bool isolate_buffers; ///< Isolate global/server buffers
|
2015-07-05 16:28:27 +02:00
|
|
|
bool beep_on_highlight; ///< Beep on highlight
|
2015-07-05 17:02:11 +02:00
|
|
|
bool logging; ///< Logging to file enabled
|
2015-07-26 22:21:08 +02:00
|
|
|
bool show_all_prefixes; ///< Show all prefixes before nicks
|
2016-03-21 00:30:59 +01:00
|
|
|
bool word_wrapping; ///< Enable simple word wrapping
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
struct str_map servers; ///< Our servers
|
2015-04-30 00:02:14 +02:00
|
|
|
|
|
|
|
|
// Events:
|
|
|
|
|
|
|
|
|
|
struct poller_fd tty_event; ///< Terminal input event
|
|
|
|
|
struct poller_fd signal_event; ///< Signal FD event
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-07-04 15:43:45 +02:00
|
|
|
struct poller_timer flush_timer; ///< Flush all open files (e.g. logs)
|
2015-07-11 06:10:46 +02:00
|
|
|
struct poller_timer date_chg_tmr; ///< Print a date change
|
2015-11-22 23:04:51 +01:00
|
|
|
struct poller_timer autoaway_tmr; ///< Autoaway timer
|
2015-07-04 15:43:45 +02:00
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
struct poller poller; ///< Manages polled descriptors
|
|
|
|
|
bool quitting; ///< User requested quitting
|
|
|
|
|
bool polling; ///< The event loop is running
|
|
|
|
|
|
|
|
|
|
// Buffers:
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
|
struct buffer *buffers; ///< All our buffers in order
|
|
|
|
|
struct buffer *buffers_tail; ///< The tail of our buffers
|
|
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
struct buffer *global_buffer; ///< The global buffer
|
|
|
|
|
struct buffer *current_buffer; ///< The current buffer
|
2015-04-27 22:51:40 +02:00
|
|
|
struct buffer *last_buffer; ///< Last used buffer
|
|
|
|
|
|
2015-05-15 20:05:27 +02:00
|
|
|
struct str_map buffers_by_name; ///< Buffers by name
|
2015-04-18 22:09:05 +02:00
|
|
|
|
2016-01-15 03:49:24 +01:00
|
|
|
unsigned backlog_limit; ///< Limit for buffer lines
|
2015-04-12 04:52:39 +02:00
|
|
|
time_t last_displayed_msg_time; ///< Time of last displayed message
|
|
|
|
|
|
2015-04-13 00:06:08 +02:00
|
|
|
// Terminal:
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
|
iconv_t term_to_utf8; ///< Terminal encoding to UTF-8
|
|
|
|
|
iconv_t term_from_utf8; ///< UTF-8 to terminal encoding
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
struct input *input; ///< User interface
|
2015-05-12 03:48:52 +02:00
|
|
|
|
2016-10-23 17:34:52 +02:00
|
|
|
struct poller_idle prompt_event; ///< Deferred prompt refresh
|
2016-01-07 22:12:29 +01:00
|
|
|
struct poller_idle input_event; ///< Pending input event
|
2017-01-23 23:50:27 +01:00
|
|
|
struct strv pending_input; ///< Pending input lines
|
2016-01-07 22:12:29 +01:00
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
int *nick_palette; ///< A 256-colour palette for nicknames
|
2015-08-10 07:51:03 +02:00
|
|
|
size_t nick_palette_len; ///< Number of entries in nick_palette
|
|
|
|
|
|
2015-05-12 03:48:52 +02:00
|
|
|
bool awaiting_mirc_escape; ///< Awaiting a mIRC attribute escape
|
2015-11-15 01:03:29 +01:00
|
|
|
bool in_bracketed_paste; ///< User is pasting some content
|
2015-11-15 01:23:32 +01:00
|
|
|
struct str input_buffer; ///< Buffered pasted content
|
2015-11-15 01:03:29 +01:00
|
|
|
|
2015-08-08 19:36:34 +02:00
|
|
|
bool running_backlog_helper; ///< Running a backlog helper
|
2015-12-25 04:21:18 +01:00
|
|
|
bool running_editor; ///< Running editor for the input
|
|
|
|
|
char *editor_filename; ///< The file being edited by user
|
2015-11-15 15:36:03 +01:00
|
|
|
int terminal_suspended; ///< Terminal suspension level
|
2015-11-19 14:23:10 +01:00
|
|
|
|
|
|
|
|
struct plugin *plugins; ///< Loaded plugins
|
2015-11-21 18:40:07 +01:00
|
|
|
struct hook *input_hooks; ///< Input hooks
|
|
|
|
|
struct hook *irc_hooks; ///< IRC hooks
|
2016-10-28 01:01:27 +02:00
|
|
|
struct hook *prompt_hooks; ///< Prompt hooks
|
2016-01-15 01:44:35 +01:00
|
|
|
struct hook *completion_hooks; ///< Autocomplete hooks
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
*g_ctx;
|
|
|
|
|
|
2016-10-28 00:07:49 +02:00
|
|
|
static struct ispect_field g_ctx_ispect[] =
|
2016-10-27 20:02:25 +02:00
|
|
|
{
|
2016-10-28 00:07:49 +02:00
|
|
|
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 )
|
2016-10-27 20:02:25 +02:00
|
|
|
{}
|
|
|
|
|
};
|
|
|
|
|
|
2015-08-10 07:51:03 +02:00
|
|
|
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 < 6 * 6 * 6; x++)
|
|
|
|
|
{
|
2018-01-08 21:53:42 +01:00
|
|
|
// FIXME this isn't exactly right, the values aren't linear
|
2015-08-10 07:51:03 +02:00
|
|
|
int r = x / 36;
|
|
|
|
|
int g = (x / 6) % 6;
|
|
|
|
|
int b = (x % 6);
|
|
|
|
|
|
|
|
|
|
// Use the luma value of colours within the cube to filter colours that
|
|
|
|
|
// look okay-ish on terminals with both black and white backgrounds
|
|
|
|
|
double luma = 0.2126 * r / 6. + 0.7152 * g / 6. + 0.0722 * b / 6.;
|
|
|
|
|
if (luma >= .3 && luma <= .5)
|
|
|
|
|
table[len_counter++] = 16 + x;
|
|
|
|
|
}
|
|
|
|
|
*len = len_counter;
|
|
|
|
|
return table;
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-26 14:07:09 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
static void
|
|
|
|
|
app_context_init (struct app_context *self)
|
|
|
|
|
{
|
|
|
|
|
memset (self, 0, sizeof *self);
|
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->config = config_make ();
|
2015-04-19 02:12:59 +02:00
|
|
|
poller_init (&self->poller);
|
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->servers = str_map_make ((str_map_free_fn) server_unref);
|
2015-06-01 21:45:36 +02:00
|
|
|
self->servers.key_xfrm = tolower_ascii_strxfrm;
|
2015-04-30 00:02:14 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
self->buffers_by_name = str_map_make (NULL);
|
2015-06-01 21:45:36 +02:00
|
|
|
self->buffers_by_name.key_xfrm = tolower_ascii_strxfrm;
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2016-01-15 03:49:24 +01:00
|
|
|
// So that we don't lose the logo shortly after startup
|
|
|
|
|
self->backlog_limit = 1000;
|
2015-04-12 04:52:39 +02:00
|
|
|
self->last_displayed_msg_time = time (NULL);
|
|
|
|
|
|
2016-03-26 14:07:09 +01:00
|
|
|
char *native = nl_langinfo (CODESET);
|
|
|
|
|
if (!app_iconv_open (&self->term_from_utf8, native, "UTF-8")
|
2016-04-03 04:05:04 +02:00
|
|
|
|| !app_iconv_open (&self->term_to_utf8, "UTF-8", native))
|
2015-04-12 04:52:39 +02:00
|
|
|
exit_fatal ("creating the UTF-8 conversion object failed: %s",
|
|
|
|
|
strerror (errno));
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
self->input = input_new ();
|
2016-03-10 00:06:28 +01:00
|
|
|
self->input->user_data = self;
|
2017-06-22 22:39:39 +02:00
|
|
|
self->pending_input = strv_make ();
|
|
|
|
|
self->input_buffer = str_make ();
|
2015-08-10 07:51:03 +02:00
|
|
|
|
|
|
|
|
self->nick_palette =
|
|
|
|
|
filter_color_cube_for_acceptable_nick_colors (&self->nick_palette_len);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
app_context_free (struct app_context *self)
|
|
|
|
|
{
|
2015-11-19 14:23:10 +01:00
|
|
|
// Plugins can try to use of the other fields when destroyed
|
|
|
|
|
LIST_FOR_EACH (struct plugin, iter, self->plugins)
|
|
|
|
|
plugin_destroy (iter);
|
|
|
|
|
|
2015-05-02 23:00:34 +02:00
|
|
|
config_free (&self->config);
|
2015-04-29 21:25:57 +02:00
|
|
|
for (size_t i = 0; i < ATTR_COUNT; i++)
|
2015-07-05 15:57:53 +02:00
|
|
|
{
|
|
|
|
|
free (self->attrs_defaults[i]);
|
2015-04-29 21:25:57 +02:00
|
|
|
free (self->attrs[i]);
|
2015-07-05 15:57:53 +02:00
|
|
|
}
|
2015-04-15 16:54:05 +02:00
|
|
|
|
2015-04-24 23:30:48 +02:00
|
|
|
LIST_FOR_EACH (struct buffer, iter, self->buffers)
|
2015-06-28 23:00:46 +02:00
|
|
|
{
|
|
|
|
|
#ifdef HAVE_READLINE
|
2016-03-07 01:08:52 +01:00
|
|
|
// We can use the user interface here; see buffer_destroy()
|
|
|
|
|
CALL_ (self->input, buffer_destroy, iter->input_data);
|
2015-06-28 23:00:46 +02:00
|
|
|
iter->input_data = NULL;
|
|
|
|
|
#endif // HAVE_READLINE
|
2015-11-18 23:49:09 +01:00
|
|
|
buffer_unref (iter);
|
2015-06-28 23:00:46 +02:00
|
|
|
}
|
2015-04-24 23:30:48 +02:00
|
|
|
str_map_free (&self->buffers_by_name);
|
|
|
|
|
|
2015-05-16 12:33:59 +02:00
|
|
|
str_map_free (&self->servers);
|
2015-04-19 02:12:59 +02:00
|
|
|
poller_free (&self->poller);
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
iconv_close (self->term_from_utf8);
|
|
|
|
|
iconv_close (self->term_to_utf8);
|
2015-04-13 00:06:08 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (self->input, destroy);
|
2017-01-23 23:50:27 +01:00
|
|
|
strv_free (&self->pending_input);
|
2015-11-15 01:23:32 +01:00
|
|
|
str_free (&self->input_buffer);
|
2015-12-25 04:21:18 +01:00
|
|
|
|
|
|
|
|
free (self->editor_filename);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
2016-10-23 17:34:52 +02:00
|
|
|
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);
|
|
|
|
|
}
|
2015-04-15 02:10:21 +02:00
|
|
|
|
2015-05-02 23:00:34 +02:00
|
|
|
// --- Configuration -----------------------------------------------------------
|
|
|
|
|
|
2015-07-11 04:28:34 +02:00
|
|
|
static void
|
2015-08-17 00:08:19 +02:00
|
|
|
on_config_debug_mode_change (struct config_item *item)
|
2015-07-11 04:28:34 +02:00
|
|
|
{
|
|
|
|
|
g_debug_mode = item->value.boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-26 22:21:08 +02:00
|
|
|
static void
|
2015-08-17 00:08:19 +02:00
|
|
|
on_config_show_all_prefixes_change (struct config_item *item)
|
2015-07-26 22:21:08 +02:00
|
|
|
{
|
|
|
|
|
struct app_context *ctx = item->user_data;
|
|
|
|
|
ctx->show_all_prefixes = item->value.boolean;
|
|
|
|
|
refresh_prompt (ctx);
|
|
|
|
|
}
|
|
|
|
|
|
2016-01-15 03:49:24 +01:00
|
|
|
static void on_config_backlog_limit_change (struct config_item *item);
|
2015-08-17 00:08:19 +02:00
|
|
|
static void on_config_attribute_change (struct config_item *item);
|
|
|
|
|
static void on_config_logging_change (struct config_item *item);
|
2015-05-02 23:00:34 +02:00
|
|
|
|
2015-07-05 16:28:27 +02:00
|
|
|
#define TRIVIAL_BOOLEAN_ON_CHANGE(name) \
|
|
|
|
|
static void \
|
2015-08-17 00:08:19 +02:00
|
|
|
on_config_ ## name ## _change (struct config_item *item) \
|
2015-07-05 16:28:27 +02:00
|
|
|
{ \
|
|
|
|
|
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)
|
2016-03-21 00:30:59 +01:00
|
|
|
TRIVIAL_BOOLEAN_ON_CHANGE (word_wrapping)
|
2015-07-05 16:28:27 +02:00
|
|
|
|
2015-07-11 04:28:34 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-05-02 23:00:34 +02:00
|
|
|
static bool
|
|
|
|
|
config_validate_nonjunk_string
|
2015-08-17 00:08:19 +02:00
|
|
|
(const struct config_item *item, struct error **e)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
|
|
|
|
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];
|
2020-10-02 06:52:11 +02:00
|
|
|
if (iscntrl_ascii (c))
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
|
|
|
|
error_set (e, "control characters are not allowed");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-14 06:22:58 +02:00
|
|
|
static bool
|
|
|
|
|
config_validate_addresses
|
2015-08-17 00:08:19 +02:00
|
|
|
(const struct config_item *item, struct error **e)
|
2015-05-14 06:22:58 +02:00
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-02 23:00:34 +02:00
|
|
|
static bool
|
|
|
|
|
config_validate_nonnegative
|
2015-08-17 00:08:19 +02:00
|
|
|
(const struct config_item *item, struct error **e)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-08 23:27:21 +02:00
|
|
|
static struct config_schema g_config_server[] =
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-07-09 02:46:31 +02:00
|
|
|
{ .name = "nicks",
|
2015-05-02 23:00:34 +02:00
|
|
|
.comment = "IRC nickname",
|
2015-07-09 02:46:31 +02:00
|
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
2015-05-02 23:00:34 +02:00
|
|
|
.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 },
|
|
|
|
|
|
2015-05-14 06:22:58 +02:00
|
|
|
{ .name = "addresses",
|
|
|
|
|
.comment = "Addresses of the IRC network (e.g. \"irc.net:6667\")",
|
|
|
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
|
|
|
|
.validate = config_validate_addresses },
|
2015-05-20 21:18:15 +02:00
|
|
|
{ .name = "password",
|
|
|
|
|
.comment = "Password to connect to the server, if any",
|
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
|
|
|
|
.validate = config_validate_nonjunk_string },
|
2016-02-01 21:57:43 +01:00
|
|
|
// 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,
|
2020-10-16 16:45:40 +02:00
|
|
|
.default_ = "\"multi-prefix,invite-notify,server-time,echo-message,"
|
|
|
|
|
"message-tags,away-notify\"" },
|
2015-05-02 23:00:34 +02:00
|
|
|
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls",
|
2015-07-15 23:34:36 +02:00
|
|
|
.comment = "Whether to use TLS",
|
2015-05-02 23:00:34 +02:00
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
|
.default_ = "off" },
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls_cert",
|
2015-07-15 23:34:36 +02:00
|
|
|
.comment = "Client TLS certificate (PEM)",
|
2015-05-02 23:00:34 +02:00
|
|
|
.type = CONFIG_ITEM_STRING },
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls_verify",
|
2015-05-02 23:00:34 +02:00
|
|
|
.comment = "Whether to verify certificates",
|
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
|
.default_ = "on" },
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls_ca_file",
|
2015-05-02 23:00:34 +02:00
|
|
|
.comment = "OpenSSL CA bundle file",
|
|
|
|
|
.type = CONFIG_ITEM_STRING },
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls_ca_path",
|
2015-05-02 23:00:34 +02:00
|
|
|
.comment = "OpenSSL CA bundle path",
|
|
|
|
|
.type = CONFIG_ITEM_STRING },
|
2015-07-28 20:31:42 +02:00
|
|
|
{ .name = "tls_ciphers",
|
2015-07-12 17:15:33 +02:00
|
|
|
.comment = "OpenSSL cipher preference list",
|
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
|
|
|
|
.default_ = "\"DEFAULT:!MEDIUM:!LOW\"" },
|
2015-05-02 23:00:34 +02:00
|
|
|
|
2015-07-11 06:16:53 +02:00
|
|
|
{ .name = "autoconnect",
|
|
|
|
|
.comment = "Connect automatically on startup",
|
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
|
.default_ = "on" },
|
2015-05-02 23:00:34 +02:00
|
|
|
{ .name = "autojoin",
|
2016-12-30 07:49:10 +01:00
|
|
|
.comment = "Channels to join on start (e.g. \"#abc,#def key,#ghi\")",
|
2015-05-02 23:00:34 +02:00
|
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
|
|
|
|
.validate = config_validate_nonjunk_string },
|
2015-07-18 13:23:22 +02:00
|
|
|
{ .name = "command",
|
|
|
|
|
.comment = "Command to execute after a successful connect",
|
|
|
|
|
.type = CONFIG_ITEM_STRING },
|
2015-07-18 14:12:34 +02:00
|
|
|
{ .name = "command_delay",
|
|
|
|
|
.comment = "Delay between executing \"command\" and joining channels",
|
|
|
|
|
.type = CONFIG_ITEM_INTEGER,
|
|
|
|
|
.validate = config_validate_nonnegative,
|
|
|
|
|
.default_ = "0" },
|
2015-05-02 23:00:34 +02:00
|
|
|
{ .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,
|
2015-05-16 12:45:39 +02:00
|
|
|
.validate = config_validate_nonnegative,
|
2015-05-02 23:00:34 +02:00
|
|
|
.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 },
|
|
|
|
|
{}
|
|
|
|
|
};
|
|
|
|
|
|
2015-05-08 23:27:21 +02:00
|
|
|
static struct config_schema g_config_behaviour[] =
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
|
|
|
|
{ .name = "isolate_buffers",
|
|
|
|
|
.comment = "Don't leak messages from the server and global buffers",
|
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
2015-07-05 16:28:27 +02:00
|
|
|
.default_ = "off",
|
|
|
|
|
.on_change = on_config_isolate_buffers_change },
|
2015-06-28 19:41:31 +02:00
|
|
|
{ .name = "beep_on_highlight",
|
|
|
|
|
.comment = "Beep when highlighted or on a new invisible PM",
|
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
2015-07-05 16:28:27 +02:00
|
|
|
.default_ = "on",
|
|
|
|
|
.on_change = on_config_beep_on_highlight_change },
|
2015-07-26 22:21:08 +02:00
|
|
|
{ .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 },
|
2016-03-21 00:30:59 +01:00
|
|
|
{ .name = "word_wrapping",
|
|
|
|
|
.comment = "Enable simple word wrapping in buffers",
|
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
|
.default_ = "on",
|
|
|
|
|
.on_change = on_config_word_wrapping_change },
|
2016-04-10 22:13:53 +02:00
|
|
|
{ .name = "date_change_line",
|
|
|
|
|
.comment = "Input to strftime(3) for the date change line",
|
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
|
|
|
|
.default_ = "\"%F\"" },
|
2016-04-21 23:58:44 +02:00
|
|
|
{ .name = "read_marker_char",
|
|
|
|
|
.comment = "The character to use for the read marker line",
|
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
|
|
|
|
.default_ = "\"-\"",
|
|
|
|
|
.validate = config_validate_nonjunk_string },
|
2015-07-04 15:24:08 +02:00
|
|
|
{ .name = "logging",
|
|
|
|
|
.comment = "Log buffer contents to file",
|
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
2015-07-05 17:02:11 +02:00
|
|
|
.default_ = "off",
|
|
|
|
|
.on_change = on_config_logging_change },
|
2015-07-11 02:45:24 +02:00
|
|
|
{ .name = "save_on_quit",
|
|
|
|
|
.comment = "Save configuration before quitting",
|
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
|
.default_ = "on" },
|
2015-07-11 04:28:34 +02:00
|
|
|
{ .name = "debug_mode",
|
|
|
|
|
.comment = "Produce some debugging output",
|
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
|
|
|
|
.default_ = "off",
|
|
|
|
|
.on_change = on_config_debug_mode_change },
|
2015-07-17 21:18:05 +02:00
|
|
|
|
2016-01-15 03:49:24 +01:00
|
|
|
{ .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 },
|
2015-08-08 20:29:31 +02:00
|
|
|
{ .name = "backlog_helper",
|
|
|
|
|
.comment = "Shell command to display a buffer's history",
|
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
2020-10-31 14:23:45 +01:00
|
|
|
.default_ = "\"LESSSECURE=1 less -M -R +Gb\"" },
|
2015-08-08 20:29:31 +02:00
|
|
|
{ .name = "backlog_helper_strip_formatting",
|
|
|
|
|
.comment = "Strip formatting from backlog helper input",
|
|
|
|
|
.type = CONFIG_ITEM_BOOLEAN,
|
2020-10-04 12:04:24 +02:00
|
|
|
.default_ = "off" },
|
2015-08-08 20:29:31 +02:00
|
|
|
|
2015-07-17 21:18:05 +02:00
|
|
|
{ .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 = CONFIG_ITEM_INTEGER,
|
|
|
|
|
.validate = config_validate_nonnegative,
|
|
|
|
|
.default_ = "600" },
|
2015-11-19 14:23:10 +01:00
|
|
|
|
2015-11-22 23:04:51 +01:00
|
|
|
{ .name = "autoaway_message",
|
|
|
|
|
.comment = "Automated away message",
|
|
|
|
|
.type = CONFIG_ITEM_STRING,
|
|
|
|
|
.default_ = "\"I'm not here right now\"" },
|
|
|
|
|
{ .name = "autoaway_delay",
|
|
|
|
|
.comment = "Delay from the last keypress in seconds",
|
|
|
|
|
.type = CONFIG_ITEM_INTEGER,
|
|
|
|
|
.validate = config_validate_nonnegative,
|
|
|
|
|
.default_ = "1800" },
|
|
|
|
|
|
2015-11-19 14:23:10 +01:00
|
|
|
{ .name = "plugin_autoload",
|
|
|
|
|
.comment = "Plugins to automatically load on start",
|
|
|
|
|
.type = CONFIG_ITEM_STRING_ARRAY,
|
|
|
|
|
.validate = config_validate_nonjunk_string },
|
2015-05-02 23:00:34 +02:00
|
|
|
{}
|
|
|
|
|
};
|
|
|
|
|
|
2015-05-08 23:27:21 +02:00
|
|
|
static struct config_schema g_config_attributes[] =
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2020-09-20 13:24:02 +02:00
|
|
|
#define XX(x, y, z) { .name = #y, .comment = #z, .type = CONFIG_ITEM_STRING, \
|
2015-07-05 15:57:53 +02:00
|
|
|
.on_change = on_config_attribute_change },
|
2015-05-02 23:00:34 +02:00
|
|
|
ATTR_TABLE (XX)
|
|
|
|
|
#undef XX
|
|
|
|
|
{}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
static void
|
2015-08-17 00:08:19 +02:00
|
|
|
load_config_behaviour (struct config_item *subtree, void *user_data)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-07-05 01:56:00 +02:00
|
|
|
config_schema_apply_to_object (g_config_behaviour, subtree, user_data);
|
2015-05-02 23:00:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2015-08-17 00:08:19 +02:00
|
|
|
load_config_attributes (struct config_item *subtree, void *user_data)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-07-05 01:56:00 +02:00
|
|
|
config_schema_apply_to_object (g_config_attributes, subtree, user_data);
|
2015-05-02 23:00:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
register_config_modules (struct app_context *ctx)
|
|
|
|
|
{
|
|
|
|
|
struct config *config = &ctx->config;
|
2015-07-05 01:11:20 +02:00
|
|
|
// The servers are loaded later when we can create buffers for them
|
|
|
|
|
config_register_module (config, "servers", NULL, NULL);
|
2015-07-09 22:32:14 +02:00
|
|
|
config_register_module (config, "aliases", NULL, NULL);
|
2015-12-28 02:03:26 +01:00
|
|
|
config_register_module (config, "plugins", NULL, NULL);
|
2015-07-05 01:11:20 +02:00
|
|
|
config_register_module (config, "behaviour", load_config_behaviour, ctx);
|
|
|
|
|
config_register_module (config, "attributes", load_config_attributes, ctx);
|
2015-05-02 23:00:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
static const char *
|
2015-08-17 00:08:19 +02:00
|
|
|
get_config_string (struct config_item *root, const char *key)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *item = config_item_get (root, key, NULL);
|
2015-05-02 23:00:34 +02:00
|
|
|
hard_assert (item);
|
|
|
|
|
if (item->type == CONFIG_ITEM_NULL)
|
|
|
|
|
return NULL;
|
|
|
|
|
hard_assert (config_item_type_is_string (item->type));
|
|
|
|
|
return item->value.string.str;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool
|
2015-05-16 12:33:59 +02:00
|
|
|
set_config_string
|
2015-08-17 00:08:19 +02:00
|
|
|
(struct config_item *root, const char *key, const char *value)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *item = config_item_get (root, key, NULL);
|
2015-05-02 23:00:34 +02:00
|
|
|
hard_assert (item);
|
|
|
|
|
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *new_ = config_item_string_from_cstr (value);
|
2015-05-02 23:00:34 +02:00
|
|
|
struct error *e = NULL;
|
|
|
|
|
if (config_item_set_from (item, new_, &e))
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
config_item_destroy (new_);
|
|
|
|
|
print_error ("couldn't set `%s' in configuration: %s", key, e->message);
|
|
|
|
|
error_free (e);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int64_t
|
2015-08-17 00:08:19 +02:00
|
|
|
get_config_integer (struct config_item *root, const char *key)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *item = config_item_get (root, key, NULL);
|
2015-05-02 23:00:34 +02:00
|
|
|
hard_assert (item && item->type == CONFIG_ITEM_INTEGER);
|
|
|
|
|
return item->value.integer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool
|
2015-08-17 00:08:19 +02:00
|
|
|
get_config_boolean (struct config_item *root, const char *key)
|
2015-05-02 23:00:34 +02:00
|
|
|
{
|
2015-08-17 00:08:19 +02:00
|
|
|
struct config_item *item = config_item_get (root, key, NULL);
|
2015-05-02 23:00:34 +02:00
|
|
|
hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN);
|
|
|
|
|
return item->value.boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-03 19:34:01 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-07-10 01:44:02 +02:00
|
|
|
static struct str_map *
|
|
|
|
|
get_servers_config (struct app_context *ctx)
|
|
|
|
|
{
|
|
|
|
|
return &config_item_get (ctx->config.root, "servers", NULL)->value.object;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static struct str_map *
|
|
|
|
|
get_aliases_config (struct app_context *ctx)
|
|
|
|
|
{
|
|
|
|
|
return &config_item_get (ctx->config.root, "aliases", NULL)->value.object;
|
|
|
|
|
}
|
|
|
|
|
|
2015-12-28 02:03:26 +01:00
|
|
|
static struct str_map *
|
|
|
|
|
get_plugins_config (struct app_context *ctx)
|
|
|
|
|
{
|
|
|
|
|
return &config_item_get (ctx->config.root, "plugins", NULL)->value.object;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-10 01:44:02 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-05-03 19:34:01 +02:00
|
|
|
static void
|
2015-12-10 21:21:21 +01:00
|
|
|
serialize_configuration (struct config_item *root, struct str *output)
|
2015-05-03 19:34:01 +02:00
|
|
|
{
|
|
|
|
|
str_append (output,
|
|
|
|
|
"# " PROGRAM_NAME " " PROGRAM_VERSION " configuration file\n"
|
|
|
|
|
"#\n"
|
|
|
|
|
"# Relative paths are searched for in ${XDG_CONFIG_HOME:-~/.config}\n"
|
|
|
|
|
"# /" PROGRAM_NAME " as well as in $XDG_CONFIG_DIRS/" PROGRAM_NAME "\n"
|
|
|
|
|
"#\n"
|
|
|
|
|
"# Everything is in UTF-8. Any custom comments will be overwritten.\n"
|
|
|
|
|
"\n");
|
|
|
|
|
|
2015-12-10 21:21:21 +01:00
|
|
|
config_item_write (root, true, output);
|
2015-05-03 19:34:01 +02:00
|
|
|
}
|
|
|
|
|
|
2015-05-10 09:55:44 +02:00
|
|
|
// --- Terminal output ---------------------------------------------------------
|
|
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
/// Default colour pair
|
2015-05-10 09:55:44 +02:00
|
|
|
#define COLOR_DEFAULT -1
|
|
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
/// Bright versions of the basic colour set
|
2015-05-10 09:55:44 +02:00
|
|
|
#define COLOR_BRIGHT(x) (COLOR_ ## x + 8)
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
/// Builds a colour pair for 256-colour terminals with a 16-colour backup value
|
2015-05-10 23:06:19 +02:00
|
|
|
#define COLOR_256(name, c256) \
|
2015-11-13 09:05:25 +01:00
|
|
|
(((COLOR_ ## name) & 0xFFFF) | (((c256) & 0xFFFF) << 16))
|
2015-05-10 23:06:19 +02:00
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
typedef int (*terminal_printer_fn) (int);
|
|
|
|
|
|
|
|
|
|
static int
|
|
|
|
|
putchar_stderr (int c)
|
|
|
|
|
{
|
|
|
|
|
return fputc (c, stderr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static terminal_printer_fn
|
|
|
|
|
get_attribute_printer (FILE *stream)
|
|
|
|
|
{
|
|
|
|
|
if (stream == stdout && g_terminal.stdout_is_tty)
|
|
|
|
|
return putchar;
|
|
|
|
|
if (stream == stderr && g_terminal.stderr_is_tty)
|
|
|
|
|
return putchar_stderr;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
vprint_attributed (struct app_context *ctx,
|
2015-04-29 20:56:26 +02:00
|
|
|
FILE *stream, intptr_t attribute, const char *fmt, va_list ap)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
|
|
|
|
terminal_printer_fn printer = get_attribute_printer (stream);
|
|
|
|
|
if (!attribute)
|
|
|
|
|
printer = NULL;
|
|
|
|
|
|
|
|
|
|
if (printer)
|
2015-04-29 21:25:57 +02:00
|
|
|
tputs (ctx->attrs[attribute], 1, printer);
|
2015-04-12 04:52:39 +02:00
|
|
|
|
|
|
|
|
vfprintf (stream, fmt, ap);
|
|
|
|
|
|
|
|
|
|
if (printer)
|
2015-04-29 21:25:57 +02:00
|
|
|
tputs (ctx->attrs[ATTR_RESET], 1, printer);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
print_attributed (struct app_context *ctx,
|
2015-04-29 20:56:26 +02:00
|
|
|
FILE *stream, intptr_t attribute, const char *fmt, ...)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
|
|
|
|
va_list ap;
|
|
|
|
|
va_start (ap, fmt);
|
|
|
|
|
vprint_attributed (ctx, stream, attribute, fmt, ap);
|
|
|
|
|
va_end (ap);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
log_message_attributed (void *user_data, const char *quote, const char *fmt,
|
|
|
|
|
va_list ap)
|
|
|
|
|
{
|
|
|
|
|
FILE *stream = stderr;
|
2015-05-07 18:39:43 +02:00
|
|
|
struct app_context *ctx = g_ctx;
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, hide);
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2015-05-07 18:39:43 +02:00
|
|
|
print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote);
|
|
|
|
|
vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap);
|
2015-04-12 04:52:39 +02:00
|
|
|
fputs ("\n", stream);
|
|
|
|
|
|
2016-03-07 01:08:52 +01:00
|
|
|
CALL (ctx->input, show);
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
2015-12-24 16:02:26 +01:00
|
|
|
static ssize_t
|
|
|
|
|
attr_by_name (const char *name)
|
2015-04-29 21:25:57 +02:00
|
|
|
{
|
|
|
|
|
static const char *table[ATTR_COUNT] =
|
|
|
|
|
{
|
2020-09-20 13:24:02 +02:00
|
|
|
#define XX(x, y, z) [ATTR_ ## x] = #y,
|
2015-04-29 21:25:57 +02:00
|
|
|
ATTR_TABLE (XX)
|
|
|
|
|
#undef XX
|
|
|
|
|
};
|
|
|
|
|
|
2015-07-05 16:15:30 +02:00
|
|
|
for (size_t i = 0; i < N_ELEMENTS (table); i++)
|
2015-12-24 16:02:26 +01:00
|
|
|
if (!strcmp (name, table[i]))
|
|
|
|
|
return i;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
on_config_attribute_change (struct config_item *item)
|
|
|
|
|
{
|
|
|
|
|
struct app_context *ctx = item->user_data;
|
|
|
|
|
ssize_t id = attr_by_name (item->schema->name);
|
|
|
|
|
if (id != -1)
|
|
|
|
|
{
|
2018-01-08 22:15:29 +01:00
|
|
|
cstr_set (&ctx->attrs[id], xstrdup (item->type == CONFIG_ITEM_NULL
|
2015-12-24 16:02:26 +01:00
|
|
|
? ctx->attrs_defaults[id]
|
2018-01-08 22:15:29 +01:00
|
|
|
: item->value.string.str));
|
2015-12-24 16:02:26 +01:00
|
|
|
}
|
2015-04-29 21:25:57 +02:00
|
|
|
}
|
|
|
|
|
|
2015-04-12 04:52:39 +02:00
|
|
|
static void
|
|
|
|
|
init_colors (struct app_context *ctx)
|
|
|
|
|
{
|
2015-04-21 20:52:41 +02:00
|
|
|
bool have_ti = init_terminal ();
|
2015-07-05 15:57:53 +02:00
|
|
|
char **defaults = ctx->attrs_defaults;
|
2015-04-21 20:52:41 +02:00
|
|
|
|
2015-07-05 15:57:53 +02:00
|
|
|
#define INIT_ATTR(id, ti) defaults[ATTR_ ## id] = xstrdup (have_ti ? (ti) : "")
|
2015-04-21 20:52:41 +02:00
|
|
|
|
2015-07-11 05:39:00 +02:00
|
|
|
INIT_ATTR (PROMPT, enter_bold_mode);
|
|
|
|
|
INIT_ATTR (RESET, exit_attribute_mode);
|
2016-04-10 22:13:53 +02:00
|
|
|
INIT_ATTR (DATE_CHANGE, enter_bold_mode);
|
2015-07-11 05:39:00 +02:00
|
|
|
INIT_ATTR (READ_MARKER, g_terminal.color_set_fg[COLOR_MAGENTA]);
|
|
|
|
|
INIT_ATTR (WARNING, g_terminal.color_set_fg[COLOR_YELLOW]);
|
|
|
|
|
INIT_ATTR (ERROR, g_terminal.color_set_fg[COLOR_RED]);
|
2015-04-26 18:23:43 +02:00
|
|
|
|
2015-07-11 05:39:00 +02:00
|
|
|
INIT_ATTR (EXTERNAL, g_terminal.color_set_fg[COLOR_WHITE]);
|
|
|
|
|
INIT_ATTR (TIMESTAMP, g_terminal.color_set_fg[COLOR_WHITE]);
|
|
|
|
|
INIT_ATTR (ACTION, g_terminal.color_set_fg[COLOR_RED]);
|
|
|
|
|
INIT_ATTR (USERHOST, g_terminal.color_set_fg[COLOR_CYAN]);
|
|
|
|
|
INIT_ATTR (JOIN, g_terminal.color_set_fg[COLOR_GREEN]);
|
|
|
|
|
INIT_ATTR (PART, g_terminal.color_set_fg[COLOR_RED]);
|
2015-04-21 20:52:41 +02:00
|
|
|
|
2016-03-13 05:35:23 +01:00
|
|
|
char *highlight = have_ti ? xstrdup_printf ("%s%s%s",
|
2015-05-02 20:23:02 +02:00
|
|
|
g_terminal.color_set_fg[COLOR_YELLOW],
|
|
|
|
|
g_terminal.color_set_bg[COLOR_MAGENTA],
|
2016-03-13 05:35:23 +01:00
|
|
|
enter_bold_mode) : NULL;
|
2015-04-29 21:25:57 +02:00
|
|
|
INIT_ATTR (HIGHLIGHT, highlight);
|
2015-04-27 22:40:33 +02:00
|
|
|
free (highlight);
|
|
|
|
|
|
2015-04-21 20:52:41 +02:00
|
|
|
#undef INIT_ATTR
|
2015-04-12 04:52:39 +02:00
|
|
|
|
2016-03-13 05:35:23 +01:00
|
|
|
// This prevents formatters from obtaining an attribute printer function
|
|
|
|
|
if (!have_ti)
|
2015-04-12 04:52:39 +02:00
|
|
|
{
|
2015-04-29 20:56:26 +02:00
|
|
|
g_terminal.stdout_is_tty = false;
|
|
|
|
|
g_terminal.stderr_is_tty = false;
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_log_message_real = log_message_attributed;
|
2015-07-05 15:57:53 +02:00
|
|
|
|
|
|
|
|
// Apply the default values so that we start with any formatting at all
|
|
|
|
|
config_schema_call_changed
|
|
|
|
|
(config_item_get (ctx->config.root, "attributes", NULL));
|
2015-04-12 04:52:39 +02:00
|
|
|
}
|
|
|
|
|
|
2015-05-10 23:06:19 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
// A little tool that tries to make the most of the terminal's capabilities
|
|
|
|
|
// to set up text attributes. It mostly targets just terminal emulators as that
|
|
|
|
|
// is what people are using these days. At least no stupid ncurses limits us
|
2020-10-11 17:36:21 +02:00
|
|
|
// with colour pairs.
|
2015-05-10 23:06:19 +02:00
|
|
|
|
|
|
|
|
enum
|
|
|
|
|
{
|
2020-10-11 17:58:35 +02:00
|
|
|
TEXT_BOLD = 1 << 0,
|
|
|
|
|
TEXT_ITALIC = 1 << 1,
|
|
|
|
|
TEXT_UNDERLINE = 1 << 2,
|
|
|
|
|
TEXT_INVERSE = 1 << 3,
|
|
|
|
|
TEXT_BLINK = 1 << 4,
|
|
|
|
|
TEXT_CROSSED_OUT = 1 << 5
|
2015-05-10 23:06:19 +02:00
|
|
|
};
|
|
|
|
|
|
2016-10-29 18:02:03 +02:00
|
|
|
struct attr_printer
|
2015-05-11 19:09:42 +02:00
|
|
|
{
|
2016-10-29 05:33:22 +02:00
|
|
|
char **attrs; ///< Named attributes
|
2015-08-08 20:29:31 +02:00
|
|
|
FILE *stream; ///< Output stream
|
2015-05-11 19:09:42 +02:00
|
|
|
bool dirty; ///< Attributes are set
|
|
|
|
|
};
|
|
|
|
|
|
2016-10-29 18:02:03 +02:00
|
|
|
#define ATTR_PRINTER_INIT(ctx, stream) { ctx->attrs, stream, true }
|
2016-10-29 05:33:22 +02:00
|
|
|
|
2020-10-04 12:04:24 +02:00
|
|
|
static void
|
|
|
|
|
attr_printer_filtered_puts (FILE *stream, const char *attr)
|
|
|
|
|
{
|
|
|
|
|
for (; *attr; attr++)
|
|
|
|
|
{
|
|
|
|
|
// sgr/set_attributes and sgr0/exit_attribute_mode like to enable or
|
|
|
|
|
// disable the ACS with SO/SI (e.g. for TERM=screen), however `less -R`
|
|
|
|
|
// does not skip over these characters and it screws up word wrapping
|
|
|
|
|
if (*attr == 14 /* SO */ || *attr == 15 /* SI */)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
// Trivially skip delay sequences intended to be processed by tputs()
|
|
|
|
|
const char *end = NULL;
|
|
|
|
|
if (attr[0] == '$' && attr[1] == '<' && (end = strchr (attr, '>')))
|
|
|
|
|
attr = end;
|
|
|
|
|
else
|
|
|
|
|
fputc (*attr, stream);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-08-08 20:29:31 +02:00
|
|
|
static void
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (struct attr_printer *self, const char *attr)
|
2015-08-08 20:29:31 +02:00
|
|
|
{
|
|
|
|
|
terminal_printer_fn printer = get_attribute_printer (self->stream);
|
|
|
|
|
if (printer)
|
|
|
|
|
tputs (attr, 1, printer);
|
|
|
|
|
else
|
2020-10-04 12:04:24 +02:00
|
|
|
// We shouldn't really do this but we need it to output formatting
|
|
|
|
|
// to the backlog helper--it should be SGR-only
|
|
|
|
|
attr_printer_filtered_puts (self->stream, attr);
|
2015-08-08 20:29:31 +02:00
|
|
|
}
|
|
|
|
|
|
2015-05-11 19:09:42 +02:00
|
|
|
static void
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_reset (struct attr_printer *self)
|
2015-05-11 19:09:42 +02:00
|
|
|
{
|
|
|
|
|
if (self->dirty)
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (self, self->attrs[ATTR_RESET]);
|
2015-05-11 19:09:42 +02:00
|
|
|
|
|
|
|
|
self->dirty = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_apply_named (struct attr_printer *self, int attribute)
|
2015-05-11 19:09:42 +02:00
|
|
|
{
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_reset (self);
|
2015-05-11 19:09:42 +02:00
|
|
|
if (attribute != ATTR_RESET)
|
|
|
|
|
{
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (self, self->attrs[attribute]);
|
2015-05-11 19:09:42 +02:00
|
|
|
self->dirty = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-12 02:43:53 +02:00
|
|
|
// NOTE: commonly terminals have:
|
2020-10-11 17:36:21 +02:00
|
|
|
// 8 colours (worst, bright fg often with BOLD, bg sometimes with BLINK)
|
|
|
|
|
// 16 colours (okayish, we have the full basic range guaranteed)
|
|
|
|
|
// 88 colours (the same plus a 4^3 RGB cube and a few shades of grey)
|
|
|
|
|
// 256 colours (best, like above but with a larger cube and more grey)
|
2015-05-12 02:43:53 +02:00
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
/// Interpolate from the 256-colour palette to the 88-colour one
|
2015-05-12 02:43:53 +02:00
|
|
|
static int
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_256_to_88 (int color)
|
2015-05-12 02:43:53 +02:00
|
|
|
{
|
|
|
|
|
// These colours are the same everywhere
|
|
|
|
|
if (color < 16)
|
|
|
|
|
return color;
|
|
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
// 24 -> 8 extra shades of grey
|
2015-05-12 02:43:53 +02:00
|
|
|
if (color >= 232)
|
|
|
|
|
return 80 + (color - 232) / 3;
|
|
|
|
|
|
|
|
|
|
// 6 * 6 * 6 cube -> 4 * 4 * 4 cube
|
|
|
|
|
int x[6] = { 0, 1, 1, 2, 2, 3 };
|
|
|
|
|
int index = color - 16;
|
|
|
|
|
return 16 +
|
|
|
|
|
( x[ index / 36 ] << 8
|
|
|
|
|
| x[(index / 6) % 6 ] << 4
|
|
|
|
|
| x[(index % 6) ] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_decode_color (int color, bool *is_bright)
|
2015-05-12 02:43:53 +02:00
|
|
|
{
|
|
|
|
|
int16_t c16 = color; hard_assert (c16 < 16);
|
|
|
|
|
int16_t c256 = color >> 16; hard_assert (c256 < 256);
|
|
|
|
|
|
|
|
|
|
*is_bright = false;
|
|
|
|
|
switch (max_colors)
|
|
|
|
|
{
|
|
|
|
|
case 8:
|
|
|
|
|
if (c16 >= 8)
|
|
|
|
|
{
|
|
|
|
|
c16 -= 8;
|
|
|
|
|
*is_bright = true;
|
|
|
|
|
}
|
2018-01-07 05:45:34 +01:00
|
|
|
// Fall-through
|
2015-05-12 02:43:53 +02:00
|
|
|
case 16:
|
|
|
|
|
return c16;
|
|
|
|
|
|
|
|
|
|
case 88:
|
2016-10-29 18:02:03 +02:00
|
|
|
return c256 <= 0 ? c16 : attr_printer_256_to_88 (c256);
|
2015-05-12 02:43:53 +02:00
|
|
|
case 256:
|
|
|
|
|
return c256 <= 0 ? c16 : c256;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// Unsupported palette
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-05-11 19:09:42 +02:00
|
|
|
|
|
|
|
|
static void
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_apply (struct attr_printer *self,
|
|
|
|
|
int text_attrs, int wanted_fg, int wanted_bg)
|
2015-05-11 19:09:42 +02:00
|
|
|
{
|
2015-05-12 02:43:53 +02:00
|
|
|
bool fg_is_bright;
|
2016-10-29 18:02:03 +02:00
|
|
|
int fg = attr_printer_decode_color (wanted_fg, &fg_is_bright);
|
2015-05-12 02:43:53 +02:00
|
|
|
bool bg_is_bright;
|
2016-10-29 18:02:03 +02:00
|
|
|
int bg = attr_printer_decode_color (wanted_bg, &bg_is_bright);
|
2015-05-11 19:09:42 +02:00
|
|
|
|
2016-10-29 18:02:03 +02:00
|
|
|
bool have_inverse = !!(text_attrs & TEXT_INVERSE);
|
2015-08-10 22:42:53 +02:00
|
|
|
if (have_inverse)
|
2015-05-11 19:09:42 +02:00
|
|
|
{
|
2015-05-12 02:43:53 +02:00
|
|
|
bool tmp = fg_is_bright;
|
|
|
|
|
fg_is_bright = bg_is_bright;
|
|
|
|
|
bg_is_bright = tmp;
|
2015-05-11 19:09:42 +02:00
|
|
|
}
|
|
|
|
|
|
2015-08-10 22:42:53 +02:00
|
|
|
// In 8 colour mode, some terminals don't support bright backgrounds.
|
|
|
|
|
// However, we can make use of the fact that the brightness change caused
|
|
|
|
|
// by the bold attribute is retained when inverting the colours.
|
|
|
|
|
// This has the downside of making the text bold when it's not supposed
|
|
|
|
|
// to be, and we still can't make both colours bright, so it's more of
|
|
|
|
|
// an interesting hack rather than anything else.
|
|
|
|
|
if (!fg_is_bright && bg_is_bright && have_inverse)
|
2016-10-29 18:02:03 +02:00
|
|
|
text_attrs |= TEXT_BOLD;
|
2015-08-10 22:42:53 +02:00
|
|
|
else if (!fg_is_bright && bg_is_bright
|
|
|
|
|
&& !have_inverse && fg >= 0 && bg >= 0)
|
|
|
|
|
{
|
|
|
|
|
// As long as none of the colours is the default, we can swap them
|
|
|
|
|
int tmp = fg; fg = bg; bg = tmp;
|
2016-10-29 18:02:03 +02:00
|
|
|
text_attrs |= TEXT_BOLD | TEXT_INVERSE;
|
2015-08-10 22:42:53 +02:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2020-09-16 07:42:05 +02:00
|
|
|
// This often works, however...
|
2016-10-29 18:02:03 +02:00
|
|
|
if (fg_is_bright) text_attrs |= TEXT_BOLD;
|
2020-09-16 07:42:05 +02:00
|
|
|
// this turns out to be annoying if implemented "correctly"
|
2016-10-29 18:02:03 +02:00
|
|
|
if (bg_is_bright) text_attrs |= TEXT_BLINK;
|
2015-08-10 22:42:53 +02:00
|
|
|
}
|
2015-05-11 19:09:42 +02:00
|
|
|
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_reset (self);
|
2015-05-11 19:09:42 +02:00
|
|
|
|
2016-10-29 18:02:03 +02:00
|
|
|
if (text_attrs)
|
|
|
|
|
attr_printer_tputs (self, tparm (set_attributes,
|
2015-08-08 20:29:31 +02:00
|
|
|
0, // standout
|
2016-10-29 18:02:03 +02:00
|
|
|
text_attrs & TEXT_UNDERLINE,
|
|
|
|
|
text_attrs & TEXT_INVERSE,
|
|
|
|
|
text_attrs & TEXT_BLINK,
|
2015-08-08 20:29:31 +02:00
|
|
|
0, // dim
|
2016-10-29 18:02:03 +02:00
|
|
|
text_attrs & TEXT_BOLD,
|
2015-08-08 20:29:31 +02:00
|
|
|
0, // blank
|
|
|
|
|
0, // protect
|
|
|
|
|
0)); // acs
|
2020-10-11 17:58:35 +02:00
|
|
|
if ((text_attrs & TEXT_ITALIC) && enter_italics_mode)
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (self, enter_italics_mode);
|
2015-05-11 19:09:42 +02:00
|
|
|
|
2020-10-11 17:58:35 +02:00
|
|
|
char *smxx = NULL;
|
|
|
|
|
if ((text_attrs & TEXT_CROSSED_OUT)
|
|
|
|
|
&& (smxx = tigetstr ("smxx")) && smxx != (char *) -1)
|
|
|
|
|
attr_printer_tputs (self, smxx);
|
|
|
|
|
|
2015-05-12 02:43:53 +02:00
|
|
|
if (fg >= 0)
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (self, g_terminal.color_set_fg[fg]);
|
2015-05-12 02:43:53 +02:00
|
|
|
if (bg >= 0)
|
2016-10-29 18:02:03 +02:00
|
|
|
attr_printer_tputs (self, g_terminal.color_set_bg[bg]);
|
2015-05-12 02:43:53 +02:00
|
|
|
|
2015-05-11 19:09:42 +02:00
|
|
|
self->dirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-06 01:54:02 +02:00
|
|
|
// --- Helpers -----------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
static int
|
|
|
|
|
irc_server_strcmp (struct server *s, const char *a, const char *b)
|
|
|
|
|
{
|
|
|
|
|
int x;
|
|
|
|
|
while (*a || *b)
|
|
|
|
|
if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++)))
|
|
|
|
|
return x;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int
|
|
|
|
|
irc_server_strncmp (struct server *s, const char *a, const char *b, size_t n)
|
|
|
|
|
{
|
|
|
|
|
int x;
|
|
|
|
|
while (n-- && (*a || *b))
|
|
|
|
|
if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++)))
|
|
|
|
|
return x;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static char *
|
|
|
|
|
irc_cut_nickname (const char *prefix)
|
|
|
|
|
{
|
2015-07-11 17:54:38 +02:00
|
|
|
return cstr_cut_until (prefix, "!@");
|
2015-07-06 01:54:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const char *
|
|
|
|
|
irc_find_userhost (const char *prefix)
|
|
|
|
|
{
|
|
|
|
|
const char *p = strchr (prefix, '!');
|
|
|
|
|
return p ? p + 1 : NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool
|
|
|
|
|
irc_is_this_us (struct server *s, const char *prefix)
|
|
|
|
|
{
|
|
|
|
|
// This shouldn't be called before successfully registering.
|
|
|
|
|
// Better safe than sorry, though.
|
|
|
|
|
if (!s->irc_user)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
char *nick = irc_cut_nickname (prefix);
|
|
|
|
|
bool result = !irc_server_strcmp (s, nick, s->irc_user->nickname);
|
|
|
|
|
free (nick);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool
|
|
|
|
|
irc_is_channel (struct server *s, const char *ident)
|
|
|
|
|
{
|
|
|
|
|
return *ident
|
|
|
|
|
&& (!!strchr (s->irc_chantypes, *ident) ||
|
|
|
|
|
!!strchr (s->irc_idchan_prefixes, *ident));
|
|
|
|
|
}
|
|
|
|
|
|
2015-09-24 01:39:49 +02:00
|
|
|
// Message targets can be prefixed by a character filtering their targets
|
|
|
|
|
static const char *
|
|
|
|
|
irc_skip_statusmsg (struct server *s, const char *target)
|
|
|
|
|
{
|
|
|
|
|
return target + (*target && strchr (s->irc_statusmsg, *target));
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-06 01:54:02 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2020-10-12 23:32:58 +02:00
|
|
|
// As of 2020, everything should be in UTF-8. And if it's not, we'll decode it
|
2015-07-06 01:54:02 +02:00
|
|
|
// as ISO Latin 1. This function should not be called on the whole message.
|
|
|
|
|
static char *
|
2016-04-03 04:05:04 +02:00
|
|
|
irc_to_utf8 (const char *text)
|
2015-07-06 01:54:02 +02:00
|
|
|
{
|
|
|
|
|
if (!text)
|
|
|
|
|
return NULL;
|
2020-10-21 05:39:05 +02:00
|
|
|
|
|
|
|
|
// XXX: the validation may be unnecessarily harsh, could do with a lenient
|
|
|
|
|
// first pass, then replace any errors with the replacement character
|
2015-07-06 01:54:02 +02:00
|
|
|
size_t len = strlen (text) + 1;
|
|
|
|
|
if (utf8_validate (text, len))
|
|
|
|
|
return xstrdup (text);
|
2016-04-03 04:05:04 +02:00
|
|
|
|
|
|
|
|
// Windows 1252 redefines several silly C1 control characters as glyphs
|
2016-10-28 18:16:21 +02:00
|
|
|
static const char c1[32][4] =
|
2016-04-03 04:05:04 +02:00
|
|
|
{
|
|
|
|
|
"\xe2\x82\xac", "\xc2\x81", "\xe2\x80\x9a", "\xc6\x92",
|
|
|
|
|
"\xe2\x80\x9e", "\xe2\x80\xa6", "\xe2\x80\xa0", "\xe2\x80\xa1",
|
|
|
|
|
"\xcb\x86", "\xe2\x80\xb0", "\xc5\xa0", "\xe2\x80\xb9",
|
|
|
|
|
"\xc5\x92", "\xc2\x8d", "\xc5\xbd", "\xc2\x8f",
|
|
|
|
|
"\xc2\x90", "\xe2\x80\x98", "\xe2\x80\x99", "\xe2\x80\x9c",
|
|
|
|
|
"\xe2\x80\x9d", "\xe2\x80\xa2", "\xe2\x80\x93", "\xe2\x80\x94",
|
|
|
|
|
"\xcb\x9c", "\xe2\x84\xa2", "\xc5\xa1", "\xe2\x80\xba",
|
|
|
|
|
"\xc5\x93", "\xc2\x9d", "\xc5\xbe", "\xc5\xb8",
|
|
|
|
|
};
|
|
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str s = str_make ();
|
2016-04-03 04:05:04 +02:00
|
|
|
for (const char *p = text; *p; p++)
|
|
|
|
|
{
|
|
|
|
|
int c = *(unsigned char *) p;
|
|
|
|
|
if (c < 0x80)
|
|
|
|
|
str_append_c (&s, c);
|
|
|
|
|
else if (c < 0xA0)
|
|
|
|
|
str_append (&s, c1[c & 0x1f]);
|
|
|
|
|
else
|
|
|
|
|
str_append_data (&s,
|
|
|
|
|
(char[]) {0xc0 | (c >> 6), 0x80 | (c & 0x3f)}, 2);
|
|
|
|
|
}
|
|
|
|
|
return str_steal (&s);
|
2015-07-06 01:54:02 +02:00
|
|
|
}
|
|
|
|
|
|
2015-04-26 18:23:43 +02:00
|
|
|
// --- Output formatter --------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// This complicated piece of code makes attributed text formatting simple.
|
|
|
|
|
// We use a printf-inspired syntax to push attributes and text to the object,
|
|
|
|
|
// then flush it either to a terminal, or a log file with formatting stripped.
|
|
|
|
|
//
|
|
|
|
|
// Format strings use a #-quoted notation, to differentiate from printf:
|
2015-06-28 02:49:28 +02:00
|
|
|
// #s inserts a string (expected to be in UTF-8)
|
|
|
|
|
// #d inserts a signed integer
|
2015-12-25 21:22:59 +01:00
|
|
|
// #l inserts a locale-encoded string
|
2015-06-28 02:49:28 +02:00
|
|
|
//
|
|
|
|
|
// #S inserts a string from the server with unknown encoding
|
2015-05-10 05:28:12 +02:00
|
|
|
// #m inserts a mIRC-formatted string (auto-resets at boundaries)
|
2015-06-28 02:49:28 +02:00
|
|
|
// #n cuts the nickname from a string and automatically colours it
|
2015-06-29 08:54:00 +02:00
|
|
|
// #N is like #n but also appends userhost, if present
|
2015-04-26 18:23:43 +02:00
|
|
|
//
|
|
|
|
|
// #a inserts named attributes (auto-resets)
|
|
|
|
|
// #r resets terminal attributes
|
2020-10-11 17:36:21 +02:00
|
|
|
// #c sets foreground colour
|
|
|
|
|
// #C sets background colour
|
2015-06-28 02:49:28 +02:00
|
|
|
//
|
|
|
|
|
// Modifiers:
|
|
|
|
|
// & free() the string argument after using it
|
2015-04-26 18:23:43 +02:00
|
|
|
|
|
|
|
|
static void
|
2015-05-10 23:06:19 +02:00
|
|
|
formatter_add_item (struct formatter *self, struct formatter_item template_)
|
2015-04-26 18:23:43 +02:00
|
|
|
{
|
2015-05-10 23:06:19 +02:00
|
|
|
if (template_.text)
|
|
|
|
|
template_.text = xstrdup (template_.text);
|
2015-04-26 18:39:38 +02:00
|
|
|
|
2016-01-31 20:07:20 +01:00
|
|
|
if (self->items_len == self->items_alloc)
|
|
|
|
|
self->items = xreallocarray
|
|
|
|
|
(self->items, sizeof *self->items, (self->items_alloc <<= 1));
|
|
|
|
|
self->items[self->items_len++] = template_;
|
2015-04-26 18:23:43 +02:00
|
|
|
}
|
|
|
|
|
|
2015-05-10 23:06:19 +02:00
|
|
|
#define FORMATTER_ADD_ITEM(self, type_, ...) formatter_add_item ((self), \
|
|
|
|
|
(struct formatter_item) { .type = FORMATTER_ITEM_ ## type_, __VA_ARGS__ })
|
2015-04-26 18:39:38 +02:00
|
|
|
|
2015-05-10 23:06:19 +02:00
|
|
|
#define FORMATTER_ADD_RESET(self) \
|
|
|
|
|
FORMATTER_ADD_ITEM ((self), ATTR, .attribute = ATTR_RESET)
|
|
|
|
|
#define FORMATTER_ADD_TEXT(self, text_) \
|
|
|
|
|
FORMATTER_ADD_ITEM ((self), TEXT, .text = (text_))
|
|
|
|
|
#define FORMATTER_ADD_SIMPLE(self, attribute_) \
|
2016-10-29 18:02:03 +02:00
|
|
|
FORMATTER_ADD_ITEM ((self), SIMPLE, .attribute = TEXT_ ## attribute_)
|
2015-04-26 18:23:43 +02:00
|
|
|
|
2015-05-10 05:28:12 +02:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
enum
|
|
|
|
|
{
|
|
|
|
|
MIRC_WHITE, MIRC_BLACK, MIRC_BLUE, MIRC_GREEN,
|
|
|
|
|
MIRC_L_RED, MIRC_RED, MIRC_PURPLE, MIRC_ORANGE,
|
|
|
|
|
MIRC_YELLOW, MIRC_L_GREEN, MIRC_CYAN, MIRC_L_CYAN,
|
|
|
|
|
MIRC_L_BLUE, MIRC_L_PURPLE, MIRC_GRAY, MIRC_L_GRAY,
|
|
|
|
|
};
|
|
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
// We use estimates from the 16 colour terminal palette, or the 256 colour cube,
|
2015-05-10 05:28:12 +02:00
|
|
|
// which is not always available. The mIRC orange colour is only in the cube.
|
|
|
|
|
|
2015-05-10 23:06:19 +02:00
|
|
|
static const int g_mirc_to_terminal[] =
|
|
|
|
|
{
|
|
|
|
|
[MIRC_WHITE] = COLOR_256 (BRIGHT (WHITE), 231),
|
|
|
|
|
[MIRC_BLACK] = COLOR_256 (BLACK, 16),
|
|
|
|
|
[MIRC_BLUE] = COLOR_256 (BLUE, 19),
|
|
|
|
|
[MIRC_GREEN] = COLOR_256 (GREEN, 34),
|
|
|
|
|
[MIRC_L_RED] = COLOR_256 (BRIGHT (RED), 196),
|
|
|
|
|
[MIRC_RED] = COLOR_256 (RED, 124),
|
|
|
|
|
[MIRC_PURPLE] = COLOR_256 (MAGENTA, 127),
|
|
|
|
|
[MIRC_ORANGE] = COLOR_256 (BRIGHT (YELLOW), 214),
|
|
|
|
|
[MIRC_YELLOW] = COLOR_256 (BRIGHT (YELLOW), 226),
|
|
|
|
|
[MIRC_L_GREEN] = COLOR_256 (BRIGHT (GREEN), 46),
|
|
|
|
|
[MIRC_CYAN] = COLOR_256 (CYAN, 37),
|
|
|
|
|
[MIRC_L_CYAN] = COLOR_256 (BRIGHT (CYAN), 51),
|
|
|
|
|
[MIRC_L_BLUE] = COLOR_256 (BRIGHT (BLUE), 21),
|
|
|
|
|
[MIRC_L_PURPLE] = COLOR_256 (BRIGHT (MAGENTA),201),
|
|
|
|
|
[MIRC_GRAY] = COLOR_256 (BRIGHT (BLACK), 244),
|
|
|
|
|
[MIRC_L_GRAY] = COLOR_256 (WHITE, 252),
|
2015-05-10 05:28:12 +02:00
|
|
|
};
|
|
|
|
|
|
2020-10-11 17:41:57 +02:00
|
|
|
// https://modern.ircdocs.horse/formatting.html
|
|
|
|
|
// http://anti.teamidiot.de/static/nei/*/extended_mirc_color_proposal.html
|
|
|
|
|
static const char g_extra_to_256[100 - 16] =
|
|
|
|
|
{
|
|
|
|
|
52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89,
|
|
|
|
|
88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125,
|
|
|
|
|
124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161,
|
|
|
|
|
196, 208, 226, 154, 46, 86 , 51, 75, 21, 171, 201, 198,
|
|
|
|
|
203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205,
|
|
|
|
|
217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212,
|
|
|
|
|
16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, -1
|
|
|
|
|
};
|
|
|
|
|
|
2015-05-10 09:55:44 +02:00
|
|
|
static const char *
|
|
|
|
|
formatter_parse_mirc_color (struct formatter *self, const char *s)
|
2015-05-10 05:28:12 +02:00
|
|
|
{
|
2015-05-10 09:55:44 +02:00
|
|
|
if (!isdigit_ascii (*s))
|
2016-03-08 01:59:51 +01:00
|
|
|
{
|
|
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1);
|
|
|
|
|
FORMATTER_ADD_ITEM (self, BG_COLOR, .color = -1);
|
2015-05-10 09:55:44 +02:00
|
|
|
return s;
|
2016-03-08 01:59:51 +01:00
|
|
|
}
|
2015-05-10 05:28:12 +02:00
|
|
|
|
2015-05-10 09:55:44 +02:00
|
|
|
int fg = *s++ - '0';
|
|
|
|
|
if (isdigit_ascii (*s))
|
|
|
|
|
fg = fg * 10 + (*s++ - '0');
|
2020-10-11 17:41:57 +02:00
|
|
|
if (fg < 16)
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]);
|
2020-10-11 17:41:57 +02:00
|
|
|
else
|
|
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR,
|
|
|
|
|
.color = COLOR_256 (DEFAULT, g_extra_to_256[fg]));
|
2015-05-10 09:55:44 +02:00
|
|
|
|
|
|
|
|
if (*s != ',' || !isdigit_ascii (s[1]))
|
|
|
|
|
return s;
|
2015-05-12 02:48:12 +02:00
|
|
|
s++;
|
2015-05-10 09:55:44 +02:00
|
|
|
|
2015-05-12 02:48:12 +02:00
|
|
|
int bg = *s++ - '0';
|
|
|
|
|
if (isdigit_ascii (*s))
|
|
|
|
|
bg = bg * 10 + (*s++ - '0');
|
2020-10-11 17:41:57 +02:00
|
|
|
if (bg < 16)
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]);
|
2020-10-11 17:41:57 +02:00
|
|
|
else
|
|
|
|
|
FORMATTER_ADD_ITEM (self, BG_COLOR,
|
|
|
|
|
.color = COLOR_256 (DEFAULT, g_extra_to_256[bg]));
|
2015-05-10 09:55:44 +02:00
|
|
|
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
formatter_parse_mirc (struct formatter *self, const char *s)
|
|
|
|
|
{
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_RESET (self);
|
2015-05-10 05:28:12 +02:00
|
|
|
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str buf = str_make ();
|
2015-05-10 23:11:43 +02:00
|
|
|
unsigned char c;
|
2015-05-10 05:28:12 +02:00
|
|
|
while ((c = *s++))
|
|
|
|
|
{
|
2015-05-10 09:55:44 +02:00
|
|
|
if (buf.len && c < 0x20)
|
|
|
|
|
{
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
2015-05-10 09:55:44 +02:00
|
|
|
str_reset (&buf);
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-10 05:28:12 +02:00
|
|
|
switch (c)
|
|
|
|
|
{
|
2020-10-11 17:58:35 +02:00
|
|
|
case '\x02': FORMATTER_ADD_SIMPLE (self, BOLD); break;
|
|
|
|
|
case '\x11': /* monospace, N/A */ break;
|
|
|
|
|
case '\x1d': FORMATTER_ADD_SIMPLE (self, ITALIC); break;
|
|
|
|
|
case '\x1e': FORMATTER_ADD_SIMPLE (self, CROSSED_OUT); break;
|
|
|
|
|
case '\x1f': FORMATTER_ADD_SIMPLE (self, UNDERLINE); break;
|
|
|
|
|
case '\x16': FORMATTER_ADD_SIMPLE (self, INVERSE); break;
|
2015-05-10 09:55:44 +02:00
|
|
|
|
2015-05-10 05:28:12 +02:00
|
|
|
case '\x03':
|
2015-05-10 09:55:44 +02:00
|
|
|
s = formatter_parse_mirc_color (self, s);
|
2015-05-10 05:28:12 +02:00
|
|
|
break;
|
|
|
|
|
case '\x0f':
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_RESET (self);
|
2015-05-10 05:28:12 +02:00
|
|
|
break;
|
|
|
|
|
default:
|
2015-05-10 09:55:44 +02:00
|
|
|
str_append_c (&buf, c);
|
2015-05-10 05:28:12 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-10 09:55:44 +02:00
|
|
|
if (buf.len)
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
2015-05-10 05:28:12 +02:00
|
|
|
|
2015-05-10 09:55:44 +02:00
|
|
|
str_free (&buf);
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_RESET (self);
|
2015-05-10 05:28:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
static void
|
2016-10-23 13:38:46 +02:00
|
|
|
formatter_parse_nick (struct formatter *self, const char *s)
|
2015-06-28 02:49:28 +02:00
|
|
|
{
|
2015-09-24 02:03:10 +02:00
|
|
|
// For outgoing messages; maybe we should add a special #t for them
|
|
|
|
|
// which would also make us not cut off the userhost part, ever
|
|
|
|
|
if (irc_is_channel (self->s, irc_skip_statusmsg (self->s, s)))
|
|
|
|
|
{
|
2016-04-03 04:05:04 +02:00
|
|
|
char *tmp = irc_to_utf8 (s);
|
2015-09-24 02:03:10 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, tmp);
|
|
|
|
|
free (tmp);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
char *nick = irc_cut_nickname (s);
|
2015-07-27 01:18:32 +02:00
|
|
|
int color = siphash_wrapper (nick, strlen (nick)) % 7;
|
2015-06-28 02:49:28 +02:00
|
|
|
|
2015-07-27 01:18:32 +02:00
|
|
|
// Never use the black colour, could become transparent on black terminals;
|
|
|
|
|
// white is similarly excluded from the range
|
|
|
|
|
if (color == COLOR_BLACK)
|
2015-08-11 21:22:44 +02:00
|
|
|
color = (uint16_t) -1;
|
2015-06-28 02:49:28 +02:00
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
// Use a colour from the 256-colour cube if available
|
2015-08-10 07:51:03 +02:00
|
|
|
color |= self->ctx->nick_palette[siphash_wrapper (nick,
|
|
|
|
|
strlen (nick)) % self->ctx->nick_palette_len] << 16;
|
2015-07-27 01:18:32 +02:00
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
// We always use the default colour for ourselves
|
2015-07-27 01:18:32 +02:00
|
|
|
if (self->s && irc_is_this_us (self->s, nick))
|
2015-06-28 02:49:28 +02:00
|
|
|
color = -1;
|
|
|
|
|
|
|
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = color);
|
|
|
|
|
|
2016-04-03 04:05:04 +02:00
|
|
|
char *x = irc_to_utf8 (nick);
|
2015-06-28 02:49:28 +02:00
|
|
|
free (nick);
|
|
|
|
|
FORMATTER_ADD_TEXT (self, x);
|
|
|
|
|
free (x);
|
|
|
|
|
|
2020-10-11 17:36:21 +02:00
|
|
|
// Need to reset the colour afterwards
|
2015-06-28 02:49:28 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1);
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-29 08:54:00 +02:00
|
|
|
static void
|
2016-10-23 13:38:46 +02:00
|
|
|
formatter_parse_nick_full (struct formatter *self, const char *s)
|
2015-06-29 08:54:00 +02:00
|
|
|
{
|
|
|
|
|
formatter_parse_nick (self, s);
|
|
|
|
|
|
|
|
|
|
const char *userhost;
|
|
|
|
|
if (!(userhost = irc_find_userhost (s)))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
FORMATTER_ADD_TEXT (self, " (");
|
|
|
|
|
FORMATTER_ADD_ITEM (self, ATTR, .attribute = ATTR_USERHOST);
|
|
|
|
|
|
2016-04-03 04:05:04 +02:00
|
|
|
char *x = irc_to_utf8 (userhost);
|
2015-06-29 08:54:00 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, x);
|
|
|
|
|
free (x);
|
|
|
|
|
|
|
|
|
|
FORMATTER_ADD_RESET (self);
|
|
|
|
|
FORMATTER_ADD_TEXT (self, ")");
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-26 18:23:43 +02:00
|
|
|
static const char *
|
|
|
|
|
formatter_parse_field (struct formatter *self,
|
|
|
|
|
const char *field, struct str *buf, va_list *ap)
|
|
|
|
|
{
|
2015-06-28 02:49:28 +02:00
|
|
|
bool free_string = false;
|
|
|
|
|
char *s = NULL;
|
|
|
|
|
char *tmp = NULL;
|
2015-04-26 18:23:43 +02:00
|
|
|
int c;
|
|
|
|
|
|
|
|
|
|
restart:
|
|
|
|
|
switch ((c = *field++))
|
|
|
|
|
{
|
|
|
|
|
// We can push boring text content to the caller's buffer
|
|
|
|
|
// and let it flush the buffer only when it's actually needed
|
2015-06-28 02:49:28 +02:00
|
|
|
case 'd':
|
|
|
|
|
tmp = xstrdup_printf ("%d", va_arg (*ap, int));
|
|
|
|
|
str_append (buf, tmp);
|
|
|
|
|
free (tmp);
|
|
|
|
|
break;
|
2015-04-26 18:23:43 +02:00
|
|
|
case 's':
|
2015-06-28 02:49:28 +02:00
|
|
|
str_append (buf, (s = va_arg (*ap, char *)));
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
2015-12-25 21:22:59 +01:00
|
|
|
case 'l':
|
|
|
|
|
if (!(tmp = iconv_xstrdup (self->ctx->term_to_utf8,
|
|
|
|
|
(s = va_arg (*ap, char *)), -1, NULL)))
|
|
|
|
|
print_error ("character conversion failed for: %s", "output");
|
|
|
|
|
else
|
|
|
|
|
str_append (buf, tmp);
|
|
|
|
|
free (tmp);
|
|
|
|
|
break;
|
2015-06-28 02:49:28 +02:00
|
|
|
|
|
|
|
|
case 'S':
|
2016-04-03 04:05:04 +02:00
|
|
|
tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
|
2015-06-28 02:49:28 +02:00
|
|
|
str_append (buf, tmp);
|
|
|
|
|
free (tmp);
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
2015-05-10 09:55:44 +02:00
|
|
|
case 'm':
|
2016-04-03 04:05:04 +02:00
|
|
|
tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
|
2015-06-28 02:49:28 +02:00
|
|
|
formatter_parse_mirc (self, tmp);
|
|
|
|
|
free (tmp);
|
|
|
|
|
break;
|
|
|
|
|
case 'n':
|
|
|
|
|
formatter_parse_nick (self, (s = va_arg (*ap, char *)));
|
2015-05-10 09:55:44 +02:00
|
|
|
break;
|
2015-06-29 08:54:00 +02:00
|
|
|
case 'N':
|
|
|
|
|
formatter_parse_nick_full (self, (s = va_arg (*ap, char *)));
|
|
|
|
|
break;
|
2015-05-10 09:55:44 +02:00
|
|
|
|
2015-04-26 18:23:43 +02:00
|
|
|
case 'a':
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, ATTR, .attribute = va_arg (*ap, int));
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
|
|
|
|
case 'c':
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, FG_COLOR, .color = va_arg (*ap, int));
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
|
|
|
|
case 'C':
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_ITEM (self, BG_COLOR, .color = va_arg (*ap, int));
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
|
|
|
|
case 'r':
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_RESET (self);
|
2015-04-26 18:23:43 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
2015-06-28 02:49:28 +02:00
|
|
|
if (c == '&' && !free_string)
|
|
|
|
|
free_string = true;
|
2015-04-26 18:23:43 +02:00
|
|
|
else if (c)
|
|
|
|
|
hard_assert (!"unexpected format specifier");
|
|
|
|
|
else
|
|
|
|
|
hard_assert (!"unexpected end of format string");
|
|
|
|
|
goto restart;
|
|
|
|
|
}
|
2015-06-28 02:49:28 +02:00
|
|
|
|
|
|
|
|
if (free_string)
|
|
|
|
|
free (s);
|
2015-04-26 18:23:43 +02:00
|
|
|
return field;
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
// I was unable to take a pointer of a bare "va_list" when it was passed in
|
|
|
|
|
// as a function argument, so it has to be a pointer from the beginning
|
2015-04-26 18:23:43 +02:00
|
|
|
static void
|
2015-06-28 02:49:28 +02:00
|
|
|
formatter_addv (struct formatter *self, const char *format, va_list *ap)
|
2015-04-26 18:23:43 +02:00
|
|
|
{
|
2017-06-22 22:39:39 +02:00
|
|
|
struct str buf = str_make ();
|
2015-04-26 18:23:43 +02:00
|
|
|
while (*format)
|
|
|
|
|
{
|
|
|
|
|
if (*format != '#' || *++format == '#')
|
|
|
|
|
{
|
|
|
|
|
str_append_c (&buf, *format++);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (buf.len)
|
|
|
|
|
{
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
2015-04-26 18:23:43 +02:00
|
|
|
str_reset (&buf);
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
format = formatter_parse_field (self, format, &buf, ap);
|
2015-04-26 18:23:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (buf.len)
|
2015-05-10 23:06:19 +02:00
|
|
|
FORMATTER_ADD_TEXT (self, buf.str);
|
2015-04-26 18:23:43 +02:00
|
|
|
|
|
|
|
|
str_free (&buf);
|
2015-06-28 02:49:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
formatter_add (struct formatter *self, const char *format, ...)
|
|
|
|
|
{
|
|
|
|
|
va_list ap;
|
|
|
|
|
va_start (ap, format);
|
|
|
|
|
formatter_addv (self, format, &ap);
|
2015-04-26 18:23:43 +02:00
|
|
|
va_end (ap);
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-21 00:30:59 +01:00
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
struct line_char_attrs
|
|
|
|
|
{
|
2020-10-19 05:19:28 +02:00
|
|
|
short named; ///< Named attribute or -1
|
|
|
|
|
short text; ///< Text attributes
|
2020-10-11 17:36:21 +02:00
|
|
|
int fg; ///< Foreground colour (-1 for default)
|
|
|
|
|
int bg; ///< Background colour (-1 for default)
|
2016-03-21 00:30:59 +01:00
|
|
|
};
|
|
|
|
|
|
2020-10-19 23:37:19 +02:00
|
|
|
// We can get rid of the linked list and do this in one allocation (use strlen()
|
|
|
|
|
// for the upper bound)--since we only prepend and/or replace characters, add
|
|
|
|
|
// a member to specify the prepended character and how many times to repeat it.
|
|
|
|
|
// Tabs may nullify the wide character but it's not necessary.
|
|
|
|
|
//
|
|
|
|
|
// This would be slighly more optimal but it would also set the algorithm in
|
|
|
|
|
// stone and complicate flushing.
|
|
|
|
|
|
2016-03-21 00:30:59 +01:00
|
|
|
struct line_char
|
|
|
|
|
{
|
|
|
|
|
LIST_HEADER (struct line_char)
|
|
|
|
|
|
|
|
|
|
wchar_t wide; ///< The character as a wchar_t
|
|
|
|
|
int width; ///< Width of the character in cells
|
|
|
|
|
struct line_char_attrs attrs; ///< Attributes
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static struct line_char *
|
2020-10-19 05:19:28 +02:00
|
|
|
line_char_new (wchar_t wc)
|
2016-03-21 00:30:59 +01:00
|
|
|
{
|
|
|
|
|
struct line_char *self = xcalloc (1, sizeof *self);
|
|
|
|
|
self->width = wcwidth ((self->wide = wc));
|
|
|
|
|
|
2016-03-26 20:59:37 +01:00
|
|
|
// Typically various control characters
|
|
|
|
|
if (self->width < 0)
|
|
|
|
|
self->width = 0;
|
|
|
|
|
|
2016-03-21 00:30:59 +01:00
|
|
|
self->attrs.bg = self->attrs.fg = -1;
|
|
|
|
|
self->attrs.named = ATTR_RESET;
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
struct line_wrap_mark
|
|
|
|
|
{
|
|
|
|
|
struct line_char *start; ///< First character
|
|
|
|
|
int used; ///< Display cells used
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
line_wrap_mark_push (struct line_wrap_mark *mark, struct line_char *c)
|
|
|
|
|
{
|
|
|
|
|
if (!mark->start)
|
|
|
|
|
mark->start = c;
|
|
|
|
|
mark->used += c->width;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct line_wrap_state
|
|
|
|
|
{
|
|
|
|
|
struct line_char *result; ///< Head of result
|
|
|
|
|
struct line_char *result_tail; ///< Tail of result
|
|
|
|
|
|
|
|
|
|
int line_used; ///< Line length before marks
|
|
|
|
|
int line_max; ///< Maximum line length
|
|
|
|
|
struct line_wrap_mark chunk; ///< All buffered text
|
|
|
|
|
struct line_wrap_mark overflow; ///< Overflowing text
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
line_wrap_flush_split (struct line_wrap_state *s, struct line_wrap_mark *before)
|
|
|
|
|
{
|
2020-10-19 05:19:28 +02:00
|
|
|
struct line_char *nl = line_char_new (L'\n');
|
2016-03-21 00:30:59 +01:00
|
|
|
LIST_INSERT_WITH_TAIL (s->result, s->result_tail, nl, before->start);
|
|
|
|
|
s->line_used = before->used;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
line_wrap_flush (struct line_wrap_state *s, bool force_split)
|
|
|
|
|
{
|
|
|
|
|
if (!s->overflow.start)
|
|
|
|
|
s->line_used += s->chunk.used;
|
|
|
|
|
else if (force_split || s->chunk.used > s->line_max)
|
|
|
|
|
{
|
|
|
|
|
#ifdef WRAP_UNNECESSARILY
|
2016-04-21 22:34:11 +02:00
|
|
|
// When the line wraps at the end of the screen and a background colour
|
|
|
|
|
// is set, the terminal paints the entire new line with that colour.
|
|
|
|
|
// Explicitly inserting a newline with the default attributes fixes it.
|
2016-03-21 00:30:59 +01:00
|
|
|
line_wrap_flush_split (s, &s->overflow);
|
|
|
|
|
#else
|
2016-04-21 22:34:11 +02:00
|
|
|
// Splitting here breaks link searching mechanisms in some terminals,
|
|
|
|
|
// though, so we make a trade-off and let the chunk wrap naturally.
|
|
|
|
|
// Fuck terminals, really.
|
2016-03-21 00:30:59 +01:00
|
|
|
s->line_used = s->overflow.used;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
// Print the chunk in its entirety on a new line
|
|
|
|
|
line_wrap_flush_split (s, &s->chunk);
|
|
|
|
|
|
|
|
|
|
memset (&s->chunk, 0, sizeof s->chunk);
|
|
|
|
|
memset (&s->overflow, 0, sizeof s->overflow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
line_wrap_nl (struct line_wrap_state *s)
|
|
|
|
|
{
|
|
|
|
|
line_wrap_flush (s, true);
|
2020-10-19 05:19:28 +02:00
|
|
|
struct line_char *nl = line_char_new (L'\n');
|
2016-03-21 00:30:59 +01:00
|
|
|
LIST_APPEND_WITH_TAIL (s->result, s->result_tail, nl);
|
|
|
|
|
s->line_used = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
line_wrap_tab (struct line_wrap_state *s, struct line_char *c)
|
|
|
|
|
{
|
|
|
|
|
line_wrap_flush (s, true);
|
|
|
|
|
if (s->line_used >= s->line_max)
|
|
|
|
|
line_wrap_nl (s);
|
|
|
|
|
|
|
|
|
|
// Compute the number of characters needed to get to the next tab stop
|
|
|
|
|
int tab_width = ((s->line_used + 8) & ~7) - s->line_used;
|
|
|
|
|
// On overflow just fill the rest of the line with spaces
|
|
|
|
|
if (s->line_used + tab_width > s->line_max)
|
|
|
|
|
tab_width = s->line_max - s->line_used;
|
|
|
|
|
|
|
|
|
|
s->line_used += tab_width;
|
|
|
|
|
while (tab_width--)
|
|
|
|
|
{
|
2020-10-19 05:19:28 +02:00
|
|
|
struct line_char *space = line_char_new (L' ');
|
2016-03-21 00:30:59 +01:00
|
|
|
space->attrs = c->attrs;
|
|
|
|
|
LIST_APPEND_WITH_TAIL (s->result, s->result_tail, space);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
line_wrap_push_char (struct line_wrap_state *s, struct line_char *c)
|
|
|
|
|
{
|
|
|
|
|
// Note that when processing whitespace here, any non-WS chunk has already
|
|
|
|
|
// been flushed, and thus it matters little if we flush with force split
|
|
|
|
|
if (wcschr (L"\r\f\v", c->wide))
|
|
|
|
|
/* Skip problematic characters */;
|
|
|
|
|
else if (c->wide == L'\n')
|
|
|
|
|
line_wrap_nl (s);
|
|
|
|
|
else if (c->wide == L'\t')
|
|
|
|
|
line_wrap_tab (s, c);
|
|
|
|
|
else
|
|
|
|
|
goto use_as_is;
|
|
|
|
|
free (c);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
use_as_is:
|
|
|
|
|
if (s->overflow.start
|
|
|
|
|
|| s->line_used + s->chunk.used + c->width > s->line_max)
|
|
|
|
|
{
|
|
|
|
|
if (s->overflow.used + c->width > s->line_max)
|
|
|
|
|
{
|
|
|
|
|
#ifdef WRAP_UNNECESSARILY
|
|
|
|
|
// If the overflow overflows, restart on a new line
|
|
|
|
|
line_wrap_nl (s);
|
|
|
|
|
#else
|
|
|
|
|
// See line_wrap_flush(), we would end up on a new line anyway
|
|
|
|
|
line_wrap_flush (s, true);
|
|
|
|
|
s->line_used = 0;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
line_wrap_mark_push (&s->overflow, c);
|
|
|
|
|
}
|
|
|
|
|
line_wrap_mark_push (&s->chunk, c);
|
|
|
|
|
LIST_APPEND_WITH_TAIL (s->result, s->result_tail, c);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Basic word wrapping that respects wcwidth(3) and expands tabs.
|
|
|
|
|
/// Besides making text easier to read, it also fixes the problem with
|
|
|
|
|
/// formatting spilling over the entire new line on line wrap.
|
|
|
|
|
static struct line_char *
|
|
|
|
|
line_wrap (struct line_char *line, int max_width)
|
|
|
|
|
{
|
|
|
|
|
struct line_wrap_state s = { .line_max = max_width };
|
|
|
|
|
bool last_was_word_char = false;
|
|
|
|
|
LIST_FOR_EACH (struct line_char, c, line)
|
|
|
|
|
{
|
|
|
|
|
// Act on the right boundary of (\s*\S+) chunks
|
|
|
|
|
bool this_is_word_char = !wcschr (L" \t\r\n\f\v", c->wide);
|
|
|
|
|
if (last_was_word_char && !this_is_word_char)
|
|
|
|
|
line_wrap_flush (&s, false);
|
|
|
|
|
last_was_word_char = this_is_word_char;
|
|
|
|
|
|
|
|
|
|
LIST_UNLINK (line, c);
|
|
|
|
|
line_wrap_push_char (&s, c);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make sure to process the last word and return the modified list
|
|
|
|
|
line_wrap_flush (&s, false);
|
|
|
|
|
return s.result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
|
|
struct exploder
|
|
|
|
|
{
|
|
|
|
|
struct app_context *ctx; ///< Application context
|
|
|
|
|
struct line_char *result; ///< Result
|
|
|
|
|
struct line_char *result_tail; ///< Tail of result
|
|
|
|
|
struct line_char_attrs attrs; ///< Current attributes
|
|
|
|
|
};
|
|
|
|
|
|
2015-06-28 02:49:28 +02:00
|
|
|
static bool
|
2016-03-21 00:30:59 +01:00
|
|
|
explode_formatter_attr (struct exploder *self, struct formatter_item *item)
|
2015-06-28 02:49:28 +02:00
|
|
|
{
|
|
|
|
|
switch (item->type)
|
|
|
|
|
{
|
|
|
|
|
case FORMATTER_ITEM_ATTR:
|
2016-03-21 00:30:59 +01:00
|
|
|
self->attrs.named = item->attribute;
|
|
|
|
|
self->attrs.text = 0;
|
|
|
|
|
self->attrs.fg = -1;
|
|
|
|
|
self->attrs.bg = -1;
|
2015-06-28 02:49:28 +02:00
|
|
|
return true;
|
|
|
|
|
case FORMATTER_ITEM_SIMPLE:
|
2016-03-21 00:30:59 +01:00
|
|
|
self->attrs.named = -1;
|
|
|
|
|
self->attrs.text ^= item->attribute;
|
2015-06-28 02:49:28 +02:00
|
|
|
return true;
|
|
|
|
|
case FORMATTER_ITEM_FG_COLOR:
|
2016-03-21 00:30:59 +01:00
|
|
|
self->attrs.named = -1;
|
|
|
|
|
self->attrs.fg = item->color;
|
2015-06-28 02:49:28 +02:00
|
|
|
return true;
|
|
|
|
|
case FORMATTER_ITEM_BG_COLOR:
|
2016-03-21 00:30:59 +01:00
|
|
|
self->attrs.named = -1;
|
|
|
|
|
self->attrs.bg = item->color;
|
2015-06-28 02:49:28 +02:00
|
|
|
return true;
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-11 04:40:48 +02:00
|
|
|
static void
|
2016-03-21 00:30:59 +01:00
|
|
|
explode_text (struct exploder *self, const char *text)
|
2015-07-11 04:40:48 +02:00
|
|
|
{
|
2017-07-07 20:45:24 +02:00
|
|
|
// Throw away any potentially harmful control characters first
|
|
|
|
|
struct str filtered = str_make ();
|
|
|
|
|
for (const char *p = text; *p; p++)
|
2020-10-04 12:04:24 +02:00
|
|
|
if (!strchr ("\a\b\x0e\x0f\x1b" /* BEL BS SO SI ESC */, *p))
|
2017-07-07 20:45:24 +02:00
|
|
|
str_append_c (&filtered, *p);
|
|
|
|
|
|
2016-03-21 00:30:59 +01:00
|
|
|
size_t term_len = 0;
|
|
|
|
|
char *term = iconv_xstrdup (self->ctx->term_from_utf8,
|
2017-07-07 20:45:24 +02:00
|
|
|
filtered.str, filtered.len + 1, &term_len);
|
|
|
|
|
str_free (&filtered);
|
2016-03-21 00:30:59 +01:00
|
|
|
|
|
|
|
|
mbstate_t ps;
|
|
|
|
|
memset (&ps, 0, sizeof ps);
|
|
|
|
|
|
|
|
|
|
wchar_t wch;
|
|
|
|
|
size_t len, processed = 0;
|
|
|
|
|
while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps)))
|
|
|
|
|
{
|
|
|
|
|
hard_assert (len != (size_t) -2 && len != (size_t) -1);
|
|
|
|
|
processed += len;
|
2015-07-11 04:40:48 +02:00
|
|
|
|
2020-10-19 05:19:28 +02:00
|
|
|
struct line_char *c = line_char_new (wch);
|
2016-03-21 00:30:59 +01:00
|
|
|
c->attrs = self->attrs;
|
|
|
|
|
LIST_APPEND_WITH_TAIL (self->result, self->result_tail, c);
|
|
|
|
|
}
|
2015-07-11 04:40:48 +02:00
|
|
|
free (term);
|
2016-03-21 00:30:59 +01:00
|
|
|
}
|
2015-07-11 04:40:48 +02:00
|
|
|
|
2016-03-21 00:30:59 +01:00
|
|
|
static struct line_char *
|
|
|
|
|
formatter_to_chars (struct formatter *formatter)
|
|
|
|
|
{
|
|
|
|
|
struct exploder self = { .ctx = formatter->ctx };
|
|
|
|
|
self.attrs.fg = self.attrs.bg = self.attrs.named = -1;
|
|
|
|
|
|
|
|
|
|