~rcr/rirc

55d42ad851fb88e5f8dd5e223b37054700c65ae6 — Richard Robbins 3 years ago d9d0a0d + 7774a4a
Merge branch 'static_analysis'
M .travis.yml => .travis.yml +15 -4
@@ 3,7 3,6 @@ language: c

env:
  global:
    # COVERITY_SCAN_TOKEN
    - secure: "NXwB/s0ZpQr0kJTESNxuVCrIVgX2ZsivmiODwK2LDFT1Nchdijd/Ope0cHb92FxURLcDetnl69EWlmLD5ROZQ5Qc+XakAdAEfy2ZrrCq14S9fSZzUtLB0wMW/GZ5hVneSnFmbprR3zQbTbcVvpTyvzDEr26Dd+HQNP0QQsOJ7B5kanLwKeiuNy437pjIOm7x4sjaylnA/b6FlV3a26C7eGN2ZiFCGT++9y3YlJAlxNlySSI+phRe0bihna/RygoYAs5efXefAuibEjtG5kL29cKMKzwEdXEFfDZcl0QYjkrAJqXUUC54YqIWnY56mhDzn0nfWIScQYth0ZS/vhteA8x+MYKFgGnqD9LBw+TsvRO3ZQ1iVrI5NBM6P2Ka6mAVv8OODN2EwWpwoqm2PJ36ZZ+2vJHBwHw10i3pDR48Fzw0x9Av6rkI5lS0hFLMogXtTaM2IlWu4VIevVRBICzrEz47LeOvJs17MZPYeOUhCFrG0ZP/8DR9nqJpmHVFA9FYcaalD/SXfDbBJTI+yV37mgfSM1zJ8tApPy4KOZ1wM2F0+COqEdxHqTJp2u9c1VQo1NegsYxxdB3mOYMLrTnAzQ5WOUdDezcFkAT+QE5WVhGUzpT4lzM1kVirJa2y3beh2i7uHfP2X+EtzJvWRbefJYQJNo8nYXXEwpdJ5v/0LyI="

addons:


@@ 13,9 12,12 @@ addons:
      description: "Coverity scan of the static_analysis branch"
    notification_email: mail@rcr.io
    build_command_prepend: "make clean"
    build_command: "make rirc debug test -j2"
    build_command: "make test debug rirc -j2"
    branch_pattern: static_analysis

  sonarcloud:
    organization: "rcr-github"

matrix:
  include:
    - os: linux


@@ 31,5 33,14 @@ before_install:
  - test "${TRAVIS_BRANCH}" != "static_analysis" -o "${TRAVIS_JOB_NUMBER##*.}" = "1" || exit 0

script:
  - test "${TRAVIS_BRANCH}" != "static_analysis" || exit 0
  - make test rirc
  - set -e
  - make clean test rirc
  - test "${TRAVIS_JOB_NUMBER##*.}" = "1" || exit 0
  - make clean
  - build-wrapper-linux-x86-64 --out-dir bw-output make debug
  - make test
  - echo "sonar.projectKey=rcr_rirc" > sonar-project.properties
  - echo "sonar.sources=src" >> sonar-project.properties
  - echo "sonar.tests=test" >> sonar-project.properties
  - echo "sonar.cfamily.build-wrapper-output=bw-output" >> sonar-project.properties
  - sonar-scanner

M CHANGELOG => CHANGELOG +18 -2
@@ 2,11 2,27 @@
Summary of notable changes and features

## Unreleased (dev)
merged

## [0.1.1]
### Features
 - tab completion for :commands
### Refactor
 - input module complete rewrite
   - generic word completion API
   - add full test coverage
   - performance and memory usage improvements
   - tab completion as generic callbacks
### Fixes
 - fix error in sending PING
 - fix error messages on socket disconnect
 - fix fatal error on /quit
 - close/cleanup servers on :quit
 - print message on channel re-join
 - fix recursive exit() calls on fatal

## [0.1.0]
### Initial release
Started in 2014 as a hobby project, rirc has been a fully useable,
Started in 2013 as a hobby project, rirc has been a fully useable,
albeit basic, IRC client since it's early inception. This version
marks an initial release supporting a basic subset of the IRC
protocol and client management features one would expect in everyday

M Makefile => Makefile +10 -11
@@ 1,22 1,26 @@
.POSIX:

VERSION = 0.1.0
VERSION = 0.1.1

# Release and debug build executable names
EXE_R := rirc
EXE_D := rirc.debug

# Install paths
EXE_DIR = /usr/local/bin
MAN_DIR = /usr/local/share/man/man1

STANDARDS = -std=c99 \
 -D_POSIX_C_SOURCE=200812L \
 -D_DARWIN_C_SOURCE=200812L \
 -D_POSIX_C_SOURCE=200112L \
 -D_DARWIN_C_SOURCE=200112L \
 -D_BSD_VISIBLE=1

CC = cc
PP = cc -E
CFLAGS    = -I. $(STANDARDS) -DVERSION=\"$(VERSION)\" $(D_EXT) -Wall -Wextra -pedantic -O2 -flto
CFLAGS_D  = -I. $(STANDARDS) -DVERSION=\"$(VERSION)\" $(D_EXT) -Wall -Wextra -pedantic -O0 -g -DDEBUG
LDFLAGS   = -pthread
LDFLAGS_D = -pthread
LDFLAGS   = $(L_EXT) -pthread
LDFLAGS_D = $(L_EXT) -pthread

# Build, source, test source directories
DIR_B := bld


@@ 34,10 38,6 @@ OBJS_R := $(patsubst %.c, $(DIR_B)/%.o,    $(SRC))
OBJS_D := $(patsubst %.c, $(DIR_B)/%.db.o, $(SRC))
OBJS_T := $(patsubst %.c, $(DIR_B)/%.t,    $(SRC_T))

# Release build, Debug build
EXE_R := rirc
EXE_D := rirc.debug

# Release build executable
$(EXE_R): $(DIR_B) $(OBJS_R)
	@echo cc $@


@@ 81,8 81,7 @@ clean:
	rm -rf $(DIR_B) $(EXE_R) $(EXE_D)

define make-dirs
	for dir in $(SRCDIRS);   do mkdir -p $(DIR_B)/$$dir; done
	for dir in $(SRCDIRS_T); do mkdir -p $(DIR_B)/$$dir; done
	for dir in $(SRCDIRS) $(SRCDIRS_T); do mkdir -p $(DIR_B)/$$dir; done
endef

install: $(EXE_R)

M README.md => README.md +27 -5
@@ 1,9 1,27 @@
[![Coverity Scan Build](https://scan.coverity.com/projects/4940/badge.svg)](https://scan.coverity.com/projects/4940)
![Sonarcloud](https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=ncloc)
<p align="center">
  <img src="https://raw.githubusercontent.com/rcr/rirc/master/docs/birb.jpg" alt="birb"/>
</p>

# rirc
![rirc](docs/birb.jpg?raw=true "rirc")
---

<p align="center">
  <a href="https://sonarcloud.io/dashboard?id=rcr_rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=ncloc"/>
  </a>
  <a href="https://scan.coverity.com/projects/4940">
    <img alt="coverity" src="https://scan.coverity.com/projects/4940/badge.svg"/>
  </a>
  <a href="https://sonarcloud.io/dashboard?id=rcr_rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=sqale_rating"/>
  </a>
  <a href="https://sonarcloud.io/dashboard?id=rcr_rirc">
    <img alt="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=security_rating"/>
  </a>
</p>

---

# rirc
A minimalistic irc client written in C.

While still under development, it currently supports


@@ 26,7 44,7 @@ make install

## Usage:
```
  rirc [-hv] [-s server [-p port] [-w pass] [-n nicks] [-c chans] [-u user] [-r real]], ...]
  rirc [-hv] [-s server [-p port] [-w pass] [-u user] [-r real] [-n nicks] [-c chans]], ...]

Help:
  -h, --help            Print this message and exit


@@ 60,6 78,10 @@ Keys:
  ^C : cancel input/action
  ^U : scroll buffer up
  ^D : scroll buffer down
   ← : input cursor back
   → : input cursor forward
   ↑ : input history back
   ↓ : input history forward
```

## More info:

M TODO => TODO +33 -17
@@ 1,16 1,39 @@
FIXME:
UNHANDLED ~ 461 PING :Not enough parameters
 - add basic ssl support, with ifdef guards, see lessandro's patch
 - remove server channel from channel list? does this break searching?
   what benefit is there to this? s->channel can then just be s->clist.head
 - are join/part/quit messages working with the threshold?
 - server: channel/ulist/clist
 - posix 2001 with removal of recusive mutex?
 - fatal/error/debug macros with reusage

 - when reconnecting, if cant rejoin channel (e.g. archlinux) and bumped
   to -unregistered... closing it causes 442: not on that channel...
   what happens to messages sent to it?
	If in ##channel that requires authentication (ie bumps you to ##channel-unauthorized
	or similar), /disconnect, /connect, rirc attempts to join ##channel, can't, and is
	bumped to ##channel-unauthorized, but ##channel buffer remains open and not flagged
	as parted, ie could potentially send messages to the channel, but won't receive any
		- or it returns 403, and cant /join the channel because it's not parted

TODO:
 - Make sure all the thread related code is using thread-safe functions,
   and that proper cancellation points are used. Make sure no resources
   can be left hanging (eg: sockets left open when a thread is canceled?)


TODO
---------------------------
next iteration
	- RFC2812 compliance
	- Split and prototype:
		src/handlers
		  \ recv
		  \ send
		  \ command
		  \ numeric
		  \ ctcp
		  \ sasl

Continue refactoring:
	- Untangle headers
	- Rewrite input DLL as circular buffer of gap buffers, with copy on repeat,
		use abstractions in _draw_input
	- Error/warning writing from stateless components, e.g.:
		- mode.c should be able to call err("...") on some passed parameter
		  and have it written to the appropriate buffer with the appropriate


@@ 22,6 45,7 @@ Continue refactoring:
		  be added
	- more debug information, levels, for various events, a debug pannel as a new channel
	  type when debug flag is set
	- split action handling out

action_error
display error to user for confirmation, e.g. :connect <server>


@@ 39,13 63,15 @@ Testing:
		- ./test/script.sh ?
	- Fuzzing handlers (afl)
	- Try musl C
	- include-what-you-use for header detangling
	- clang tidy, .clang-tidy config

buffer performance improvements:
	- reduce overall size, pack strings in contiguous realloc'ed array
	- cache line breaks
	- pre-format time

abstract list.h from channel/server/input lists
abstract list.h from channel/server

SASL auth:
	- cli -a/--auth


@@ 452,10 478,6 @@ show disconnected/parted in status bar for channels instead of 0 user count

Keep state of tab complete for successively getting the next nick lexicographically

Make sure all the thread related code is using thread-safe functions,
and that proper cancellation points are used. Make sure no resources
can be left hanging (eg: sockets left open when a thread is canceled?)

Empty trailing should be null, e.g.:
":nick!~user@host PART #channel :" -> "< ~ nick!~user@host has left #channel ()"



@@ 470,12 492,6 @@ parsing whatever it last parsed. Instead, a function like strsep should be imple
which correctly handles NULL input in this case
	-> replace stringtok/stringtok_r with getarg

If in ##channel that requires authentication (ie bumps you to ##channel-unauthorized
or similar), /disconnect, /connect, rirc attempts to join ##channel, can't, and is
bumped to ##channel-unauthorized, but ##channel buffer remains open and not flagged
as parted, ie could potentially send messages to the channel, but won't receive any
	- or it returns 403, and cant /join the channel because it's not parted

use the alternate screen buffer to restore screen when rirc exits

##linux ~ cannot send to channel

M rirc.1 => rirc.1 +9 -5
@@ 11,14 11,14 @@ rirc \- a minimalistic irc client written in C
.IR port ]
.RB [ -w
.IR pass ]
.RB [ -u
.IR username ]
.RB [ -r
.IR realname ]
.RB [ -n
.IR nicks ]
.RB [ -c
.IR chans "]
.RB [ -u
.IR username "]
.RB [ -r
.IR realname "]], ...]"
.IR chans "]], ...]"
.SH DESCRIPTION
.P
rirc is a lightweight Internet Relay Chat client.


@@ 93,6 93,10 @@ Keys:
  ^X;close current channel
  ^U;scroll current buffer up
  ^D;scroll current buffer down
   >;input cursor back
   <;input cursor forward
   ^;input history back
   v;input history forward
.TE

.TS

M src/components/README.md => src/components/README.md +2 -0
@@ 9,6 9,8 @@ Stateful component hierarchy
       |   |
       |   |__*buffer_line
       |
       |__channel
       |
       |__channel_list
       |   |
       |   |__*channel

M src/components/buffer.c => src/components/buffer.c +34 -32
@@ 30,12 30,12 @@

#include "src/components/buffer.h"

#if (BUFFER_LINES_MAX & (BUFFER_LINES_MAX - 1)) != 0
	/* Required for proper masking when indexing */
	#error BUFFER_LINES_MAX must be a power of 2
#endif
#define BUFFER_MASK(X) ((X) & (BUFFER_LINES_MAX - 1))

#define MASK(X) ((X) & (BUFFER_LINES_MAX - 1))
#if BUFFER_MASK(BUFFER_LINES_MAX)
/* Required for proper masking when indexing */
#error BUFFER_LINES_MAX must be a power of 2
#endif

static inline unsigned int buffer_full(struct buffer*);
static inline unsigned int buffer_size(struct buffer*);


@@ 73,7 73,7 @@ buffer_push(struct buffer *b)
		b->tail++;
	}

	return &b->buffer_lines[MASK(b->head++)];
	return &b->buffer_lines[BUFFER_MASK(b->head++)];
}

struct buffer_line*


@@ 81,7 81,7 @@ buffer_head(struct buffer *b)
{
	/* Return the first printable line in a buffer */

	return buffer_size(b) == 0 ? NULL : &b->buffer_lines[MASK(b->head - 1)];
	return buffer_size(b) == 0 ? NULL : &b->buffer_lines[BUFFER_MASK(b->head - 1)];
}

struct buffer_line*


@@ 89,7 89,7 @@ buffer_tail(struct buffer *b)
{
	/* Return the last printable line in a buffer */

	return buffer_size(b) == 0 ? NULL : &b->buffer_lines[MASK(b->tail)];
	return buffer_size(b) == 0 ? NULL : &b->buffer_lines[BUFFER_MASK(b->tail)];
}

struct buffer_line*


@@ 119,9 119,9 @@ buffer_line(struct buffer *b, unsigned int i)

	if (((b->head > b->tail) && (i < b->tail || i >= b->head)) ||
	    ((b->tail > b->head) && (i < b->tail && i >= b->head)))
		fatal("invalid index", 0);
		fatal("invalid index: %d", i);

	return &b->buffer_lines[MASK(i)];
	return &b->buffer_lines[BUFFER_MASK(i)];
}

unsigned int


@@ 132,7 132,7 @@ buffer_line_rows(struct buffer_line *line, unsigned int w)
	char *p;

	if (w == 0)
		fatal("width is zero", 0);
		fatal("width is zero");

	/* Empty lines are considered to occupy a row */
	if (!*line->text)


@@ 152,28 152,30 @@ void
buffer_newline(
		struct buffer *b,
		enum buffer_line_t type,
		struct string from,
		struct string text,
		const char *from_str,
		const char *text_str,
		size_t from_len,
		size_t text_len,
		char prefix)
{
	struct buffer_line *line;

	if (from.str == NULL)
		fatal("from string is NULL", 0);
	if (from_str == NULL)
		fatal("from string is NULL");

	if (text.str == NULL)
		fatal("text string is NULL", 0);
	if (text_str == NULL)
		fatal("text string is NULL");

	line = memset(buffer_push(b), 0, sizeof(*line));

	line->from_len = MIN(from.len + (!!prefix), FROM_LENGTH_MAX);
	line->text_len = MIN(text.len,              TEXT_LENGTH_MAX);
	line->from_len = MIN(from_len + (!!prefix), FROM_LENGTH_MAX);
	line->text_len = MIN(text_len,              TEXT_LENGTH_MAX);

	if (prefix)
		*line->from = prefix;

	memcpy(line->from + (!!prefix), from.str, line->from_len);
	memcpy(line->text,              text.str, line->text_len);
	memcpy(line->from + (!!prefix), from_str, line->from_len);
	memcpy(line->text,              text_str, line->text_len);

	*(line->from + line->from_len) = '\0';
	*(line->text + line->text_len) = '\0';


@@ 184,14 186,14 @@ buffer_newline(
	if (line->from_len > b->pad)
		b->pad = line->from_len;

	if (text.len > TEXT_LENGTH_MAX) {

		struct string _text = {
			.str = text.str + TEXT_LENGTH_MAX,
			.len = text.len - TEXT_LENGTH_MAX
		};

		buffer_newline(b, type, from, _text, prefix);
	if (text_len > TEXT_LENGTH_MAX) {
		buffer_newline(b,
				type,
				from_str,
				text_str + TEXT_LENGTH_MAX,
				from_len,
				text_len - TEXT_LENGTH_MAX,
				prefix);
	}
}



@@ 206,10 208,10 @@ buffer_scrollback_status(struct buffer *b)
	return (float)(b->head - b->scrollback) / (float)(buffer_size(b));
}

struct buffer
buffer(void)
void
buffer(struct buffer *b)
{
	/* Initialize a buffer */

	return (struct buffer) {0};
	memset(b, 0, sizeof(*b));
}

M src/components/buffer.h => src/components/buffer.h +10 -3
@@ 10,7 10,7 @@
#define FROM_LENGTH_MAX 100

#ifndef BUFFER_LINES_MAX
	#define BUFFER_LINES_MAX (1 << 10)
#define BUFFER_LINES_MAX (1 << 10)
#endif

/* Buffer line types, in order of precedence */


@@ 55,12 55,19 @@ int buffer_page_forw(struct buffer*, unsigned int, unsigned int);

unsigned int buffer_line_rows(struct buffer_line*, unsigned int);

struct buffer buffer(void);
void buffer(struct buffer*);

struct buffer_line* buffer_head(struct buffer*);
struct buffer_line* buffer_tail(struct buffer*);
struct buffer_line* buffer_line(struct buffer*, unsigned int);

void buffer_newline(struct buffer*, enum buffer_line_t, struct string, struct string, char);
void buffer_newline(
	struct buffer*,
	enum buffer_line_t,
	const char*,
	const char*,
	size_t,
	size_t,
	char);

#endif

M src/components/channel.c => src/components/channel.c +26 -9
@@ 1,3 1,4 @@
#include <errno.h>
#include <stdlib.h>
#include <string.h>



@@ 10,7 11,7 @@ static inline int
channel_cmp(struct channel *c, const char *name)
{
	/* TODO: CASEMAPPING, as ftpr held by the server */
	return irc_strcmp(c->name.str, name);
	return irc_strcmp(c->name, name);
}

struct channel*


@@ 21,31 22,49 @@ channel(const char *name, enum channel_t type)
	size_t len = strlen(name);

	if ((c = calloc(1, sizeof(*c) + len + 1)) == NULL)
		fatal("calloc", errno);
		fatal("calloc: %s", strerror(errno));

	c->chanmodes_str.type = MODE_STR_CHANMODE;
	c->input = new_input();
	c->name.len = len;
	c->name.str = memcpy(c->_, name, len + 1);
	c->name_len = len;
	c->type = type;

	memcpy(c->name, name, len + 1);

	buffer(&c->buffer);
	input_init(&c->input);

	return c;
}

void
channel_free(struct channel *c)
{
	input_free(&c->input);
	user_list_free(&(c->users));
	free_input(c->input);
	free(c);
}

void
channel_list_free(struct channel_list *cl)
{
	struct channel *c1, *c2;

	if ((c1 = cl->head) == NULL)
		return;

	do {
		c2 = c1;
		c1 = c2->next;
		channel_free(c2);
	} while (c1 != cl->head);
}

struct channel*
channel_list_add(struct channel_list *cl, struct channel *c)
{
	struct channel *tmp;

	if ((tmp = channel_list_get(cl, c->name.str)) != NULL)
	if ((tmp = channel_list_get(cl, c->name)) != NULL)
		return tmp;

	if (cl->head == NULL) {


@@ 62,8 81,6 @@ channel_list_add(struct channel_list *cl, struct channel *c)
	return NULL;
}

// TODO: segault when deleting the tail and try to `prev` the head

struct channel*
channel_list_del(struct channel_list *cl, struct channel *c)
{

M src/components/channel.h => src/components/channel.h +5 -4
@@ 29,19 29,19 @@ enum channel_t

struct channel
{
	enum channel_t type;
	enum activity_t activity;
	enum channel_t type;
	size_t name_len;
	struct buffer buffer;
	struct channel *next;
	struct channel *prev;
	struct input *input;
	struct input input;
	struct mode chanmodes;
	struct mode_str chanmodes_str;
	struct server *server;
	struct string name;
	struct user_list users;
	unsigned int parted : 1;
	char _[];
	char name[];
};

struct channel_list


@@ 73,5 73,6 @@ void channel_part(struct channel*);
void channel_reset(struct channel*);

void channel_free(struct channel*);
void channel_list_free(struct channel_list*);

#endif

M src/components/input.c => src/components/input.c +220 -275
@@ 1,396 1,341 @@
/* TODO:
 * complete rewrite and unit test
 */

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

#include "src/components/input.h"
#include "src/state.h"
#include "src/utils/utils.h"

/* List of common IRC commands for tab completion */
static char* irc_commands[] = {
	"admin",    "away",     "clear",   "close",
	"connect",  "ctcp",     "die",     "disconnect",
	"encap",    "help",     "ignore",  "info",
	"invite",   "ison",     "join",    "kick",
	"kill",     "knock",    "links",   "list",
	"lusers",   "me",       "mode",    "motd",
	"msg",      "names",    "namesx",  "nick",
	"notice",   "oper",     "part",    "pass",
	"privmsg",  "quit",     "raw",     "rehash",
	"restart",  "rules",    "server",  "service",
	"servlist", "setname",  "silence", "squery",
	"squit",    "stats",    "summon",  "time",
	"topic",    "trace",    "uhnames", "unignore",
	"user",     "userhost", "userip",  "users",
	"version",  "wallops",  "watch",   "who",
	"whois",    "whowas",
	NULL
};

/* User input handlers */
static int input_char(struct input*, char);

/* Case insensitive tab complete for commands and nicks */
static int tab_complete_command(struct input*, char*, size_t);
static int tab_complete_nick(struct input*, struct user_list*, char*, size_t);

/* Input line util functions */
static inline void reset_line(struct input*);
static inline void reframe_line(struct input*, unsigned int);

static void new_list_head(struct input*);

//TODO: struct input input(struct input*)
struct input*
new_input(void)
{
	struct input *i;

	if ((i = calloc(1, sizeof(*i))) == NULL)
		fatal("calloc", errno);
#define INPUT_MASK(X) ((X) & (INPUT_HIST_MAX - 1))

	new_list_head(i);
#if INPUT_MASK(INPUT_HIST_MAX)
/* Required for proper masking when indexing */
#error INPUT_HIST_MAX must be a power of 2
#endif

	return i;
}
static char *input_text_copy(struct input*);
static int input_text_isfull(struct input*);
static int input_text_iszero(struct input*);
static uint16_t input_hist_size(struct input*);
static uint16_t input_text_size(struct input*);

/* TODO: ideally inputs shouldnt require being freed */
void
free_input(struct input *i)
input_init(struct input *inp)
{
	/* Free an input and all of it's lines */
	memset(inp, 0, sizeof(*inp));

	struct input_line *t, *l = i->list_head;

	do {
		t = l;
		l = l->next;
		free(t);
	} while (l != i->list_head);

	free(i);
	inp->tail = INPUT_LEN_MAX;
}

static void
new_list_head(struct input *i)
void
input_free(struct input *inp)
{
	/* Append a new line as the list_head */
	free(inp->hist.save);

	struct input_line *l;

	if ((l = calloc(1, sizeof(*l))) == NULL)
		fatal("calloc", errno);

	DLL_ADD(i->list_head, l);

	i->line = i->list_head = l;

	/* Gap buffer pointers */
	i->head = l->text;
	i->tail = l->text + RIRC_MAX_INPUT;

	i->window = l->text;
	while (inp->hist.tail != inp->hist.head)
		free(inp->hist.ptrs[INPUT_MASK(inp->hist.tail++)]);
}

int
input(struct input *inp, const char *buff, size_t count)
input_cursor_back(struct input *inp)
{
	/* Handle input, checking for control character or escape
	 * sequence. Otherwise copy all characters to the input struct
	 * of the current context */

	size_t start = count;

	while (count && input_char(inp, *buff)) {
		buff++;
		count--;
	}

	/* returns zero if no characters added */
	return (start != count);
}

/*
 * User input handlers
 * */

static int
input_char(struct input *inp, char c)
{
	/* Input a single character */

	if (inp->head >= inp->tail)
		/* Gap buffer is full */
	if (inp->head == 0)
		return 0;

	*inp->head++ = c;
	inp->text[--inp->tail] = inp->text[--inp->head];

	return 1;
}

/*
 * Input line manipulation functions
 * */

int
cursor_left(struct input *in)
input_cursor_forw(struct input *inp)
{
	/* Move the cursor left */
	if (inp->tail == INPUT_LEN_MAX)
		return 0;

	if (in->head > in->line->text) {
		*(--in->tail) = *(--in->head);
		return 1;
	}
	inp->text[inp->head++] = inp->text[inp->tail++];

	return 0;
	return 1;
}

int
cursor_right(struct input *in)
input_delete_back(struct input *inp)
{
	/* Move the cursor right */
	if (inp->head == 0)
		return 0;

	if (in->tail < in->line->text + RIRC_MAX_INPUT) {
		*(in->head++) = *(in->tail++);
		return 1;
	}
	inp->head--;

	return 0;
	return 1;
}

int
delete_left(struct input *in)
input_delete_forw(struct input *inp)
{
	/* Delete the character left of the cursor */
	if (inp->tail == INPUT_LEN_MAX)
		return 0;

	if (in->head > in->line->text) {
		in->head--;
		return 1;
	}
	inp->tail++;

	return 0;
	return 1;
}

int
delete_right(struct input *in)
input_insert(struct input *inp, const char *c, size_t count)
{
	/* Delete the character right of the cursor */
	/* TODO: may want to discard control characters */

	size_t i = count;

	if (in->tail < in->line->text + RIRC_MAX_INPUT) {
		in->tail++;
		return 1;
	while (!input_text_isfull(inp) && i--) {
		inp->text[inp->head++] = *c++;
	}

	return 0;
	return (i != count);
}

int
input_scroll_back(struct input *in, unsigned int cols)
input_reset(struct input *inp)
{
	/* Scroll backwards through the input history */

	/* Scrolling backwards on the last line */
	if (in->line->prev == in->list_head)
	if (input_text_iszero(inp))
		return 0;

	reset_line(in);
	in->line = in->line->prev;
	reframe_line(in, cols);
	free(inp->hist.save);

	inp->hist.current = inp->hist.head;
	inp->hist.save = NULL;

	inp->head = 0;
	inp->tail = INPUT_LEN_MAX;
	inp->window = 0;

	return 1;
}

int
input_scroll_forw(struct input *in, unsigned int cols)
input_complete(struct input *inp, f_completion_cb cb)
{
	/* Scroll forwards through the input history */
	/* Completion is valid when the cursor is:
	 *  - above any character in a word
	 *  - above a space immediately following a word
	 *  - at the end of a line following a word */

	/* Scrolling forward on the first line */
	if (in->line == in->list_head)
		return 0;
	uint16_t head, tail, ret;

	reset_line(in);
	in->line = in->line->next;
	reframe_line(in, cols);
	if (input_text_iszero(inp))
		return 0;

	return 1;
}
	head = inp->head;
	tail = inp->tail;

/*
 * Input line util functions
 * */
	while (head && inp->text[head - 1] != ' ')
		head--;

static inline void
reset_line(struct input *in)
{
	/* Reset a line's gap buffer pointers such that new chars are inserted at the gap head */
	if (inp->text[head] == ' ')
		return 0;

	char *h_tmp = in->head, *t_tmp = in->tail;
	while (tail < INPUT_LEN_MAX && inp->text[tail] != ' ')
		tail++;

	while (t_tmp < (in->line->text + RIRC_MAX_INPUT))
		*h_tmp++ = *t_tmp++;
	ret = (*cb)(
		(inp->text + head),
		(inp->head - head - inp->tail + tail),
		(INPUT_LEN_MAX - input_text_size(inp)),
		(head == 0));

	*h_tmp = '\0';
	if (ret) {
		inp->head = head + ret;
		inp->tail = tail;
	}

	in->line->end = h_tmp;
	return (ret != 0);
}

static inline void
reframe_line(struct input *in, unsigned int cols)
int
input_hist_back(struct input *inp)
{
	/* Reframe a line's draw window */
	size_t len;

	if (input_hist_size(inp) == 0 || inp->hist.current == inp->hist.tail)
		return 0;

	if (inp->hist.current == inp->hist.head) {
		inp->hist.save = input_text_copy(inp);
	} else {
		free(inp->hist.ptrs[INPUT_MASK(inp->hist.current)]);
		inp->hist.ptrs[INPUT_MASK(inp->hist.current)] = input_text_copy(inp);
	}

	in->head = in->line->end;
	in->tail = in->line->text + RIRC_MAX_INPUT;
	in->window = in->head - (2 * cols / 3);
	inp->hist.current--;

	if (in->window < in->line->text)
		in->window = in->line->text;
	len = strlen(inp->hist.ptrs[INPUT_MASK(inp->hist.current)]);
	memcpy(inp->text, inp->hist.ptrs[INPUT_MASK(inp->hist.current)], len);

	inp->head = len;
	inp->tail = INPUT_LEN_MAX;

	return 1;
}

int
tab_complete(struct input *inp, struct user_list *ul)
input_hist_forw(struct input *inp)
{
	/* Case insensitive tab complete for commands and nicks */

	char *str = inp->head;
	char *str;
	size_t len = 0;

	/* Don't tab complete at beginning of line or if previous character is space */
	if (inp->head == inp->line->text || *(inp->head - 1) == ' ')
	if (input_hist_size(inp) == 0 || inp->hist.current == inp->hist.head)
		return 0;

	/* Don't tab complete if cursor is scrolled left and next character isn't space */
	if (inp->tail < (inp->line->text + RIRC_MAX_INPUT) && *inp->tail != ' ')
		return 0;
	free(inp->hist.ptrs[INPUT_MASK(inp->hist.current)]);
	inp->hist.ptrs[INPUT_MASK(inp->hist.current)] = input_text_copy(inp);

	/* Scan backwards for the point to tab complete from */
	while (str > inp->line->text && *(str - 1) != ' ')
		len++, str--;
	inp->hist.current++;

	if (str == inp->line->text && *str == '/')
		return tab_complete_command(inp, ++str, --len);
	if (inp->hist.current == inp->hist.head)
		str = inp->hist.save;
	else
		return tab_complete_nick(inp, ul, str, len);
		str = inp->hist.ptrs[INPUT_MASK(inp->hist.current)];

	if (str) {
		len = strlen(str);
		memcpy(inp->text, str, len);
	}

	if (inp->hist.current == inp->hist.head) {
		free(inp->hist.save);
		inp->hist.save = NULL;
	}

	inp->head = len;
	inp->tail = INPUT_LEN_MAX;

	return 1;
}

static int
tab_complete_command(struct input *inp, char *str, size_t len)
int
input_hist_push(struct input *inp)
{
	/* Command tab completion */
	char *save;

	if ((save = input_text_copy(inp)) == NULL)
		return 0;

	char *p, **command = irc_commands;
	if (inp->hist.current < inp->hist.head) {

	while (*command && strncmp(*command, str, len))
		command++;
		uint16_t i;

	if (*command) {
		free(inp->hist.ptrs[INPUT_MASK(inp->hist.current)]);

		p = *command;
		for (i = inp->hist.current; i < inp->hist.head - 1; i++)
			inp->hist.ptrs[INPUT_MASK(i)] = inp->hist.ptrs[INPUT_MASK(i + 1)];

		/* Case insensitive matching, delete prefix */
		while (len--)
			delete_left(inp);
		inp->hist.current = i;
		inp->hist.ptrs[INPUT_MASK(inp->hist.current)] = save;

		while (*p && input_char(inp, *p++))
			;
	} else if (input_hist_size(inp) == INPUT_HIST_MAX) {

		input_char(inp, ' ');
		free(inp->hist.ptrs[INPUT_MASK(inp->hist.tail++)]);
		inp->hist.ptrs[INPUT_MASK(inp->hist.head++)] = save;

		return 1;
	} else {
		return 0;

		inp->hist.ptrs[INPUT_MASK(inp->hist.head++)] = save;
	}

	return input_reset(inp);
}

static int
tab_complete_nick(struct input *inp, struct user_list *ul, char *str, size_t len)
uint16_t
input_frame(struct input *inp, char *buf, uint16_t max)
{
	/* Nick tab completion */

	const char *p;
	/*  Keep the input head in view, reframing if the cursor would be
	 *  drawn outside [A, B] as a function of the given max width
	 *
	 * 0       W             W + M
	 * |-------|---------------|------
	 * |       |A             B|
	 *         |<--         -->| : max
	 *
	 * The cursor should track the input head, where the next
	 * character would be entered
	 *
	 * In the <= A case: deletions occurred since previous reframe;
	 * the head is less than or equal to the window
	 *
	 * In the >= B case: insertions occurred since previous reframe;
	 * the distance from window to head is greater than the distance
	 * from [A, B]
	 *
	 * Set the window 2/3 of the text area width backwards from the head
	 * and returns the cursor position relative to the window */

	struct user *u;
	if (inp->window >= inp->head || (inp->window + (max - 1)) <= inp->head)
		inp->window = (((max - 1) * 2 / 3) >= inp->head) ? 0 : inp->head - ((max - 1) * 2 / 3);

	if ((u = user_list_get(ul, str, len))) {
	input_write(inp, buf, max, inp->window);

		p = u->nick.str;
	return (inp->head - inp->window);
}

		/* Case insensitive matching, delete prefix */
		while (len--)
			delete_left(inp);
uint16_t
input_write(struct input *inp, char *buf, uint16_t max, uint16_t pos)
{
	uint16_t i = pos,
	         j = 0;

		while (*p && input_char(inp, *p++))
			;
	while (max > 1 && i < inp->head) {
		buf[j++] = inp->text[i++];
		max--;
	}

		/* Tab completing first word in input, append delimiter and space */
		if (str == inp->line->text) {
			input_char(inp, TAB_COMPLETE_DELIMITER);
			input_char(inp, ' ');
		}
	i = inp->tail;

		return 1;
	} else {
		return 0;
	while (max > 1 && i < INPUT_LEN_MAX) {
		buf[j++] = inp->text[i++];
		max--;
	}
}

/*
 * Input sending functions
 * */
	buf[j] = 0;

void
_send_input(struct input *in, char *buf)
	return j;
}

static char*
input_text_copy(struct input *inp)
{
	/* FIXME: refactoring WIP */
	char *str;
	size_t len;

	/* Before sending, copy the tail of the gap buffer back to the head */
	reset_line(in);
	if ((len = input_text_size(inp)) == 0)
		return NULL;

	/* Pass a copy of the message to the send handler, since it may modify the contents */
	strcpy(buf, in->line->text);
	if ((str = malloc(len + 1)) == NULL)
		fatal("malloc: %s", strerror(errno));

	/* Now check if the sent line was 'new' or was resent input scrollback
	 *
	 * If a new line was sent:
	 *   append a blank input as the new list head
	 *
	 * If scrollback was sent:
	 *   move it to the front of the input scrollback
	 */
	if (in->line == in->list_head) {

		if (in->count == SCROLLBACK_INPUT) {
			/* Reached maximum input scrollback lines, delete the tail */
			struct input_line *t = in->list_head->next;

			DLL_DEL(in->list_head, t);
			free(t);
		} else {
			in->count++;
		}

		new_list_head(in);
	} else {
		/* TODO: the DLL macros don't seem to handle this task well...
		 * Maybe they should be replaced with generic DLL functions in utils */
	input_write(inp, str, len + 1, 0);

		in->line->next->prev = in->line->prev;
		in->line->prev->next = in->line->next;
	return str;
}

		in->list_head->prev->next = in->line;
		in->line->prev = in->list_head->prev;
		in->list_head->prev = in->line;
		in->line->next = in->list_head;
static int
input_text_isfull(struct input *inp)
{
	return (input_text_size(inp) == INPUT_LEN_MAX);
}

		in->line = in->list_head;
	}
static int
input_text_iszero(struct input *inp)
{
	return (input_text_size(inp) == 0);
}

int
input_empty(struct input *in)
static uint16_t
input_text_size(struct input *inp)
{
	return (inp->head + (INPUT_LEN_MAX - inp->tail));
}

static uint16_t
input_hist_size(struct input *inp)
{
	/* FIXME: if cursor is 0 this is wrong */
	return (in->head == in->line->text);
	return (inp->hist.head - inp->hist.tail);
}

M src/components/input.h => src/components/input.h +58 -47
@@ 1,66 1,77 @@
#ifndef INPUT_H
#define INPUT_H

#include "user.h"
/* Buffer input
 *
 * Supports line editing, input history, word completion
 *
 * The working edit area is implemented as a fixed width
 * gap buffer for O(1) insertions, deletions and O(n)
 * cursor movements
 *
 * Input history is kept as a ring buffer of strings,
 * copied into the working area when scrolling
 */

#define SCROLLBACK_INPUT 15
#define BUFFSIZE 512
#define RIRC_MAX_INPUT 256 /* FIXME: MAX_INPUT conflicts with limits.h */
#include <stdint.h>

/* When tab completing a nick at the beginning of the line, append the following char */
#define TAB_COMPLETE_DELIMITER ':'
/* 410 max characters for input should be sufficient given
 * rfc2812 maximum length of 50 characters for channel names,
 * plus 50 characters for additional message formatting */
#ifndef INPUT_LEN_MAX
#define INPUT_LEN_MAX 410
#endif

/* Compile time checks */
#if BUFFSIZE < RIRC_MAX_INPUT
	/* Required so input lines can be safely strcpy'ed into a send buffer */
	#error BUFFSIZE must be greater than RIRC_MAX_INPUT
/* Number of history lines to keep for input. For proper
 * ring buffer masking this must be a power of 2 */
#ifndef INPUT_HIST_MAX
#define INPUT_HIST_MAX 16
#endif

/* Channel input line */
struct input_line
{
	char *end;
	char text[RIRC_MAX_INPUT + 1];
	struct input_line *next;
	struct input_line *prev;
};
/* Input completion callback type, returning length
 * of replacement word, or 0 if no match, with args: */
typedef uint16_t (*f_completion_cb)(
	char*,    /* word to replace */
	uint16_t, /* word length */
	uint16_t, /* word replacement max length */
	int);     /* word is start of input */

/* Channel input */
struct input
{
	char *head;
	char *tail;
	char *window;
	unsigned int count;
	struct input_line *line;
	struct input_line *list_head;
	char text[INPUT_LEN_MAX];
	struct {
		char *ptrs[INPUT_HIST_MAX];
		char *save;
		uint16_t current; /* Ring buffer current entry */
		uint16_t head;    /* Ring buffer head */
		uint16_t tail;    /* Ring buffer tail */
	} hist;
	uint16_t head;        /* Gap buffer head */
	uint16_t tail;        /* Gap buffer tail */
	uint16_t window;      /* Gap buffer frame window */
};

/* TODO: refactor */
struct input* new_input(void);
void action(int(*)(char), const char*, ...);
void free_input(struct input*);
extern char *action_message;

/* TODO: return state altering function */
int input(struct input*, const char*, size_t);

/* FIXME: */
void _send_input(struct input*, char*);

void input_init(struct input*);
void input_free(struct input*);

/* Input manipulation */
int input_cursor_back(struct input*);
int input_cursor_forw(struct input*);
int input_delete_back(struct input*);
int input_delete_forw(struct input*);
int input_insert(struct input*, const char*, size_t);
int input_reset(struct input*);

int input_empty(struct input*);
/* Input completion */
int input_complete(struct input*, f_completion_cb);

/* Input line manipulation functions */
// TODO: rename, input_*
int cursor_left(struct input*);
int cursor_right(struct input*);
int delete_left(struct input*);
int delete_right(struct input*);
int input_scroll_back(struct input*, unsigned int);
int input_scroll_forw(struct input*, unsigned int);
/* Input history */
int input_hist_back(struct input*);
int input_hist_forw(struct input*);
int input_hist_push(struct input*);

int tab_complete(struct input*, struct user_list*);
/* Write input to string */
uint16_t input_frame(struct input*, char*, uint16_t);
uint16_t input_write(struct input*, char*, uint16_t, uint16_t);

#endif

M src/components/mode.c => src/components/mode.c +5 -4
@@ 157,7 157,7 @@ mode_cfg(struct mode_cfg *cfg, const char *cfg_str, enum mode_cfg_t cfg_type)
			return mode_cfg_modes(cfg, cfg_str);

		default:
			fatal("mode configuration type unknown", 0);
			fatal("mode configuration type unknown: %d", cfg_type);
	}

	return MODE_ERR_NONE;


@@ 414,14 414,15 @@ mode_str(const struct mode *m, struct mode_str *m_str)
	switch (m_str->type) {
		case MODE_STR_CHANMODE:
			skip = "sp";
			break;
		case MODE_STR_USERMODE:
		case MODE_STR_PRFXMODE:
			break;
		case MODE_STR_UNSET:
			fatal("mode_str type not set", 0);
			fatal("mode_str type not set");
			break;
		default:
			fatal("mode_str type unknown", 0);
			fatal("mode_str type unknown");
	}

	for (c = 'a'; c <= 'z' && lower; c++, lower >>= 1)


@@ 443,7 444,7 @@ mode_reset(struct mode *m, struct mode_str *s)
	/* Set mode and mode_str to initial state */

	if (!m || !s)
		fatal("mode or mode_str is null", 0);
		fatal("mode or mode_str is null");

	enum mode_str_t type = s->type;


M src/components/server.c => src/components/server.c +31 -27
@@ 1,4 1,5 @@
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>



@@ 31,28 32,20 @@ server(const char *host, const char *port, const char *pass, const char *user, c
	struct server *s;

	if ((s = calloc(1, sizeof(*s))) == NULL)
		fatal("calloc", errno);
		fatal("calloc: %s", strerror(errno));

	s->host = strdup(host);
	s->port = strdup(port);
	s->pass = pass ? strdup(pass) : NULL;
	s->username = strdup(user);
	s->realname = strdup(real);

	s->channel = channel(host, CHANNEL_T_SERVER);
	s->mode_str.type = MODE_STR_USERMODE;

	mode_cfg(&(s->mode_cfg), NULL, MODE_CFG_DEFAULTS);

	// FIXME: channel()
	s->channel = new_channel(host, s, CHANNEL_T_SERVER);

	// move this to the state_new_server
	channel_set_current(s->channel);

	if ((s->connection = connection(s, host, port)) == NULL) {
		server_free(s);
		return NULL;
	}
	/* FIXME: remove server pointer from channel, remove
	 * server's channel from clist */
	s->channel->server = s;
	channel_list_add(&(s->clist), s->channel);

	return s;
}


@@ 134,8 127,25 @@ server_list_del(struct server_list *sl, struct server *s)
}

void
server_reset(struct server *s)
{
	mode_reset(&(s->usermodes), &(s->mode_str));
	s->ping = 0;
	s->quitting = 0;
	s->nicks.next = 0;
}

void
server_free(struct server *s)
{
	// FIXME: add this back when removing it from
	// server's channel_list
	// channel_free(s->channel);

	channel_list_free(&(s->clist));
	channel_list_free(&(s->ulist));
	user_list_free(&(s->ignore));

	free((void *)s->host);
	free((void *)s->port);
	free((void *)s->pass);


@@ 173,7 183,7 @@ server_set_004(struct server *s, char *str)

	if (user_modes) {

		DEBUG_MSG("Setting numeric 004 user_modes: %s", user_modes);
		debug("Setting numeric 004 user_modes: %s", user_modes);

		if (mode_cfg(&(s->mode_cfg), user_modes, MODE_CFG_USERMODES) != MODE_ERR_NONE)
			newlinef(c, 0, "-!!-", "invalid numeric 004 user_modes: %s", user_modes);


@@ 181,7 191,7 @@ server_set_004(struct server *s, char *str)

	if (chan_modes) {

		DEBUG_MSG("Setting numeric 004 chan_modes: %s", chan_modes);
		debug("Setting numeric 004 chan_modes: %s", chan_modes);

		if (mode_cfg(&(s->mode_cfg), chan_modes, MODE_CFG_CHANMODES) != MODE_ERR_NONE)
			newlinef(c, 0, "-!!-", "invalid numeric 004 chan_modes: %s", chan_modes);


@@ 235,7 245,7 @@ server_set_nicks(struct server *s, const char *nicks)
	s->nicks.base = base;

	if ((s->nicks.set = malloc(sizeof(*s->nicks.set) * n)) == NULL)
		fatal("malloc", errno);
		fatal("malloc: %s", strerror(errno));

	for (const char **set = s->nicks.set; n; n--, set++) {
		*set = base;


@@ 342,7 352,7 @@ server_set_CASEMAPPING(struct server *s, char *val)
static int
server_set_CHANMODES(struct server *s, char *val)
{
	DEBUG_MSG("Setting numeric 005 CHANMODES: %s", val);
	debug("Setting numeric 005 CHANMODES: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_SUBTYPES) != MODE_ERR_NONE);
}


@@ 350,7 360,7 @@ server_set_CHANMODES(struct server *s, char *val)
static int
server_set_MODES(struct server *s, char *val)
{
	DEBUG_MSG("Setting numeric 005 MODES: %s", val);
	debug("Setting numeric 005 MODES: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_MODES) != MODE_ERR_NONE);
}


@@ 358,7 368,7 @@ server_set_MODES(struct server *s, char *val)
static int
server_set_PREFIX(struct server *s, char *val)
{
	DEBUG_MSG("Setting numeric 005 PREFIX: %s", val);
	debug("Setting numeric 005 PREFIX: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_PREFIX) != MODE_ERR_NONE);
}


@@ 366,7 376,7 @@ server_set_PREFIX(struct server *s, char *val)
void
server_nick_set(struct server *s, const char *nick)
{
	DEBUG_MSG("Setting server nick: %s", nick);
	debug("Setting server nick: %s", nick);

	if (s->nick)
		free((void *)s->nick);


@@ 393,9 403,3 @@ server_nicks_next(struct server *s)
		server_nick_set(s, nick_rand);
	}
}

void
server_nicks_reset(struct server *s)
{
	s->nicks.next = 0;
}

M src/components/server.h => src/components/server.h +5 -4
@@ 4,7 4,6 @@
#include "src/components/buffer.h"
#include "src/components/channel.h"
#include "src/components/mode.h"
#include "src/io.h"

struct server
{


@@ 22,7 21,7 @@ struct server
	} nicks;
	struct channel *channel;
	struct channel_list clist;
	struct connection *connection;
	struct channel_list ulist; // TODO: seperate privmsg
	struct mode usermodes;
	struct mode_str mode_str;
	struct mode_cfg mode_cfg;


@@ 30,6 29,8 @@ struct server
	struct server *prev;
	struct user_list ignore;
	unsigned ping;
	unsigned quitting : 1;
	void *connection;
};

struct server_list


@@ 48,7 49,7 @@ struct server* server(

struct server* server_list_add(struct server_list*, struct server*);
struct server* server_list_del(struct server_list*, struct server*);
struct server* server_list_get(struct server_list*, const char *, const char*);
struct server* server_list_get(struct server_list*, const char*, const char*);

void server_set_004(struct server*, char*);
void server_set_005(struct server*, char*);


@@ 56,8 57,8 @@ int server_set_nicks(struct server*, const char*);

void server_nick_set(struct server*, const char*);
void server_nicks_next(struct server*);
void server_nicks_reset(struct server*);

void server_reset(struct server*);
void server_free(struct server*);

#endif

M src/components/user.c => src/components/user.c +9 -8
@@ 1,3 1,4 @@
#include <errno.h>
#include <stdlib.h>
#include <string.h>



@@ 17,14 18,14 @@ static inline int
user_cmp(struct user *u1, struct user *u2)
{
	/* TODO: CASEMAPPING */
	return irc_strcmp(u1->nick.str, u2->nick.str);
	return irc_strcmp(u1->nick, u2->nick);
}

static inline int
user_ncmp(struct user *u1, struct user *u2, size_t n)
{
	/* TODO: CASEMAPPING */
	return irc_strncmp(u1->nick.str, u2->nick.str, n);
	return irc_strncmp(u1->nick, u2->nick, n);
}

static inline void


@@ 41,10 42,10 @@ user(const char *nick)
	size_t len = strlen(nick);

	if ((u = calloc(1, sizeof(*u) + len + 1)) == NULL)
		fatal("calloc", errno);
		fatal("calloc: %s", strerror(errno));

	u->nick.len = len;
	u->nick.str = memcpy(u->_, nick, len + 1);
	u->nick_len = len;
	u->nick = memcpy(u->_, nick, len + 1);

	return u;
}


@@ 113,12 114,12 @@ user_list_rpl(struct user_list *ul, const char *nick_old, const char *nick_new)
struct user*
user_list_get(struct user_list *ul, const char *nick, size_t prefix_len)
{
	struct user u = { .nick = { .str = nick } };
	struct user u2 = { .nick = nick };

	if (prefix_len == 0)
		return AVL_GET(user_list, ul, &u);
		return AVL_GET(user_list, ul, &u2);
	else
		return AVL_NGET(user_list, ul, &u, prefix_len);
		return AVL_NGET(user_list, ul, &u2, prefix_len);
}

void

M src/components/user.h => src/components/user.h +2 -2
@@ 3,7 3,6 @@

#include "src/components/mode.h"
#include "src/utils/tree.h"
#include "src/utils/utils.h"

enum user_err
{


@@ 15,8 14,9 @@ enum user_err
struct user
{
	AVL_NODE(user) ul;
	size_t nick_len;
	struct mode prfxmodes;
	struct string nick;
	const char *nick;
	char _[];
};


M src/draw.c => src/draw.c +29 -83
@@ 19,6 19,7 @@

#include "config.h"
#include "src/components/input.h"
#include "src/components/channel.h"
#include "src/io.h"
#include "src/state.h"
#include "src/utils/utils.h"


@@ 49,9 50,9 @@
#define COLOUR_SIZE sizeof(RESET_ATTRIBUTES FG(255) BG(255))

#ifndef BUFFER_PADDING
	#define BUFFER_PADDING 1
#define BUFFER_PADDING 1
#elif BUFFER_PADDING != 0 && BUFFER_PADDING != 1
	#error "BUFFER_PADDING options are 0 (no pad), 1 (padded)"
#error "BUFFER_PADDING options are 0 (no pad), 1 (padded)"
#endif

static int actv_colours[ACTIVITY_T_SIZE] = ACTIVITY_COLOURS


@@ 116,7 117,7 @@ draw(union draw draw)

	if (draw.bits.nav)    _draw_nav(c);

	if (draw.bits.input)  _draw_input(c->input,
	if (draw.bits.input)  _draw_input(&c->input,
		(struct coords) {
			.c1 = 1,
			.cN = io_tty_cols(),


@@ 420,7 421,7 @@ _draw_nav(struct channel *c)
	size_t len, total_len = 0;

	/* Bump the channel frames, if applicable */
	if ((total_len = (c->name.len + 2)) >= io_tty_cols())
	if ((total_len = (c->name_len + 2)) >= io_tty_cols())
		return;
	else if (c == frame_prev && frame_prev != c_first)
		frame_prev = channel_get_prev(frame_prev);


@@ 437,14 438,14 @@ _draw_nav(struct channel *c)
			/* Pad out nextward */

			tmp = channel_get_next(tmp_next);
			len = tmp->name.len;
			len = tmp->name_len;

			while ((total_len += (len + 2)) < io_tty_cols() && tmp != c_first) {

				tmp_next = tmp;

				tmp = channel_get_next(tmp);
				len = tmp->name.len;
				len = tmp->name_len;
			}

			break;


@@ 455,21 456,21 @@ _draw_nav(struct channel *c)
			/* Pad out prevward */

			tmp = channel_get_prev(tmp_prev);
			len = tmp->name.len;
			len = tmp->name_len;

			while ((total_len += (len + 2)) < io_tty_cols() && tmp != c_last) {

				tmp_prev = tmp;

				tmp = channel_get_prev(tmp);
				len = tmp->name.len;
				len = tmp->name_len;
			}

			break;
		}

		tmp = nextward ? channel_get_next(tmp_next) : channel_get_prev(tmp_prev);
		len = tmp->name.len;
		len = tmp->name_len;

		/* Next channel doesn't fit */
		if ((total_len += (len + 2)) >= io_tty_cols())


@@ 494,7 495,7 @@ _draw_nav(struct channel *c)
		if (fputs(_colour(colour, -1), stdout) < 0)
			break;

		if (printf(" %s ", tmp->name.str) < 0)
		if (printf(" %s ", tmp->name) < 0)
			break;

		if (tmp == frame_next)


@@ 503,7 504,7 @@ _draw_nav(struct channel *c)
}

static void
_draw_input(struct input *in, struct coords coords)
_draw_input(struct input *inp, struct coords coords)
{
	/* Draw the input line, or the current action message */



@@ 554,59 555,7 @@ _draw_input(struct input *in, struct coords coords)
		cursor = cols_t - text_n + 1;

	} else {

		/*  Keep the input head in view, reframing if the cursor would be
		 *  drawn outside [A, B] as a function of input window and head
		 *
		 * |  <prefix>     <text area>  |
		 * |............|---------------|
		 * |             A             B|
		 * |                            | : cols_t
		 *              |               | : text_n
		 *
		 * The cursor should track the input head, where the next
		 * character would be entered
		 *
		 * In the <= A case: deletions occurred; the head is less than
		 * or equal to the window
		 *
		 * In the >= B case: insertions occurred; the distance from window
		 * to head is greater than the distance from [A, B]
		 *
		 * Set the window 2/3 of the text area width backwards from the head */

		size_t frame = text_n * 2 / 3;

		char *w1, *w2, *ptr;

		if (in->window >= in->head || (in->window + text_n) <= in->head) {

			w1 = in->line->text,
			w2 = in->line->text + frame;

			in->window = (w2 >= in->head) ? w1 : in->head - frame;
		}

		cursor = (cols_t - text_n) + (in->head - in->window + 1);

		for (ptr = in->window; text_n; text_n--) {

			/* Copy characters, x, from the gap buffer, i.e.:
			 *
			 *  window   head    tail
			 *       v      v       v
			 * |.....xxxxxxx|------|xxxxxxx| */

			if (ptr == in->head)
				ptr = in->tail;

			if (ptr == in->line->text + RIRC_MAX_INPUT)
				break;

			*input_ptr++ = *ptr++;
		}

		*input_ptr = '\0';
		cursor += input_frame(inp, input_ptr, text_n);
	}

print_input:


@@ 626,7 575,7 @@ _draw_status(struct channel *c)
	 *
	 * channel:
	 * |-[usermodes]-[chancount chantype chanmodes]/[priv]-(ping)---...|
	 * */
	 */

	float sb;
	int ret;


@@ 730,10 679,10 @@ check_coords(struct coords coords)
	/* Check coordinate validity before drawing, ensure at least one row, column */

	if (coords.r1 > coords.rN)
		fatal("row coordinates invalid", 0);
		fatal("row coordinates invalid (%u > %u)", coords.r1, coords.rN);

	if (coords.c1 > coords.cN)
		fatal("column coordinates invalid", 0);
		fatal("col coordinates invalid (%u > %u)", coords.c1, coords.cN);
}

static inline unsigned int


@@ 757,26 706,23 @@ _colour(int fg, int bg)
	 * Background(B): ESC"[48;5;Bm"
	 * */

	static char col_buff[COLOUR_SIZE + 1] = RESET_ATTRIBUTES;

	static char buf[COLOUR_SIZE + 1] = RESET_ATTRIBUTES;
	size_t len = sizeof(RESET_ATTRIBUTES) - 1;
	int ret = 0;

	char *col_buff_ptr = col_buff + sizeof(RESET_ATTRIBUTES) - 1;

	/* Assume any colour sequence begins by resetting all attributes */
	*(col_buff_ptr = col_buff + sizeof(RESET_ATTRIBUTES) - 1) = '\0';

	/* Set valid foreground colour */
	if (fg >= 0 && fg <= 255 && ((ret = sprintf(col_buff_ptr, ESC"[38;5;%dm", fg)) < 0))
		return strcpy(col_buff, RESET_ATTRIBUTES);

	col_buff_ptr += ret;
	if (fg >= 0 && fg <= 255) {
		if ((ret = snprintf(buf + len, sizeof(buf) - len, ESC"[38;5;%dm", fg)) < 0)
			buf[len] = 0;
		else
			len += ret;
	}

	/* Set valid background colour */
	if (bg >= 0 && bg <= 255 && ((ret = sprintf(col_buff_ptr, ESC"[48;5;%dm", bg)) < 0))
		return strcpy(col_buff, RESET_ATTRIBUTES);
	if (bg >= 0 && bg <= 255) {
		if ((ret = snprintf(buf + len, sizeof(buf) - len, ESC"[48;5;%dm", bg)) < 0)
			buf[len] = 0;
	}

	return col_buff;
	return buf;
}

static int

M src/io.c => src/io.c +116 -169
@@ 19,57 19,62 @@
#include "src/io.h"
#include "utils/utils.h"

#define IO_RECV_SIZE 2 << 12
#define IO_RECV_SIZE 4096

/* RFC 2812, section 2.3 */
#ifndef IO_MESG_LEN
	#define IO_MESG_LEN 510
#define IO_MESG_LEN 510
#endif

#ifndef IO_PING_MIN
	#define IO_PING_MIN 150
#define IO_PING_MIN 150
#elif (IO_PING_MIN < 0 || IO_PING_MIN > 86400)
	#error "IO_PING_MIN: [0, 86400]"
#error "IO_PING_MIN: [0, 86400]"
#endif

#ifndef IO_PING_REFRESH
	#define IO_PING_REFRESH 5
#define IO_PING_REFRESH 5
#elif (IO_PING_REFRESH < 0 || IO_PING_REFRESH > 86400)
	#error "IO_PING_REFRESH: [0, 86400]"
#error "IO_PING_REFRESH: [0, 86400]"
#endif

#ifndef IO_PING_MAX
	#define IO_PING_MAX 300
#define IO_PING_MAX 300
#elif (IO_PING_MAX < 0 || IO_PING_MAX > 86400)
	#error "IO_PING_MAX: [0, 86400]"
#error "IO_PING_MAX: [0, 86400]"
#endif

#ifndef IO_RECONNECT_BACKOFF_BASE
	#define IO_RECONNECT_BACKOFF_BASE 4
#define IO_RECONNECT_BACKOFF_BASE 4
#elif (IO_RECONNECT_BACKOFF_BASE < 1 || IO_RECONNECT_BACKOFF_BASE > 86400)
	#error "IO_RECONNECT_BACKOFF_BASE: [1, 32]"
#error "IO_RECONNECT_BACKOFF_BASE: [1, 32]"
#endif

#ifndef IO_RECONNECT_BACKOFF_FACTOR
	#define IO_RECONNECT_BACKOFF_FACTOR 2
#define IO_RECONNECT_BACKOFF_FACTOR 2
#elif (IO_RECONNECT_BACKOFF_FACTOR < 1 || IO_RECONNECT_BACKOFF_FACTOR > 32)
	#error "IO_RECONNECT_BACKOFF_FACTOR: [1, 32]"
#error "IO_RECONNECT_BACKOFF_FACTOR: [1, 32]"
#endif

#ifndef IO_RECONNECT_BACKOFF_MAX
	#define IO_RECONNECT_BACKOFF_MAX 86400
#define IO_RECONNECT_BACKOFF_MAX 86400
#elif (IO_RECONNECT_BACKOFF_MAX < 1 || IO_RECONNECT_BACKOFF_MAX > 86400)
	#error "IO_RECONNECT_BACKOFF_MAX: [0, 86400]"
#error "IO_RECONNECT_BACKOFF_MAX: [0, 86400]"
#endif

#define PT_CF(X) do { int ret; if ((ret = (X)) < 0) fatal((#X), ret); } while (0)
#if EAGAIN == EWOULDBLOCK
#define CHECK_BLOCK(X) ((X) == EAGAIN)
#else
#define CHECK_BLOCK(X) ((X) == EAGAIN || (X) == EWOULDBLOCK)
#endif

#define PT_CF(X) do { io_check_fatal((#X), (X)); } while (0)
#define PT_LK(X) PT_CF(pthread_mutex_lock((X)))
#define PT_UL(X) PT_CF(pthread_mutex_unlock((X)))
#define PT_CB(R, ...) \
	do { \
		PT_LK(&cb_mutex); \
		io_cb((R), __VA_ARGS__); \
		PT_UL(&cb_mutex); \
#define PT_CB(...) \
	do { PT_LK(&cb_mutex); \
	     io_cb(__VA_ARGS__); \
	     PT_UL(&cb_mutex); \
	} while (0)

enum io_err_t


@@ 80,13 85,12 @@ enum io_err_t
	IO_ERR_DXED,
	IO_ERR_FMT,
	IO_ERR_SEND,
	IO_ERR_TERM,
	IO_ERR_TRUNC,
};

struct io_lock
{
	pthread_cond_t  cnd;
	pthread_cond_t cnd;
	pthread_mutex_t mtx;
	volatile int predicate;
};


@@ 103,7 107,6 @@ struct connection
		IO_ST_CXNG, /* Socket connection in progress */
		IO_ST_CXED, /* Socket connected */
		IO_ST_PING, /* Socket connected, network state in question */
		IO_ST_TERM /* Terminal thread state */
	} st_c, /* current thread state */
	  st_f; /* forced thread state */
	char ip[INET6_ADDRSTRLEN];


@@ 116,6 119,7 @@ struct connection
	} read;
	struct io_lock lock;
	unsigned rx_backoff;
	pthread_t pt_tid;
};

static const char* io_strerror(struct connection*, int);


@@ 124,30 128,32 @@ static enum io_state_t io_state_cxng(struct connection*);
static enum io_state_t io_state_dxed(struct connection*);
static enum io_state_t io_state_ping(struct connection*);
static enum io_state_t io_state_rxng(struct connection*);
static void io_init(void);
static void io_init_sig(void);
static void io_init_tty(void);
static void io_lock_init(struct io_lock*);
static void io_lock_term(struct io_lock*);
static void io_check_fatal(const char*, int);
static void io_lock_wait(struct io_lock*, struct timespec*);
static void io_lock_wake(struct io_lock*);
static void io_net_set_timeout(struct connection*, unsigned);
static void io_recv(struct connection*, const char*, size_t);
static void io_sig_init(void);
static void io_soc_close(int*);
static void io_soc_shutdown(int);
static void io_state_force(struct connection*, enum io_state_t);
static void io_term(void);
static void io_term_tty(void);
static void io_tty_winsize(void);
static void io_tty_init(void);
static void io_tty_term(void);
static void io_tty_winsize(unsigned*, unsigned*);
static void* io_thread(void*);

static pthread_mutex_t cb_mutex;
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
static int io_running;
static pthread_mutex_t cb_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct termios term;
static struct winsize tty_ws;
static volatile sig_atomic_t flag_sigwinch_cb; /* sigwinch callback */
static volatile sig_atomic_t flag_tty_resized; /* sigwinch ws resize */

static void
io_check_fatal(const char *f, int ret)
{
	if (ret < 0)
		fatal("%s: %s", f, strerror(ret));
}

static const char*
io_strerror(struct connection *c, int errnum)
{


@@ 159,7 165,7 @@ static void
io_soc_close(int *soc)
{
	if (*soc >= 0 && close(*soc) < 0) {
		fatal("close", errno);
		fatal("close: %s", strerror(errno));
	}
	*soc = -1;
}


@@ 167,61 173,12 @@ io_soc_close(int *soc)
static void
io_soc_shutdown(int soc)
{
	if (soc >= 0 && shutdown(soc, SHUT_RDWR) < 0) {
		fatal("shutdown", errno);
	if (soc >= 0 && shutdown(soc, SHUT_RDWR) < 0 && errno != ENOTCONN) {
		fatal("shutdown: %s", strerror(errno));
	}
}

static void
io_init(void)
{
	pthread_mutexattr_t m_attr;

	PT_CF(pthread_mutexattr_init(&m_attr));
	PT_CF(pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_ERRORCHECK));
	PT_CF(pthread_mutex_init(&cb_mutex, &m_attr));
	PT_CF(pthread_mutexattr_destroy(&m_attr));

	if (atexit(io_term) != 0)
		fatal("atexit", 0);
}

static void
io_term(void)
{
	int ret;

	if ((ret = pthread_mutex_trylock(&cb_mutex)) < 0 && ret != EBUSY)
		fatal("pthread_mutex_trylock", ret);

	PT_UL(&cb_mutex);
	PT_CF(pthread_mutex_destroy(&cb_mutex));
}

static void
io_lock_init(struct io_lock *lock)
{
	pthread_mutexattr_t m_attr;

	PT_CF(pthread_mutexattr_init(&m_attr));
	PT_CF(pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_RECURSIVE));

	PT_CF(pthread_cond_init(&(lock->cnd), NULL));
	PT_CF(pthread_mutex_init(&(lock->mtx), &m_attr));

	PT_CF(pthread_mutexattr_destroy(&m_attr));

	lock->predicate = 0;
}

static void
io_lock_term(struct io_lock *lock)
{
	PT_CF(pthread_cond_destroy(&(lock->cnd)));
	PT_CF(pthread_mutex_destroy(&(lock->mtx)));
}

static void
io_lock_wait(struct io_lock *lock, struct timespec *timeout)
{
	PT_LK(&(lock->mtx));


@@ 237,46 194,29 @@ io_lock_wait(struct io_lock *lock, struct timespec *timeout)
	}

	if (ret && (timeout == NULL || ret != ETIMEDOUT))
		fatal("io_lock_wait", ret);
		fatal("io_lock_wait: %s", strerror(ret));

	lock->predicate = 0;

	PT_UL(&(lock->mtx));
}

static void
io_lock_wake(struct io_lock *lock)
{
	PT_LK(&(lock->mtx));
	lock->predicate = 1;
	PT_CF(pthread_cond_signal(&(lock->cnd)));
	PT_UL(&(lock->mtx));
}

struct connection*
connection(const void *obj, const char *host, const char *port)
{
	struct connection *c;

	if ((c = calloc(1U, sizeof(*c))) == NULL)
		fatal("malloc", errno);
		fatal("malloc: %s", strerror(errno));

	c->obj = obj;
	c->host = strdup(host);
	c->port = strdup(port);
	c->st_c = IO_ST_DXED;
	c->st_f = IO_ST_INVALID;
	io_lock_init(&(c->lock));

	PT_CF(pthread_once(&init_once, io_init));

	pthread_attr_t pt_attr;
	pthread_t pt_tid;

	PT_CF(pthread_attr_init(&pt_attr));
	PT_CF(pthread_attr_setdetachstate(&pt_attr, PTHREAD_CREATE_DETACHED));
	PT_CF(pthread_create(&pt_tid, &pt_attr, io_thread, c));
	PT_CF(pthread_attr_destroy(&pt_attr));
	PT_CF(pthread_cond_init(&(c->lock.cnd), NULL));
	PT_CF(pthread_mutex_init(&(c->lock.mtx), NULL));
	PT_CF(pthread_create(&c->pt_tid, NULL, io_thread, c));

	return c;
}


@@ 284,7 224,7 @@ connection(const void *obj, const char *host, const char *port)
int
io_sendf(struct connection *c, const char *fmt, ...)
{
	char sendbuf[512];
	char sendbuf[IO_MESG_LEN + 2];
	int ret;
	size_t len;
	va_list ap;


@@ 304,14 244,14 @@ io_sendf(struct connection *c, const char *fmt, ...)
	if (len >= sizeof(sendbuf) - 2)
		return IO_ERR_TRUNC;

	debug("send: (%zu) %s", len, sendbuf);

	sendbuf[len++] = '\r';
	sendbuf[len++] = '\n';

	if (send(c->soc, sendbuf, len, 0) < 0)
		return IO_ERR_SEND;

	DEBUG_MSG("send: (%zu) %s", len, sendbuf);

	return IO_ERR_NONE;
}



@@ 328,7 268,6 @@ io_cx(struct connection *c)
		case IO_ST_CXNG: err = IO_ERR_CXNG; break;
		case IO_ST_CXED: err = IO_ERR_CXED; break;
		case IO_ST_PING: err = IO_ERR_CXED; break;
		case IO_ST_TERM: err = IO_ERR_TERM; break;
		default:
			io_state_force(c, IO_ST_CXNG);
	}


@@ 349,7 288,6 @@ io_dx(struct connection *c)

	switch (c->st_c) {
		case IO_ST_DXED: err = IO_ERR_DXED; break;
		case IO_ST_TERM: err = IO_ERR_TERM; break;
		default:
			io_state_force(c, IO_ST_DXED);
	}


@@ 362,11 300,16 @@ io_dx(struct connection *c)
void
io_free(struct connection *c)
{
	/* Force a socket thread into the IO_ST_TERM state */
	pthread_t pt_tid = c->pt_tid;

	PT_LK(&(c->lock.mtx));
	io_state_force(c, IO_ST_TERM);
	PT_UL(&(c->lock.mtx));
	PT_CF(pthread_cancel(pt_tid));
	PT_CF(pthread_join(pt_tid, NULL));
	PT_CF(pthread_cond_destroy(&(c->lock.cnd)));
	PT_CF(pthread_mutex_destroy(&(c->lock.mtx)));
	io_soc_close(&(c->soc));
	free((void*)c->host);
	free((void*)c->port);
	free(c);
}

static void


@@ 379,17 322,16 @@ io_state_force(struct connection *c, enum io_state_t st_f)
	switch (c->st_c) {
		case IO_ST_DXED: /* io_lock_wait() */
		case IO_ST_RXNG: /* io_lock_wait() */
			io_lock_wake(&(c->lock));
			c->lock.predicate = 1;
			PT_CF(pthread_cond_signal(&(c->lock.cnd)));
			break;
		case IO_ST_CXNG: /* connect() */
		case IO_ST_CXED: /* recv() */
		case IO_ST_PING: /* recv() */
			io_soc_shutdown(c->soc);
			break;
		case IO_ST_TERM:
			break;
		default:
			fatal("Unknown net state", 0);
			fatal("Unknown net state: %d", c->st_c);
	}
}



@@ 420,7 362,7 @@ io_state_rxng(struct connection *c)
		(c->rx_backoff % 60));

	if (clock_gettime(CLOCK_REALTIME, &ts) < 0)
		fatal("clock_gettime", errno);
		fatal("clock_gettime: %s", strerror(errno));

	ts.tv_sec += c->rx_backoff;



@@ 436,6 378,7 @@ io_state_cxng(struct connection *c)
	/* TODO: mutex should protect access to c->soc, else race condition
	 *       when the main thread tries to shutdown() for cancel */
	/* TODO: how to cancel getaddrinfo/getnameinfo? */
	/* FIXME: addrinfo leak if canceled during connection */

	int ret, soc = -1;



@@ 500,16 443,16 @@ io_state_cxed(struct connection *c)
	while ((ret = recv(c->soc, c->read.tmp, sizeof(c->read.tmp), 0)) > 0)
		io_recv(c, c->read.tmp, (size_t) ret);

	if (errno == EAGAIN || errno == EWOULDBLOCK) {
	if (CHECK_BLOCK(errno)) {
		return IO_ST_PING;
	}

	if (ret == 0) {
		PT_CB(IO_CB_DXED, c->obj, "Connection closed");
		PT_CB(IO_CB_DXED, c->obj, "connection closed");
	} else if (errno == EPIPE || errno == ECONNRESET) {
		PT_CB(IO_CB_DXED, c->obj, "Connection closed by peer");
		PT_CB(IO_CB_DXED, c->obj, "connection closed by peer");
	} else {
		PT_CB(IO_CB_DXED, c->obj, "recv error:", io_strerror(c, errno));
		PT_CB(IO_CB_DXED, c->obj, "recv error: %s", io_strerror(c, errno));
	}

	io_soc_close(&(c->soc));


@@ 532,15 475,15 @@ io_state_ping(struct connection *c)
		}

		if (ret == 0) {
			PT_CB(IO_CB_DXED, c->obj, "Connection closed");
		} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
			PT_CB(IO_CB_DXED, c->obj, "connection closed");
		} else if (CHECK_BLOCK(errno)) {
			if ((ping += IO_PING_REFRESH) < IO_PING_MAX) {
				PT_CB(IO_CB_PING_N, c->obj, ping);
				continue;
			}
			PT_CB(IO_CB_DXED, c->obj, "Connection timeout (%u)", ping);
			PT_CB(IO_CB_DXED, c->obj, "connection timeout (%u)", ping);
		} else {
			PT_CB(IO_CB_DXED, c->obj, "recv error:", io_strerror(c, errno));
			PT_CB(IO_CB_DXED, c->obj, "recv error: %s", io_strerror(c, errno));
		}

		break;


@@ 574,7 517,7 @@ io_thread(void *arg)
			case IO_ST_CXED: st_fn = io_state_cxed; break;
			case IO_ST_PING: st_fn = io_state_ping; break;
			default:
				fatal("invalid state", 0);
				fatal("invalid state: %d", c->st_c);
		}

		st_f = c->st_c;


@@ 591,9 534,6 @@ io_thread(void *arg)

		PT_UL(&(c->lock.mtx));

		if (c->st_c == IO_ST_TERM)
			break;

		if (st_f == IO_ST_PING && st_t == IO_ST_CXED)
			PT_CB(IO_CB_PING_0, c->obj, 0);



@@ 607,14 547,6 @@ io_thread(void *arg)
			c->rx_backoff = 0;
	}

	io_soc_shutdown(c->soc);
	io_soc_close(&(c->soc));
	io_lock_term(&(c->lock));

	free((void*)c->host);
	free((void*)c->port);
	free(c);

	return NULL;
}



@@ 631,7 563,7 @@ io_recv(struct connection *c, const char *buf, size_t n)

			c->read.buf[ci] = 0;

			DEBUG_MSG("recv: (%zu) %s", ci, c->read.buf);
			debug("recv: (%zu) %s", ci, c->read.buf);

			PT_LK(&cb_mutex);
			io_cb_read_soc(c->read.buf, ci, c->obj);


@@ 655,7 587,7 @@ io_net_set_timeout(struct connection *c, unsigned timeout)
	};

	if (setsockopt(c->soc, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0)
		fatal("setsockopt", errno);
		fatal("setsockopt: %s", strerror(errno));
}

static void


@@ 668,7 600,7 @@ sigaction_sigwinch(int sig)
}

static void
io_init_sig(void)
io_sig_init(void)
{
	struct sigaction sa;



@@ 677,19 609,19 @@ io_init_sig(void)
	sigemptyset(&sa.sa_mask);

	if (sigaction(SIGWINCH, &sa, NULL) < 0)
		fatal("sigaction - SIGWINCH", errno);
		fatal("sigaction - SIGWINCH: %s", strerror(errno));
}

static void
io_init_tty(void)
io_tty_init(void)
{
	struct termios nterm;

	if (isatty(STDIN_FILENO) == 0)
		fatal("isatty", errno);
		fatal("isatty: %s", strerror(errno));

	if (tcgetattr(STDIN_FILENO, &term) < 0)
		fatal("tcgetattr", errno);
		fatal("tcgetattr: %s", strerror(errno));

	nterm = term;
	nterm.c_lflag &= ~(ECHO | ICANON | ISIG);


@@ 697,29 629,32 @@ io_init_tty(void)
	nterm.c_cc[VTIME] = 0;

	if (tcsetattr(STDIN_FILENO, TCSANOW, &nterm) < 0)
		fatal("tcsetattr", errno);
		fatal("tcsetattr: %s", strerror(errno));

	if (atexit(io_term_tty) < 0)
		fatal("atexit", 0);
	if (atexit(io_tty_term) != 0)
		fatal("atexit");
}

static void
io_term_tty(void)
io_tty_term(void)
{
	/* Exit handler, must return normally */

	if (tcsetattr(STDIN_FILENO, TCSADRAIN, &term) < 0)
		fatal("tcsetattr", errno);
		fatal_noexit("tcsetattr: %s", strerror(errno));
}

void
io_loop(void)
io_init(void)
{
	PT_CF(pthread_once(&init_once, io_init));
	io_sig_init();
	io_tty_init();

	io_init_sig();
	io_init_tty();
	io_running = 1;

	for (;;) {
		char buf[512];
	while (io_running) {

		char buf[128];
		ssize_t ret = read(STDIN_FILENO, buf, sizeof(buf));

		if (ret > 0) {


@@ 735,35 670,48 @@ io_loop(void)
					PT_CB(IO_CB_SIGNAL, NULL, IO_SIGWINCH);
				}
			} else {
				fatal("read", ret ? errno : 0);
				fatal("read: %s", ret ? strerror(errno) : "EOF");
			}
		}
	}
}

void
io_term(void)
{
	io_running = 0;
}

static void
io_tty_winsize(void)
io_tty_winsize(unsigned *rows, unsigned *cols)
{
	static struct winsize tty_ws;

	if (flag_tty_resized == 0) {
		flag_tty_resized = 1;

		if (ioctl(0, TIOCGWINSZ, &tty_ws) < 0)
			fatal("ioctl", errno);
			fatal("ioctl: %s", strerror(errno));
	}

	*rows = tty_ws.ws_row;
	*cols = tty_ws.ws_col;
}

unsigned
io_tty_cols(void)
{
	io_tty_winsize();
	return tty_ws.ws_col;
	unsigned rows, cols;
	io_tty_winsize(&rows, &cols);
	return cols;
}

unsigned
io_tty_rows(void)
{
	io_tty_winsize();
	return tty_ws.ws_row;
	unsigned rows, cols;
	io_tty_winsize(&rows, &cols);
	return rows;
}

const char*


@@ 776,7 724,6 @@ io_err(int err)
		case IO_ERR_DXED:  return "socket not connected";
		case IO_ERR_FMT:   return "failed to format message";
		case IO_ERR_SEND:  return "failed to send message";
		case IO_ERR_TERM:  return "thread is terminating";
		case IO_ERR_TRUNC: return "data truncated";
		default:
			return "unknown error";

M src/io.h => src/io.h +13 -12
@@ 15,16 15,16 @@
 *                            +--------+
 *                 +----(B)-- |  rxng  |
 *                 |          +--------+
 *  INIT           |           |      ^
 *    v            |         (A,C)    |
 *    |            |           |     (E)
 *                 |           |      ^
 *   INIT          |         (A,C)    |
 *    v            |           |     (E)
 *    |            v           v      |
 *    +--> +--------+ --(A)-> +--------+
 *         |  dxed  |         |  cxng  | <--+
 *    +--< +--------+ <-(B)-- +--------+    |
 *    |     ^      ^           |      ^    (F)
 *    v     |      |          (D)     |     |
 *  TERM    |      |           |     (F)    |
 *    |    +--------+ --(A)-> +--------+
 *    +--> |  dxed  |         |  cxng  | <--+
 *         +--------+ <-(B)-- +--------+    |
 *          ^      ^           |      ^    (F)
 *          |      |          (D)     |     |
 *          |      |           |     (F)    |
 *          |      |           v      |     |
 *          |      |          +--------+    |
 *          |      +----(B)-- |  cxed  |    |


@@ 69,7 69,7 @@
 *   t(n) = t(n - 1) * factor
 *   t(0) = base
 *
 * Calling io_loop starts the io context and never returns
 * Calling io_init starts the io context and doesn't return until io_term
 */

#define IO_MAX_CONNECTIONS 8


@@ 119,8 119,9 @@ void io_cb(enum io_cb_t, const void*, ...);
void io_cb_read_inp(char*, size_t);
void io_cb_read_soc(char*, size_t, const void*);

/* Start non-returning IO context */
void io_loop(void);
/* Start/stop IO context */
void io_init(void);
void io_term(void);

/* Get tty dimensions */
unsigned io_tty_cols(void);

M src/mesg.c => src/mesg.c +46 -35
@@ 261,7 261,9 @@ send_mesg(struct server *s, struct channel *chan, char *mesg)

	char *p, *cmd_str;

	if (*mesg == '/' && *(++mesg) != '/') {
	if (s == NULL) {
		newline(chan, 0, "-!!-", "This is not a server");
	} else if (*mesg == '/' && *(++mesg) != '/') {

		int ret;



@@ 277,7 279,7 @@ send_mesg(struct server *s, struct channel *chan, char *mesg)
		const struct send_handler* handler = send_handler_lookup(cmd_str, strlen(cmd_str));

		if (handler) {
			handler->func(mesg, chan->server, chan);
			handler->func(mesg, s, chan);
		} else if ((ret = io_sendf(s->connection, "%s %s", cmd_str, mesg)))
			newlinef(chan, 0, "-!!-", "sendf fail: %s", io_err(ret));



@@ 286,7 288,7 @@ send_mesg(struct server *s, struct channel *chan, char *mesg)
		int ret;

		if (*mesg == 0)
			fatal("message is empty", 0);
			fatal("message is empty");

		else if (chan->type != CHANNEL_T_CHANNEL && chan->type != CHANNEL_T_PRIVATE)
			newline(chan, 0, "-!!-", "Error: This is not a channel");


@@ 294,11 296,11 @@ send_mesg(struct server *s, struct channel *chan, char *mesg)
		else if (chan->parted)
			newline(chan, 0, "-!!-", "Error: Parted from channel");

		else if ((ret = io_sendf(s->connection, "PRIVMSG %s :%s", chan->name.str, mesg)))
		else if ((ret = io_sendf(s->connection, "PRIVMSG %s :%s", chan->name, mesg)))
			newlinef(chan, 0, "-!!-", "sendf fail: %s", io_err(ret));

		else
			newline(chan, BUFFER_LINE_CHAT, chan->server->nick, mesg);
			newline(chan, BUFFER_LINE_CHAT, s->nick, mesg);
	}
}



@@ 343,7 345,7 @@ send_me(char *mesg, struct server *s, struct channel *c)
	if (c->parted)
		fail(c, "Error: Parted from channel");

	if ((ret= io_sendf(s->connection, "PRIVMSG %s :\x01""ACTION %s\x01", c->name.str, mesg)))
	if ((ret= io_sendf(s->connection, "PRIVMSG %s :\x01""ACTION %s\x01", c->name, mesg)))
		failf(c, "sendf fail: %s", io_err(ret));

	newlinef(c, 0, "*", "%s %s", s->nick, mesg);


@@ 400,7 402,7 @@ send_join(char *mesg, struct server *s, struct channel *c)
		if (!c->parted)
			fail(c, "Error: Not parted from channel");

		if ((ret = io_sendf(s->connection, "JOIN %s", c->name.str)))
		if ((ret = io_sendf(s->connection, "JOIN %s", c->name)))
			failf(c, "sendf fail: %s", io_err(ret));
	}



@@ 456,7 458,7 @@ send_part(char *mesg, struct server *s, struct channel *c)
		if (c->parted)
			fail(c, "Error: Already parted from channel");

		if ((ret = io_sendf(s->connection, "PART %s :%s", c->name.str, DEFAULT_QUIT_MESG)))
		if ((ret = io_sendf(s->connection, "PART %s :%s", c->name, DEFAULT_QUIT_MESG)))
			failf(c, "sendf fail: %s", io_err(ret));
	}



@@ 481,8 483,10 @@ send_privmsg(char *mesg, struct server *s, struct channel *c)
	if ((ret = io_sendf(s->connection, "PRIVMSG %s :%s", targ, mesg)))
		failf(c, "sendf fail: %s", io_err(ret));

	if ((cc = channel_list_get(&s->clist, targ)) == NULL)
		cc = new_channel(targ, s, CHANNEL_T_PRIVATE);
	if ((cc = channel_list_get(&s->clist, targ)) == NULL) {
		cc = channel(targ, CHANNEL_T_PRIVATE);
		channel_list_add(&s->clist, cc);
	}

	newline(cc, BUFFER_LINE_CHAT, s->nick, mesg);



@@ 516,10 520,10 @@ send_topic(char *mesg, struct server *s, struct channel *c)
		mesg++;

	if (*mesg == '\0') {
		if ((ret = io_sendf(s->connection, "TOPIC %s", c->name.str)))
		if ((ret = io_sendf(s->connection, "TOPIC %s", c->name)))
			failf(c, "sendf fail: %s", io_err(ret));
	} else {
		if ((ret = io_sendf(s->connection, "TOPIC %s :%s", c->name.str, mesg)))
		if ((ret = io_sendf(s->connection, "TOPIC %s :%s", c->name, mesg)))
			failf(c, "sendf fail: %s", io_err(ret));
	}



@@ 559,6 563,8 @@ send_quit(char *mesg, struct server *s, struct channel *c)
	if (!s)
		fail(c, "Error: Not connected to server");

	s->quitting = 1;

	if ((ret = io_sendf(s->connection, "QUIT :%s", (*mesg ? mesg : DEFAULT_QUIT_MESG))))
		failf(c, "sendf fail: %s", io_err(ret));



@@ 744,8 750,10 @@ recv_ctcp_req(struct parsed_mesg *p, struct server *s)
		if (IS_ME(targ)) {
			/* Sending emote to private channel */

			if ((c = channel_list_get(&s->clist, p->from)) == NULL)
				c = new_channel(p->from, s, CHANNEL_T_PRIVATE);
			if ((c = channel_list_get(&s->clist, p->from)) == NULL) {
				c = channel(p->from, CHANNEL_T_PRIVATE);
				channel_list_add(&s->clist, c);
			}

			if (c != current_channel()) {
				c->activity = ACTIVITY_PINGED;


@@ 857,6 865,10 @@ recv_ctcp_rpl(struct parsed_mesg *p, struct server *s)
	if (!(cmd = getarg(&mesg, " ")))
		fail(s->channel, "CTCP: command is null");

	// FIXME: CTCP PING replies should come back with the same
	// <second> <millisecond> value that was sent out, and is
	// used to calculate the ping here

	newlinef(s->channel, 0, p->from, "CTCP %s reply: %s", cmd, mesg);

	return 0;


@@ 865,17 877,12 @@ recv_ctcp_rpl(struct parsed_mesg *p, struct server *s)
static int
recv_error(struct parsed_mesg *p, struct server *s)
{
	/* ERROR :<message> */

	struct channel *c = s->channel;

	newlinef(c, 0, "ERROR", "%s", (p->trailing ? p->trailing : "Remote hangup"));

	for (c = c->next; c != s->channel; c = c->next) {
		newlinef(c, 0, "-!!-", "(disconnected)");
	}
	/* ERROR :<message>
	 *
	 * Sent to clients before terminating their connection
	 */

	io_dx(s->connection);
	newlinef(s->channel, 0, (s->quitting ? "--" : "ERROR"), "%s", p->trailing);

	return 0;
}


@@ 895,12 902,14 @@ recv_join(struct parsed_mesg *p, struct server *s)
		fail(s->channel, "JOIN: channel is null");

	if (IS_ME(p->from)) {
		if ((c = channel_list_get(&s->clist, chan)) == NULL)
			channel_set_current(new_channel(chan, s, CHANNEL_T_CHANNEL));
		else {
		if ((c = channel_list_get(&s->clist, chan)) == NULL) {
			c = channel(chan, CHANNEL_T_CHANNEL);
			channel_list_add(&s->clist, c);
			channel_set_current(c);
		} else {
			c->parted = 0;
			newlinef(c, 0, ">", "Joined %s", chan);
		}
		newlinef(c, 0, ">", "Joined %s", chan);
		draw_all();
	} else {



@@ 908,7 917,7 @@ recv_join(struct parsed_mesg *p, struct server *s)
			failf(s->channel, "JOIN: channel '%s' not found", chan);

		if (user_list_add(&(c->users), p->from, MODE_EMPTY) == USER_ERR_DUPLICATE)
			failf(s->channel, "Error: user '%s' alread on channel '%s'", p->from, c->name.str);
			failf(s->channel, "Error: user '%s' alread on channel '%s'", p->from, c->name);

		if (c->users.count <= jpq_threshold)
			newlinef(c, 0, ">", "%s!%s has joined %s", p->from, p->host, chan);


@@ 1057,7 1066,7 @@ recv_mode_chanmodes(struct parsed_mesg *p, const struct mode_cfg *cfg, struct ch
						newlinef(c, 0, "--", "%s%s%s mode: %c%c",
								(p->from ? p->from : ""),
								(p->from ? " set " : ""),
								c->name.str,
								c->name,
								(mode_set == MODE_SET_ON ? '+' : '-'),
								flag);
					}


@@ 1077,7 1086,7 @@ recv_mode_chanmodes(struct parsed_mesg *p, const struct mode_cfg *cfg, struct ch
						newlinef(c, 0, "--", "%s%s%s mode: %c%c %s",
								(p->from ? p->from : ""),
								(p->from ? " set " : ""),
								c->name.str,
								c->name,
								(mode_set == MODE_SET_ON ? '+' : '-'),
								flag,
								modearg);


@@ 1229,7 1238,7 @@ recv_nick(struct parsed_mesg *p, struct server *s)
			newlinef(c, 0, "--", "%s  >>  %s", p->from, nick);

		else if (ret == USER_ERR_DUPLICATE)
			newlinef(c, 0, "-!!-", "Error: user '%s' alread on channel '%s'", p->from, c->name.str);
			newlinef(c, 0, "-!!-", "Error: user '%s' alread on channel '%s'", p->from, c->name);

	} while ((c = c->next) != s->channel);



@@ 1317,7 1326,7 @@ recv_numeric(struct parsed_mesg *p, struct server *s)
		do {
			//TODO: channel_list_foreach
			if (c->type == CHANNEL_T_CHANNEL && !c->parted) {
				if ((ret = io_sendf(s->connection, "JOIN %s", c->name.str)))
				if ((ret = io_sendf(s->connection, "JOIN %s", c->name)))
					failf(s->channel, "sendf fail: %s", io_err(ret));
			}
			c = c->next;


@@ 1681,8 1690,10 @@ recv_privmsg(struct parsed_mesg *p, struct server *s)
	/* Find the target channel */
	if (IS_ME(targ)) {

		if ((c = channel_list_get(&s->clist, p->from)) == NULL)
			c = new_channel(p->from, s, CHANNEL_T_PRIVATE);
		if ((c = channel_list_get(&s->clist, p->from)) == NULL) {
			c = channel(p->from, CHANNEL_T_PRIVATE);
			channel_list_add(&s->clist, c);
		}

		if (c != current_channel()) {
			c->activity = ACTIVITY_PINGED;

M src/rirc.c => src/rirc.c +49 -37
@@ 1,5 1,9 @@
#include <errno.h>
#include <getopt.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "config.h"


@@ 9,14 13,13 @@
#define arg_error(...) \
	do { fprintf(stderr, "%s: ", runtime_name); \
	     fprintf(stderr, __VA_ARGS__); \
	     fprintf(stderr, "\n"); \
	     fprintf(stderr, "%s --help for usage\n", runtime_name); \
	     exit(EXIT_FAILURE); \
	} while (0);
	     fprintf(stderr, "\n%s --help for usage\n", runtime_name); \
	     return 1; \
	} while (0)

static const char* opt_arg_str(char);
static const char* getpwuid_pw_name(void);
static void parse_args(int, char**);
static int parse_args(int, char**);

#ifndef DEBUG
const char *runtime_name = "rirc";


@@ 43,32 46,31 @@ const char *default_realname;
#endif

static const char *const rirc_usage =
"\nrirc v"VERSION" ~ Richard C. Robbins <mail@rcr.io>"
"\n"
"rirc v"VERSION" ~ Richard C. Robbins <mail@rcr.io>\n"
"\nUsage:"
"\n  rirc [-hv] [-s server [-p port] [-w pass] [-n nicks] [-c chans] [-u user] [-r real]], ...]"
"\n"
"Usage:\n"
"  rirc [-hv] [-s server [-p port] [-w pass] [-n nicks] [-c chans] [-u user] [-r real]], ...]\n"
"\nHelp:"
"\n  -h, --help            Print this message and exit"
"\n  -v, --version         Print rirc version and exit"
"\n"
"Help:\n"
"  -h, --help            Print this message and exit\n"
"  -v, --version         Print rirc version and exit\n"
"\n"
"Options:\n"
"  -s, --server=SERVER      Connect to SERVER\n"
"  -p, --port=PORT          Connect to SERVER using PORT\n"
"  -w, --pass=PASS          Connect to SERVER using PASS\n"
"  -u, --username=USERNAME  Connect to SERVER using USERNAME\n"
"  -r, --realname=REALNAME  Connect to SERVER using REALNAME\n"
"  -n, --nicks=NICKS        Comma separated list of nicks to use for SERVER\n"
"  -c, --chans=CHANNELS     Comma separated list of channels to join for SERVER\n"
;
"\nOptions:"
"\n  -s, --server=SERVER      Connect to SERVER"
"\n  -p, --port=PORT          Connect to SERVER using PORT"
"\n  -w, --pass=PASS          Connect to SERVER using PASS"
"\n  -u, --username=USERNAME  Connect to SERVER using USERNAME"
"\n  -r, --realname=REALNAME  Connect to SERVER using REALNAME"
"\n  -n, --nicks=NICKS        Comma separated list of nicks to use for SERVER"
"\n  -c, --chans=CHANNELS     Comma separated list of channels to join for SERVER"
"\n";

static const char *const rirc_version =
"rirc v"VERSION
#ifdef DEBUG
" (debug build)"
"rirc v"VERSION" (debug build)";
#else
"rirc v"VERSION;
#endif
;

static const char*
opt_arg_str(char c)


@@ 82,8 84,9 @@ opt_arg_str(char c)
		case 'u': return "-u/--username";
		case 'r': return "-r/--realname";
		default:
			fatal("unknown option flag", 0);
			fatal("unknown option flag '%c'", c);
	}
	return NULL;
}

static const char*


@@ 94,12 97,12 @@ getpwuid_pw_name(void)
	errno = 0;

	if (!passwd && !(passwd = getpwuid(geteuid())))
		fatal("getpwuid", (errno ? errno : ENOENT));
		fatal("getpwuid: %s", strerror((errno ? errno : ENOENT)));

	return passwd->pw_name;
}

static void
static int
parse_args(int argc, char **argv)
{
	int opt_c = 0,


@@ 122,8 125,8 @@ parse_args(int argc, char **argv)
		{"chans",    required_argument, 0, 'c'},
		{"username", required_argument, 0, 'u'},
		{"realname", required_argument, 0, 'r'},
		{"version",  no_argument,       0, 'v'},
		{"help",     no_argument,       0, 'h'},
		{"version",  no_argument,       0, 'v'},
		{0, 0, 0, 0}
	};



@@ 139,7 142,7 @@ parse_args(int argc, char **argv)
	} cli_servers[IO_MAX_CONNECTIONS];

	/* FIXME: getopt_long is a GNU extension */
	while (0 < (opt_c = getopt_long(argc, argv, ":s:p:w:n:c:r:u:vh", long_opts, &opt_i))) {
	while (0 < (opt_c = getopt_long(argc, argv, ":s:p:w:n:c:r:u:hv", long_opts, &opt_i))) {

		switch (opt_c) {



@@ 198,14 201,14 @@ parse_args(int argc, char **argv)

			#undef CHECK_SERVER_OPTARG

			case 'v':
				puts(rirc_version);
				exit(EXIT_SUCCESS);

			case 'h':
				puts(rirc_usage);
				exit(EXIT_SUCCESS);

			case 'v':
				puts(rirc_version);
				exit(EXIT_SUCCESS);

			case '?':
				arg_error("unknown options '%s'", argv[optind - 1]);



@@ 241,8 244,7 @@ parse_args(int argc, char **argv)
			(cli_servers[i].realname ? cli_servers[i].realname : default_realname)
		);

		if (s == NULL)
			arg_error("failed to create: %s:%s", cli_servers[i].host, cli_servers[i].port);
		s->connection = connection(s, cli_servers[i].host, cli_servers[i].port);

		if (server_list_add(state_server_list(), s))
			arg_error("duplicate server: %s:%s", cli_servers[i].host, cli_servers[i].port);


@@ 254,15 256,25 @@ parse_args(int argc, char **argv)
			arg_error("invalid nicks: '%s'", cli_servers[i].nicks);

		cli_servers[i].s = s;

		channel_set_current(s->channel);
	}

	for (size_t i = 0; i < n_servers; i++)
		io_cx(cli_servers[i].s->connection);

	return 0;
}

int
main(int argc, char **argv)
{
	parse_args(argc, argv);
	io_loop();
	int ret;

	if ((ret = parse_args(argc, argv)) == 0)
		io_init();

	state_term();

	return ret;
}

M src/state.c => src/state.c +262 -182
@@ 2,8 2,6 @@
 * state.c
 *
 * All manipulation of global program state
 *
 * TODO: moved keys, actions to this file. needs cleanup
 */

#include <ctype.h>


@@ 19,13 17,28 @@
#include "src/state.h"
#include "src/utils/utils.h"

// See: https://vt100.net/docs/vt100-ug/chapter3.html
#define CTRL(k) ((k) & 0x1f)

static void _newline(struct channel*, enum buffer_line_t, const char*, const char*, va_list);
static void state_io_cxed(struct server*);
static void state_io_dxed(struct server*, va_list);
static void state_io_ping(struct server*, unsigned int);
static void state_io_signal(enum io_sig_t);

static int state_input_linef(struct channel*);
static int state_input_ctrlch(const char*, size_t);
static int state_input_action(const char*, size_t);

static uint16_t state_complete(char*, uint16_t, uint16_t, int);
static uint16_t state_complete_list(char*, uint16_t, uint16_t, const char**);
static uint16_t state_complete_user(char*, uint16_t, uint16_t, int);

static struct
{
	struct channel *current_channel; /* the current channel being drawn */
	struct channel *default_channel; /* the default rirc channel at startup */

	struct server_list servers;

	union draw draw;
} state;



@@ 35,17 48,25 @@ state_server_list(void)
	return &state.servers;
}

static void term_state(void);

static void _newline(struct channel*, enum buffer_line_t, const char*, const char*, va_list);
struct channel*
current_channel(void)
{
	return state.current_channel;
}

struct channel* current_channel(void) { return state.current_channel; }
struct channel* default_channel(void) { return state.default_channel; }
/* List of IRC commands for tab completion */
static const char *irc_list[] = {
	"admin",   "connect", "info",     "invite", "join",
	"kick",    "kill",    "links",    "list",   "lusers",
	"mode",    "motd",    "names",    "nick",   "notice",
	"oper",    "part",    "pass",     "ping",   "pong",
	"privmsg", "quit",    "servlist", "squery", "stats",
	"time",    "topic",   "trace",    "user",   "version",
	"who",     "whois",   "whowas",   NULL };

static void state_io_cxed(struct server*);
static void state_io_dxed(struct server*, const char *);
static void state_io_ping(struct server*, unsigned int);
static void state_io_signal(enum io_sig_t);
/* List of rirc commands for tab completeion */
static const char *cmd_list[] = {
	"clear", "close", "connect", "quit", "set", NULL};

/* Set draw bits */
#define X(BIT) void draw_##BIT(void) { state.draw.bits.BIT = 1; }


@@ 69,11 90,7 @@ redraw(void)
void
state_init(void)
{
	/* atexit doesn't set errno */
	if (atexit(term_state) != 0)
		fatal("atexit", 0);

	state.default_channel = state.current_channel = new_channel("rirc", NULL, CHANNEL_T_OTHER);
	state.default_channel = state.current_channel = channel("rirc", CHANNEL_T_OTHER);

	/* Splashscreen */
	newline(state.default_channel, 0, "--", "      _");


@@ 93,22 110,31 @@ state_init(void)
	redraw();
}

static void
term_state(void)
void
state_term(void)
{
	/* Exit handler; must return normally */

	struct server *s1, *s2;

	channel_free(state.default_channel);

	/* Reset terminal colours */
	printf("\x1b[38;0;m");
	printf("\x1b[48;0;m");

#ifndef DEBUG
	/* Clear screen */
	if (!fatal_exit) {
		printf("\x1b[H\x1b[J");
		channel_free(state.default_channel);
	}
#endif
	printf("\x1b[H\x1b[J");

	if ((s1 = state_server_list()->head) == NULL)
		return;

	do {
		s2 = s1;
		s1 = s2->next;
		io_free(s2->connection);
		server_free(s2);
	} while (s1 != state_server_list()->head);
}

void


@@ 136,33 162,42 @@ _newline(struct channel *c, enum buffer_line_t type, const char *from, const cha
{
	/* Static function for handling inserting new lines into buffers */

	char buf[BUFFSIZE];
	char buf[TEXT_LENGTH_MAX];

	int len;

	struct string _from,
	              _text;
	const char *_from_str;
	const char *_text_str;
	size_t _from_len;
	size_t _text_len;

	struct user *u = NULL;

	if ((len = vsnprintf(buf, BUFFSIZE, fmt, ap)) < 0) {
		_text.str = "newlinef error: vsprintf failure";
		_text.len = strlen(_text.str);
		_from.str = "-!!-";
		_from.len = strlen(_from.str);
	if ((len = vsnprintf(buf, sizeof(buf), fmt, ap)) < 0) {
		_text_str = "newlinef error: vsprintf failure";
		_text_len = strlen(_text_str);
		_from_str = "-!!-";
		_from_len = strlen(_from_str);
	} else {
		_text.str = buf;
		_text.len = len;
		_from.str = from;
		_text_str = buf;
		_text_len = len;
		_from_str = from;

		// FIXME: don't need to get user for many non-user message types
		if ((u = user_list_get(&(c->users), from, 0)) != NULL)
			_from.len = u->nick.len;
			_from_len = u->nick_len;
		else
			_from.len = strlen(from);
			_from_len = strlen(from);
	}

	buffer_newline(&(c->buffer), type, _from, _text, ((u == NULL) ? 0 : u->prfxmodes.prefix));
	buffer_newline(
		&(c->buffer),
		type,
		_from_str,
		_text_str,
		_from_len,
		_text_len,
		((u == NULL) ? 0 : u->prfxmodes.prefix));

	c->activity = MAX(c->activity, ACTIVITY_ACTIVE);



@@ 172,23 207,6 @@ _newline(struct channel *c, enum buffer_line_t type, const char *from, const cha
		draw_nav();
}

struct channel*
new_channel(const char *name, struct server *s, enum channel_t type)
{
	struct channel *c = channel(name, type);

	/* TODO: deprecated, move to channel.c */

	if (s) {
		c->server = s;
		channel_list_add(&s->clist, c);
	}

	draw_all();

	return c;
}

int
state_server_set_chans(struct server *s, const char *chans)
{


@@ 213,7 231,8 @@ state_server_set_chans(struct server *s, const char *chans)
	} while (p2);

	for (const char *chan = base; n; n--) {
		new_channel(chan, s, CHANNEL_T_CHANNEL);
		struct channel *c = channel(chan, CHANNEL_T_CHANNEL);
		channel_list_add(&s->clist, c);
		chan = strchr(chan, 0) + 1;
	}



@@ 240,7 259,6 @@ channel_clear(struct channel *c)
#define MAX_ACTION_MESG 256
char *action_message;
static int action_close_server(char);
static int input_action(const char*, size_t);
/* Action handling */
static int (*action_handler)(char);
static char action_buff[MAX_ACTION_MESG];


@@ 251,8 269,8 @@ static int action_find_channel(char);
 * It can be cleaned up, and input.c is probably not the most ideal place for this */
#define MAX_SEARCH 128
struct channel *search_cptr; /* Used for iterative searching, before setting the current channel */
static char search_buff[MAX_SEARCH];
static char *search_ptr = search_buff;
static char search_buf[MAX_SEARCH + 1];
static size_t search_i;

static struct channel* search_channels(struct channel*, char*);
static struct channel*


@@ 266,7 284,7 @@ search_channels(struct channel *start, char *search)

	while (c != start) {

		if (strstr(c->name.str, search))
		if (strstr(c->name, search))
			return c;

		c = channel_get_next(c);


@@ 275,12 293,12 @@ search_channels(struct channel *start, char *search)
	return NULL;
}
static int
input_action(const char *input, size_t len)
state_input_action(const char *input, size_t len)
{
	/* Waiting for user confirmation */

	if (len == 1 && (*input == 0x03 || action_handler(*input))) {
		/* ^C canceled the action, or the action was resolved */
	if (len == 1 && (*input == CTRL('c') || action_handler(*input))) {
		/* ^c canceled the action, or the action was resolved */

		action_message = NULL;
		action_handler = NULL;


@@ 308,9 326,13 @@ action_close_server(char c)
		if ((state.current_channel = c->server->next->channel) == c->server->channel)
			state.current_channel = state.default_channel;

		if ((ret = io_sendf(s->connection, "QUIT %s", DEFAULT_QUIT_MESG)))
		if ((ret = io_sendf(s->connection, "QUIT :%s", DEFAULT_QUIT_MESG)))
			newlinef(s->channel, 0, "-!!-", "sendf fail: %s", io_err(ret));

		// FIXME:
		// - state_server_free shouldn't have to call io/dx/free.
		// - server objects can have void* connections and remove
		//   the dependancy on io.h
		io_dx(s->connection);
		server_list_del(state_server_list(), s);
		io_free(s->connection);


@@ 332,16 354,20 @@ action(int (*a_handler)(char), const char *fmt, ...)
	 * expected to return a non-zero value when the action is resolved
	 * */

	int len;
	va_list ap;

	va_start(ap, fmt);
	vsnprintf(action_buff, MAX_ACTION_MESG, fmt, ap);
	len = vsnprintf(action_buff, MAX_ACTION_MESG, fmt, ap);
	va_end(ap);

	action_handler = a_handler;
	action_message = action_buff;

	draw_input();
	if (len < 0) {
		debug("vsnprintf failed");
	} else {
		action_handler = a_handler;
		action_message = action_buff;
		draw_input();
	}
}
/* Action line should be:
 *


@@ 352,65 378,61 @@ action_find_channel(char c)
{
	/* Incremental channel search */

	/* \n confirms selecting the current match */
	if (c == '\n' && search_cptr) {
		*(search_ptr = search_buff) = '\0';
		channel_set_current(search_cptr);
		search_cptr = NULL;
		draw_all();
		return 1;
	}

	/* \n, Esc, ^C cancels a search if no results are found */
	if (c == '\n' || c == 0x1b || c == 0x03) {
		*(search_ptr = search_buff) = '\0';
	if (c == '\n' || c == 0x1b || c == CTRL('c')) {

		/* Confirm non-empty match */
		if (c == '\n' && search_cptr)
			channel_set_current(search_cptr);

		search_buf[0] = 0;
		search_i = 0;
		search_cptr = NULL;
		return 1;
	}

	/* ^F repeats the search forward from the current result,
	 * or resets search criteria if no match */
	if (c == 0x06) {
	if (c == CTRL('f')) {
		if (search_cptr == NULL) {
			*(search_ptr = search_buff) = '\0';
			search_buf[0] = 0;
			search_i = 0;
			action(action_find_channel, "Find: ");
			return 0;
		}

		search_cptr = search_channels(search_cptr, search_buff);
	} else if (c == 0x7f && search_ptr > search_buff) {
		search_cptr = search_channels(search_cptr, search_buf);
	} else if (c == 0x7f && search_i) {
		/* Backspace */
		search_buf[--search_i] = 0;
		search_cptr = search_channels(current_channel(), search_buf);

		*(--search_ptr) = '\0';

		search_cptr = search_channels(current_channel(), search_buff);
	} else if (isprint(c) && search_ptr < search_buff + MAX_SEARCH && (search_cptr != NULL || *search_buff == '\0')) {
	} else if (isprint(c) && search_i < MAX_SEARCH) {
		/* All other input */

		*(search_ptr++) = c;
		*search_ptr = '\0';

		search_cptr = search_channels(current_channel(), search_buff);
		search_buf[search_i++] = c;
		search_buf[search_i] = 0;
		search_cptr = search_channels(current_channel(), search_buf);
	}

	/* Reprint the action message */
	if (search_cptr == NULL) {
		if (*search_buff)
			action(action_find_channel, "Find: NO MATCH -- %s", search_buff);
		if (*search_buf)
			action(action_find_channel, "Find: NO MATCH -- %s", search_buf);
		else
			action(action_find_channel, "Find: ");
	} else {
		/* Found a channel */
		if (search_cptr->server == current_channel()->server) {
			action(action_find_channel, "Find: %s -- %s",
					search_cptr->name.str, search_buff);
					search_cptr->name, search_buf);
		} else {
			if (!strcmp(search_cptr->server->port, "6667"))
				action(action_find_channel, "Find: %s/%s -- %s",
						search_cptr->server->host, search_cptr->name.str, search_buff);
						search_cptr->server->host, search_cptr->name, search_buf);
			else
				action(action_find_channel, "Find: %s:%s/%s -- %s",
						search_cptr->server->host, search_cptr->server->port,
						search_cptr->name.str, search_buff);
						search_cptr->name, search_buf);
		}
	}



@@ 443,10 465,9 @@ channel_close(struct channel *c)
			action(action_close_server, "Close server '%s'?   [y/n]", c->server->host);
	} else {
		/* Closing a channel */

		if (c->type == CHANNEL_T_CHANNEL && !c->parted) {
			int ret;
			if (0 != (ret = io_sendf(c->server->connection, "PART %s", c->name.str))) {
			if (0 != (ret = io_sendf(c->server->connection, "PART %s", c->name))) {
				// FIXME: closing a parted channel when server is disconnected isnt an error
				newlinef(c->server->channel, 0, "sendf fail", "%s", io_err(ret));
			}


@@ 553,8 574,12 @@ buffer_scrollback_forw(struct channel *c)
	draw_status();
}

/* Usefull server/channel structure abstractions for drawing */

/* FIXME:
 *  - These abstractions should take into account the new component hierarchy
 *    and have the backwards pointer from channel to server removed, in favour
 *    of passing a current_server() to handlers
 *  - The server's channel should not be part of the server's channel_list
 */
struct channel*
channel_get_first(void)
{


@@ 626,22 651,64 @@ channel_move_next(void)
	}
}

static uint16_t
state_complete_list(char *str, uint16_t len, uint16_t max, const char **list)
{
	size_t list_len = 0;

	if (len == 0)
		return 0;

	while (*list && strncmp(*list, str, len))
		list++;

	if (*list == NULL || (list_len = strlen(*list)) > max)
		return 0;

	memcpy(str, *list, list_len);

	return list_len + 1;
}

static uint16_t
state_complete_user(char *str, uint16_t len, uint16_t max, int first)
{
	struct user *u;

	if ((u = user_list_get(&(current_channel()->users), str, len)) == NULL)
		return 0;

	if ((u->nick_len + (first != 0)) > max)
		return 0;

	memcpy(str, u->nick, u->nick_len);

	if (first)
		str[u->nick_len] = ':';

	return u->nick_len + (first != 0);
}

static uint16_t
state_complete(char *str, uint16_t len, uint16_t max, int first)
{
	if (first && str[0] == '/')
		return state_complete_list(str + 1, len - 1, max - 1, irc_list);

	if (first && str[0] == ':')
		return state_complete_list(str + 1, len - 1, max - 1, cmd_list);

	return state_complete_user(str, len, max, first);
}

static void
state_io_cxed(struct server *s)
{
	int ret;

	server_nicks_reset(s);
	server_reset(s);
	server_nicks_next(s);

	s->ping = 0;
	draw_status();

	if (s->pass && (ret = io_sendf(s->connection, "PASS %s", s->pass)))
		newlinef(s->channel, 0, "-!!-", "sendf fail: %s", io_err(ret));



@@ 650,15 717,23 @@ state_io_cxed(struct server *s)

	if ((ret = io_sendf(s->connection, "USER %s 8 * :%s", s->username, s->realname)))
		newlinef(s->channel, 0, "-!!-", "sendf fail: %s", io_err(ret));

	draw_status();
}

static void
state_io_dxed(struct server *s, const char *reason)
state_io_dxed(struct server *s, va_list ap)
{
	for (struct channel *c = s->channel->next; c != s->channel; c = c->next) {
		newlinef(c, 0, "-!!-", "(disconnected %s)", reason);
	struct channel *c = s->channel;
	va_list ap_copy;

	do {
		va_copy(ap_copy, ap);
		_newline(c, 0, "-!!-", va_arg(ap_copy, const char *), ap_copy);
		va_end(ap_copy);
		channel_reset(c);
	}
		c = c->next;
	} while (c != s->channel);
}

static void


@@ 670,7 745,7 @@ state_io_ping(struct server *s, unsigned int ping)

	if (ping != IO_PING_MIN)
		draw_status();
	else if ((ret = io_sendf(s->connection, "PING")))
	else if ((ret = io_sendf(s->connection, "PING :%s", s->host)))
		newlinef(s->channel, 0, "-!!-", "sendf fail: %s", io_err(ret));
}



@@ 700,7 775,7 @@ io_cb(enum io_cb_t type, const void *cb_obj, ...)
			_newline(s->channel, 0, "--", va_arg(ap, const char *), ap);
			break;
		case IO_CB_DXED:
			state_io_dxed(s, va_arg(ap, const char*));
			state_io_dxed(s, ap);
			break;
		case IO_CB_PING_0:
		case IO_CB_PING_1:


@@ 737,8 812,7 @@ send_cmnd(struct channel *c, char *buf)
	}

	if (!strcasecmp(cmnd, "quit")) {
		/* TODO: send optional quit message, close servers, free */
		exit(EXIT_SUCCESS);
		io_term();
	}

	if (!strcasecmp(cmnd, "connect")) {


@@ 767,10 841,13 @@ send_cmnd(struct channel *c, char *buf)
				channel_set_current(s->channel);
				newlinef(s->channel, 0, "-!!-", "already connected to %s:%s", host, port);
			} else {
				if ((s = server(host, port, pass, user, real)) == NULL)
					fatal("failed to create server", 0);

				s = server(host, port, pass, user, real);
				s->connection = connection(s, host, port);

				// TODO: here just get/set the connection object
				server_list_add(state_server_list(), s);

				channel_set_current(s->channel);
				io_cx(s->connection);
				draw_all();


@@ 800,12 877,14 @@ send_cmnd(struct channel *c, char *buf)
	 * unignore
	 * version
	 * find
	 * buffers
	 * b#
	 * b<num>
	 */
}

static int input_cchar(const char*, size_t);
static int
input_cchar(const char *c, size_t count)
state_input_ctrlch(const char *c, size_t len)
{
	/* Input a control character or escape sequence */



@@ 814,95 893,88 @@ input_cchar(const char *c, size_t count)

		c++;

		if (*c == 0)
		if (len == 1)
			return 0;

		/* arrow up */
		else if (!strncmp(c, "[A", count - 1))
			return input_scroll_back(current_channel()->input, io_tty_cols());
		else if (!strncmp(c, "[A", len - 1))
			return input_hist_back(&(current_channel()->input));

		/* arrow down */
		else if (!strncmp(c, "[B", count - 1))
			return input_scroll_forw(current_channel()->input, io_tty_cols());
		else if (!strncmp(c, "[B", len - 1))
			return input_hist_forw(&(current_channel()->input));

		/* arrow right */
		else if (!strncmp(c, "[C", count - 1))
			return cursor_right(current_channel()->input);
		else if (!strncmp(c, "[C", len - 1))
			return input_cursor_forw(&(current_channel()->input));

		/* arrow left */
		else if (!strncmp(c, "[D", count - 1))
			return cursor_left(current_channel()->input);
		else if (!strncmp(c, "[D", len - 1))
			return input_cursor_back(&(current_channel()->input));

		/* delete */
		else if (!strncmp(c, "[3~", count - 1))
			return delete_right(current_channel()->input);
		else if (!strncmp(c, "[3~", len - 1))
			return input_delete_forw(&(current_channel()->input));

		/* page up */
		else if (!strncmp(c, "[5~", count - 1))
		else if (!strncmp(c, "[5~", len - 1))
			buffer_scrollback_back(current_channel());

		/* page down */
		else if (!strncmp(c, "[6~", count - 1))
		else if (!strncmp(c, "[6~", len - 1))
			buffer_scrollback_forw(current_channel());

	} else switch (*c) {

		/* Backspace */
		case 0x7F:
			return delete_left(current_channel()->input);
			return input_delete_back(&(current_channel()->input));

		/* Horizontal tab */
		case 0x09:
			return tab_complete(current_channel()->input, &(current_channel()->users));
			return input_complete(&(current_channel()->input), state_complete);

		/* Line feed */
		case 0x0A:
			return state_input_linef(current_channel());

		/* ^C */
		case 0x03:
		case CTRL('c'):
			/* Cancel current input */
			current_channel()->input->head = current_channel()->input->line->text;
			current_channel()->input->tail = current_channel()->input->line->text + RIRC_MAX_INPUT;
			current_channel()->input->window = current_channel()->input->line->text;
			return 1;
			return input_reset(&(current_channel()->input));

		/* ^F */
		case 0x06:
		case CTRL('f'):
			/* Find channel */
			if (current_channel()->server)
				 action(action_find_channel, "Find: ");
			break;

		/* ^L */
		case 0x0C:
		case CTRL('l'):
			/* Clear current channel */
			/* TODO: as action with confirmation */
			channel_clear(current_channel());
			break;

		/* ^P */
		case 0x10:
		case CTRL('p'):
			/* Go to previous channel */
			channel_move_prev();
			break;

		/* ^N */
		case 0x0E:
		case CTRL('n'):
			/* Go to next channel */
			channel_move_next();
			break;

		/* ^X */
		case 0x18:
		case CTRL('x'):
			/* Close current channel */
			channel_close(current_channel());
			break;

		/* ^U */
		case 0x15:
		case CTRL('u'):
			/* Scoll buffer up */
			buffer_scrollback_back(current_channel());
			break;

		/* ^D */
		case 0x04:
		case CTRL('d'):
			/* Scoll buffer down */
			buffer_scrollback_forw(current_channel());
			break;


@@ 911,38 983,46 @@ input_cchar(const char *c, size_t count)
	return 0;
}

void
io_cb_read_inp(char *buff, size_t count)
static int
state_input_linef(struct channel *c)
{
	/* TODO: cleanup, switch on input type/contents */
	// input types:
	//    message/paste
	//    ::message
	//    :command
	//    //message
	//    /command

	int redraw_input = 0;
	char buf[INPUT_LEN_MAX + 1];
	size_t len;

	if (action_message) {
		redraw_input = input_action(buff, count);
	} else if (*buff == 0x0A) {
		/* Line feed */
		char sendbuf[BUFFSIZE];
	if ((len = input_write(&(c->input), buf, sizeof(buf), 0)) == 0)
		return 0;

		if (input_empty(state.current_channel->input))
			return;
	if (buf[0] == ':')
		send_cmnd(current_channel(), buf + 1);
	else
		send_mesg(current_channel()->server, current_channel(), buf);

		_send_input(state.current_channel->input, sendbuf);
	input_hist_push(&(c->input));

		switch (sendbuf[0]) {
			case ':':
				send_cmnd(state.current_channel, sendbuf + 1);
				break;
			default:
				send_mesg(current_channel()->server, current_channel(), sendbuf);
		}
		redraw_input = 1;
	return 1;
}

	} else if (iscntrl(*buff)) {
		redraw_input = input_cchar(buff, count);
	} else {
		redraw_input = input(current_channel()->input, buff, count);
	}
void
io_cb_read_inp(char *buf, size_t len)
{
	int redraw_input = 0;

	if (len == 0)
		fatal("zero length message");
	else if (action_message)
		redraw_input = state_input_action(buf, len);
	else if (iscntrl(*buf))
		redraw_input = state_input_ctrlch(buf, len);
	else
		redraw_input = input_insert(&current_channel()->input, buf, len);

	if (redraw_input)
		draw_input();


@@ 951,16 1031,16 @@ io_cb_read_inp(char *buff, size_t count)
}

void
io_cb_read_soc(char *buff, size_t count, const void *cb_obj)
io_cb_read_soc(char *buf, size_t len, const void *cb_obj)
{
	/* TODO: */
	(void)(count);
	(void)(len);

	struct channel *c = ((struct server *)cb_obj)->channel;

	struct parsed_mesg p;

	if (!(parse_mesg(&p, buff)))
	if (!(parse_mesg(&p, buf)))
		newlinef(c, 0, "-!!-", "failed to parse message");
	else
		recv_mesg((struct server *)cb_obj, &p);

M src/state.h => src/state.h +3 -1
@@ 13,7 13,6 @@
 * Interface for retrieving and altering global state of the program */

int state_server_set_chans(struct server*, const char*);
struct channel* new_channel(const char*, struct server*, enum channel_t);


/* state.c */


@@ 22,6 21,7 @@ struct channel* current_channel(void);
struct server_list* state_server_list(void);

void state_init(void);
void state_term(void);

// TODO: most of this stuff can be static
//TODO: move to channel.c, function of server's channel list


@@ 55,4 55,6 @@ void draw_all(void);

void redraw(void);

extern char *action_message;

#endif

M src/utils/utils.c => src/utils/utils.c +5 -45
@@ 1,3 1,4 @@
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>


@@ 35,31 36,6 @@

static inline int irc_toupper(int);

int fatal_exit;

void
handle_error(int errnum, const char *fmt, ...)
{
	/* Report an error and exit the program */

	va_list ap;

	fflush(stdout);

	fputs("\n\nrirc: ", stderr);

	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	va_end(ap);

	if (errnum)
		fprintf(stderr, " (errno: %s)\n", strerror(errnum));
	else
		fprintf(stderr, "\n");

	fatal_exit = 1;
}

char*
getarg(char **str, const char *sep)
{


@@ 110,27 86,11 @@ memdup(const void *mem, size_t len)
	void *ret;

	if ((ret = malloc(len)) == NULL)
		fatal("malloc", errno);
		fatal("malloc: %s", strerror(errno));

	return memcpy(ret, mem, len);
}

struct string*
string(struct string *s, const char *str)
{
	/* Return dynamically allocated duplicate string with cached length */

	size_t len = strlen(str) + 1;

	void *ret;

	if ((ret = malloc(len)) == NULL)
		fatal("malloc", errno);
	memcpy(ret, mem, len);

	s->len = len - 1;
	s->str = memcpy(ret, str, len);

	return s;
	return ret;
}

int


@@ 453,7 413,7 @@ word_wrap(int n, char **str, char *end)
	char *ret, *tmp;

	if (n < 1)
		fatal("insufficient columns", 0);
		fatal("insufficient columns: %d", n);

	/* All fits */
	if ((end - *str) <= n)

M src/utils/utils.h => src/utils/utils.h +34 -80
@@ 1,17 1,44 @@
#ifndef UTILS_H
#define UTILS_H

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

/* Parsed IRC message */
#define TO_STR(X) #X
#define STR(X) TO_STR(X)

#define MAX(A, B) ((A) > (B) ? (A) : (B))
#define MIN(A, B) ((A) > (B) ? (B) : (A))

#define ELEMS(X) (sizeof((X)) / sizeof((X)[0]))
#define ARR_ELEM(A, E) ((E) >= 0 && (size_t)(E) < ELEMS((A)))

#define UNUSED(X) ((void)(X))

#define message(TYPE, ...) \
	fprintf(stderr, "%s %s:%d:%s ", (TYPE), __FILE__, __LINE__, __func__); \
	fprintf(stderr, __VA_ARGS__); \
	fprintf(stderr, "\n");

#if (defined DEBUG) && !(defined TESTING)
#define debug(...) \
	do { message("DEBUG", __VA_ARGS__); } while (0)
#else
#define debug(...)
#endif

#ifndef fatal
#define fatal(...) \
	do { message("FATAL", __VA_ARGS__); exit(EXIT_FAILURE); } while (0)
#define fatal_noexit(...) \
	do { message("FATAL", __VA_ARGS__); } while (0)
#endif

/* FIXME: don't seperate trailing from params
 * simplify retrieving/tokenizing arguments
 * from a parsed_mesg struct
 */
/* Parsed IRC message */
struct parsed_mesg
{
	char *from;


@@ 21,26 48,17 @@ struct parsed_mesg
	char *trailing;
};

/* Dynamically allocated string with cached length */
struct string
{
	const char *str;
	size_t len;
};

int irc_isnickchar(char, int);
//TODO: replace comps to channel / nicks
int irc_ischanchar(char, int);
int irc_isnick(const char*);
int irc_isnickchar(char, int);
int irc_ischan(const char*);


//TODO: replace comps to channel / nicks
int irc_isnick(const char*);
//TODO: CASEMAPPING
int irc_strcmp(const char*, const char*);
int irc_strncmp(const char*, const char*, size_t);

char* getarg(char**, const char*);
char* word_wrap(int, char**, char*);

char* strdup(const char*);
void* memdup(const void*, size_t);



@@ 48,68 66,4 @@ int check_pinged(const char*, const char*);
int parse_mesg(struct parsed_mesg*, char*);
int skip_sp(char**);

struct string* string(struct string*, const char*);
void string_free(struct string*);

void handle_error(int, const char*, ...);

extern int fatal_exit;

#define TO_STR(X) #X
#define STR(X) TO_STR(X)

#define MAX(A, B) ((A) > (B) ? (A) : (B))
#define MIN(A, B) ((A) > (B) ? (B) : (A))

#define ELEMS(X) (sizeof((X)) / sizeof((X)[0]))
#define ARR_ELEM(A, E) ((E) >= 0 && (size_t)(E) < ELEMS((A)))

#define UNUSED(X) ((void)(X))

#if (defined DEBUG) && !(defined TESTING)
#define DEBUG_MSG(...) \
	do { \
		fprintf(stderr, "%s:%d:%-12s\t", __FILE__, __LINE__, __func__); \
		fprintf(stderr, __VA_ARGS__); \
		fprintf(stderr, "\n"); \
	} while (0)
#else
#define DEBUG_MSG(...)
#endif

/* Irrecoverable error
 *   this define is precluded in test.h to aggregate fatal errors in testcases */
#ifndef fatal
#define fatal(M, E) \
	do { handle_error(E, "ERROR in %s: %s", __func__, M); exit(EXIT_FAILURE); } while (0)
#endif

//TODO: refactor
/* Doubly linked list macros */
#define DLL_NEW(L, N) ((L) = (N)->next = (N)->prev = (N))

#define DLL_ADD(L, N) \
	do { \
		if ((L) == NULL) \
			DLL_NEW(L, N); \
		else { \
			((L)->next)->prev = (N); \
			(N)->next = ((L)->next); \
			(N)->prev = (L); \
			(L)->next = (N); \
		} \
	} while (0)

#define DLL_DEL(L, N) \
	do { \
		if (((N)->next) == (N)) \
			(L) = NULL; \
		else { \
			if ((L) == N) \
				(L) = ((N)->next); \
			((N)->next)->prev = ((N)->prev); \
			((N)->prev)->next = ((N)->next); \
		} \
	} while (0)

#endif

M test/components/buffer.c => test/components/buffer.c +48 -31
@@ 9,7 9,7 @@ _fmt_int(int i)
{
	static char buff[1024];

	if ((snprintf(buff, sizeof(buff) - 1, "%d", i)) < 0)
	if ((snprintf(buff, sizeof(buff), "%d", i)) < 0)
		fail_test("snprintf");

	return buff;


@@ 20,10 20,7 @@ _buffer_newline(struct buffer *b, const char *t)
{
	/* Abstract newline with default values */

	struct string from = { .str = "", .len = 0 };
	struct string text = { .str = t,  .len = strlen(t) };

	buffer_newline(b, BUFFER_LINE_OTHER, from, text, 0);
	buffer_newline(b, BUFFER_LINE_OTHER, "", t, 0, strlen(t), 0);
}

static void


@@ 31,7 28,9 @@ test_buffer(void)
{
	/* Test retrieving values from an initialized buffer and resetting it */

	struct buffer b = buffer();
	struct buffer b;

	buffer(&b);

	assert_eq(buffer_size(&b), 0);
	assert_null(buffer_head(&b));


@@ 39,7 38,7 @@ test_buffer(void)
	assert_null(buffer_line(&b, b.scrollback));

	/* Reset the buffer, check values again */
	b = buffer();
	buffer(&b);

	assert_eq(buffer_size(&b), 0);
	assert_null(buffer_head(&b));


@@ 53,8 52,9 @@ test_buffer_head(void)
	/* Test retrieving the first line after pushing to a full buffer */

	int i;
	struct buffer b;

	struct buffer b = buffer();
	buffer(&b);

	assert_null(buffer_head(&b));



@@ 71,8 71,9 @@ test_buffer_tail(void)
	/* Test retrieving the last line after pushing to a full buffer */

	int i;
	struct buffer b;

	struct buffer b = buffer();
	buffer(&b);

	assert_null(buffer_tail(&b));



@@ 93,7 94,9 @@ test_buffer_line(void)
{
	/* Test that retrieving a buffer line fails when i != [tail, head) */

	struct buffer b = buffer();
	struct buffer b;

	buffer(&b);

	/* Should retrieve null for an empty buffer */
	assert_eq(buffer_size(&b), 0);


@@ 184,7 187,9 @@ test_buffer_scrollback(void)
	 *   Buffer scrollback stays locked to the tail when incrementing
	 * */

	struct buffer b = buffer();
	struct buffer b;

	buffer(&b);

	/* Empty buffer returns NULL */
	assert_null(buffer_line(&b, b.scrollback));


@@ 222,7 227,9 @@ test_buffer_scrollback_status(void)
{
	/* Test retrieving buffer scrollback status */

	struct buffer b = buffer();
	struct buffer b;

	buffer(&b);

	b.head = (BUFFER_LINES_MAX / 2) - 1;
	b.tail = UINT_MAX - (BUFFER_LINES_MAX / 2);


@@ 245,19 252,21 @@ test_buffer_index_overflow(void)
{
	/* Test masked indexing after unsigned integer overflow */

	struct buffer b = buffer();
	struct buffer b;

	buffer(&b);

	b.head = UINT_MAX;
	b.tail = UINT_MAX - 1;
	b.scrollback = b.tail;

	assert_eq(buffer_size(&b), 1);
	assert_eq(MASK(b.head), (BUFFER_LINES_MAX - 1));
	assert_eq(BUFFER_MASK(b.head), (BUFFER_LINES_MAX - 1));

	_buffer_newline(&b, _fmt_int(0));

	assert_eq(buffer_size(&b), 2);
	assert_eq(MASK(b.head), 0);
	assert_eq(BUFFER_MASK(b.head), 0);

	_buffer_newline(&b, _fmt_int(-1));



@@ 270,7 279,9 @@ test_buffer_line_overlength(void)
{
	/* Test that lines over the maximum length are recursively split and added separately */

	struct buffer b = buffer();
	struct buffer b;

	buffer(&b);

	/* Indices to first and last positions of lines, total length = 2.5 times the maximum */
	unsigned int f1 = 0,


@@ 316,7 327,9 @@ test_buffer_line_rows(void)
{
	/* Test calculating the number of rows a buffer line occupies */

	struct buffer b = buffer();
	struct buffer b;

	buffer(&b);

	_buffer_newline(&b, "aa bb cc");



@@ 354,20 367,24 @@ test_buffer_newline_prefix(void)
{
	/* Test adding lines to a buffer with prefix */

	struct buffer b = buffer();
	struct buffer b;
	struct buffer_line *line;

	struct string from;
	struct string text;
	buffer(&b);

	char *from_str;
	char *text_str;
	size_t from_len;
	size_t text_len;

	/* Test adding line with prefix */
	from.str = "testing";
	from.len = strlen(from.str);
	from_str = "testing";
	from_len = strlen(from_str);

	text.str = "abc";
	text.len = strlen(text.str);
	text_str = "abc";
	text_len = strlen(text_str);

	buffer_newline(&b, BUFFER_LINE_OTHER, from, text, 0);
	buffer_newline(&b, BUFFER_LINE_OTHER, from_str, text_str, from_len, text_len, 0);

	line = buffer_head(&b);



@@ 375,7 392,7 @@ test_buffer_newline_prefix(void)
	assert_strcmp(line->from, "testing");
	assert_eq((int)line->from_len, (int)strlen("testing"));

	buffer_newline(&b, BUFFER_LINE_OTHER, from, text, '@');
	buffer_newline(&b, BUFFER_LINE_OTHER, from_str, text_str, from_len, text_len, '@');

	line = buffer_head(&b);



@@ 397,19 414,19 @@ test_buffer_newline_prefix(void)

	_from[FROM_LENGTH_MAX - 2] = 'b';
	_from[FROM_LENGTH_MAX - 1] = 'c';
	_from[FROM_LENGTH_MAX]     =   0;
	_from[FROM_LENGTH_MAX]     = 0;

	from.str = _from;
	from.len = FROM_LENGTH_MAX;
	from_str = _from;
	from_len = FROM_LENGTH_MAX;

	buffer_newline(&b, BUFFER_LINE_OTHER, from, text, 0);
	buffer_newline(&b, BUFFER_LINE_OTHER, from_str, text_str, from_len, text_len, 0);

	line = buffer_head(&b);
	assert_eq((int)line->from_len, FROM_LENGTH_MAX);
	assert_eq(line->from[FROM_LENGTH_MAX - 1], 'c');


	buffer_newline(&b, BUFFER_LINE_OTHER, from, text, '@');
	buffer_newline(&b, BUFFER_LINE_OTHER, from_str, text_str, from_len, text_len, '@');

	line = buffer_head(&b);
	assert_eq((int)line->from_len, FROM_LENGTH_MAX);

M test/components/channel.c => test/components/channel.c +2 -0
@@ 1,4 1,6 @@
#include "test/test.h"

#include "src/components/buffer.c"
#include "src/components/channel.c"
#include "src/components/input.c"
#include "src/components/mode.c"

A test/components/input.c => test/components/input.c +575 -0
@@ 0,0 1,575 @@
#include "test/test.h"

/* Preclude definitions for testing */
#define INPUT_LEN_MAX 16
#define INPUT_HIST_MAX 4

#include "src/components/input.c"

#define CHECK_INPUT_FRAME(I, S, F, C) \
	assert_eq(input_frame((I), buf, (F)), (C)); \
	assert_strcmp(buf, (S));

#define CHECK_INPUT_WRITE(I, S) \
	assert_eq(input_write((I), buf, sizeof(buf), 0), (int)strlen((S))); \
	assert_strcmp(buf, (S));

static uint16_t completion_l(char*, uint16_t, uint16_t, int);
static uint16_t completion_m(char*, uint16_t, uint16_t, int);
static uint16_t completion_s(char*, uint16_t, uint16_t, int);
static uint16_t completion_rot1(char*, uint16_t, uint16_t, int);

static char buf[INPUT_LEN_MAX + 1];

static uint16_t
completion_l(char *str, uint16_t len, uint16_t max, int first)
{
	/* Completes to word longer than len */

	(void)len;
	(void)first;

	const char longer[] = "xyxyxy";

	if (max < sizeof(longer) - 1)
		return 0;

	memcpy(str, longer, sizeof(longer) - 1);

	return sizeof(longer) - 1;
}

static uint16_t
completion_m(char *str, uint16_t len, uint16_t max, int first)
{
	/* Writes up to max chars */

	(void)first;

	for (uint16_t i = 0; i < (len + max); i++)
		str[i] = 'x';

	return (len + max);
}

static uint16_t
completion_s(char *str, uint16_t len, uint16_t max, int first)
{
	/* Completes to word shorter than len */

	(void)len;
	(void)first;

	const char shorter[] = "z";

	if (max < sizeof(shorter) - 1)
		return 0;

	memcpy(str, shorter, sizeof(shorter) - 1);

	return sizeof(shorter) - 1;
}

static uint16_t
completion_rot1(char *str, uint16_t len, uint16_t max, int first)
{
	/* Completetion function, increments all characters */

	uint16_t i = 0;

	while (i < len && i < max)
		str[i++] += 1;

	if (first) {
		str[i++] = '!';
		str[i++] = '!';
	}

	return i;
}

static void
test_input_init(void)
{
	struct input inp;

	input_init(&inp);
	assert_eq(input_text_iszero(&inp), 1);
	input_free(&inp);
}

static void
test_input_reset(void)
{
	struct input inp;

	input_init(&inp);

	/* Test clearing empty input */
	assert_eq(input_reset(&inp), 0);
	assert_eq(input_text_iszero(&inp), 1);

	/* Test clearing non-empty input */
	assert_eq(input_insert(&inp, "abc", 3), 1);
	assert_eq(input_reset(&inp), 1);
	assert_eq(input_text_iszero(&inp), 1);

	/* Test clearing non-empty input, cursor at start */
	assert_eq(input_insert(&inp, "abc", 3), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 0);
	assert_eq(input_reset(&inp), 1);
	assert_eq(input_text_iszero(&inp), 1);

	input_free(&inp);
}

static void
test_input_ins(void)
{
	struct input inp;

	input_init(&inp);

	/* Valid */
	assert_eq(input_insert(&inp, "a", 1), 1);
	CHECK_INPUT_WRITE(&inp, "a");

	assert_eq(input_insert(&inp, "bc", 2), 1);
	assert_eq(input_insert(&inp, "de", 2), 1);
	assert_eq(input_insert(&inp, "fgh", 3), 1);
	assert_eq(input_insert(&inp, "i", 1), 1);
	assert_eq(input_insert(&inp, "j", 1), 1);
	assert_eq(input_insert(&inp, "klmnop", 6), 1);
	CHECK_INPUT_WRITE(&inp, "abcdefghijklmnop");
	assert_eq((int)input_text_size(&inp), INPUT_LEN_MAX);

	/* Full */
	assert_eq(input_insert(&inp, "z", 1), 0);
	CHECK_INPUT_WRITE(&inp, "abcdefghijklmnop");
	assert_eq((int)input_text_size(&inp), INPUT_LEN_MAX);

	input_free(&inp);
}

static void
test_input_del(void)
{
	struct input inp;

	input_init(&inp);

	/* Deleting back/forw on empty input */
	CHECK_INPUT_WRITE(&inp, "");
	assert_eq(input_delete_back(&inp), 0);
	assert_eq(input_delete_forw(&inp), 0);

	assert_eq(input_insert(&inp, "abcefg", 6), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);

	/* Delete left */
	assert_eq(input_delete_back(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "acefg");
	assert_eq(input_delete_back(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "cefg");
	assert_eq(input_delete_back(&inp), 0);

	/* Delete right */
	assert_eq(input_delete_forw(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "efg");
	assert_eq(input_delete_forw(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "fg");
	assert_eq(input_delete_forw(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "g");
	assert_eq(input_delete_forw(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "");
	assert_eq(input_delete_forw(&inp), 0);

	input_free(&inp);
}

static void
test_input_hist(void)
{
	struct input inp;

	input_init(&inp);

	assert_eq(input_hist_push(&inp), 0);

	/* Test scrolling input fails when no history */
	assert_eq(input_hist_back(&inp), 0);
	assert_eq(input_hist_forw(&inp), 0);

	/* Test pushing clears the working input */
	assert_eq(input_insert(&inp, "111", 3), 1);
	CHECK_INPUT_WRITE(&inp, "111");
	assert_eq(input_hist_push(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "");

	/* Test pushing up to INPUT_HIST_MAX */
	assert_eq(input_insert(&inp, "222", 3), 1);
	assert_eq(input_hist_push(&inp), 1);
	assert_eq(input_insert(&inp, "333", 3), 1);
	assert_eq(input_hist_push(&inp), 1);
	assert_eq(input_insert(&inp, "444", 3), 1);
	assert_eq(input_hist_push(&inp), 1);

#define INP_HIST_CURR(I) ((I).hist.ptrs[INPUT_MASK((I).hist.current)])
#define INP_HIST_HEAD(I) ((I).hist.ptrs[INPUT_MASK((I).hist.head - 1)])
#define INP_HIST_TAIL(I) ((I).hist.ptrs[INPUT_MASK((I).hist.tail)])

	assert_strcmp(INP_HIST_HEAD(inp), "444");
	assert_strcmp(INP_HIST_TAIL(inp), "111");

	/* Test pushing after INPUT_HIST_MAX frees the tail */
	assert_eq(input_insert(&inp, "555", 3), 1);
	assert_eq(input_hist_push(&inp), 1);

	assert_strcmp(INP_HIST_HEAD(inp), "555");
	assert_strcmp(INP_HIST_TAIL(inp), "222");

	/* Test scrolling back saves the current working input */
	assert_eq(input_insert(&inp, "000", 3), 1);

	/* Test scrolling back to tail */
	assert_eq(input_hist_back(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "555");
	assert_eq(input_hist_back(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "444");
	assert_eq(input_hist_back(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "333");
	assert_eq(input_hist_back(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "222");
	assert_eq(input_hist_back(&inp), 0);
	assert_strcmp(inp.hist.save, "000");

	/* Test scrolling forw to head */
	assert_eq(input_hist_forw(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "333");
	assert_eq(input_hist_forw(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "444");
	assert_eq(input_hist_forw(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "555");
	assert_eq(input_hist_forw(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "000");
	assert_eq(input_hist_forw(&inp), 0);

	assert_strcmp(INP_HIST_HEAD(inp), "555");
	assert_strcmp(INP_HIST_TAIL(inp), "222");

	/* Test replaying hist head */
	assert_eq(input_hist_back(&inp), 1);
	assert_eq(input_hist_push(&inp), 1);
	assert_strcmp(INP_HIST_HEAD(inp), "555");
	assert_strcmp(INP_HIST_TAIL(inp), "222");

	/* Test replaying hist middle */
	assert_eq(input_hist_back(&inp), 1);
	assert_eq(input_hist_back(&inp), 1);
	assert_eq(input_hist_push(&inp), 1);
	assert_strcmp(INP_HIST_HEAD(inp), "444");
	assert_strcmp(INP_HIST_TAIL(inp), "222");

	/* Test replaying hist tail */
	assert_eq(input_hist_back(&inp), 1);
	assert_eq(input_hist_back(&inp), 1);
	assert_eq(input_hist_back(&inp), 1);
	assert_eq(input_hist_back(&inp), 1);
	assert_eq(input_hist_back(&inp), 0);
	assert_eq(input_hist_push(&inp), 1);
	assert_strcmp(INP_HIST_HEAD(inp), "222");
	assert_strcmp(INP_HIST_TAIL(inp), "333");

#undef INP_HIST_CURR
#undef INP_HIST_HEAD
#undef INP_HIST_TAIL

	input_free(&inp);
}

static void
test_input_move(void)
{
	struct input inp;

	input_init(&inp);

	/* Test move back */
	assert_eq(input_insert(&inp, "ab", 2), 1);
	CHECK_INPUT_WRITE(&inp, "ab");
	assert_eq(input_cursor_back(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "ab");
	assert_eq(input_insert(&inp, "c", 1), 1);
	CHECK_INPUT_WRITE(&inp, "acb");
	assert_eq(input_insert(&inp, "d", 1), 1);
	CHECK_INPUT_WRITE(&inp, "acdb");
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 0);
	assert_eq(input_insert(&inp, "e", 1), 1);
	CHECK_INPUT_WRITE(&inp, "eacdb");

	/* Test move forward */
	assert_eq(input_cursor_forw(&inp), 1);
	assert_eq(input_cursor_forw(&inp), 1);
	assert_eq(input_cursor_forw(&inp), 1);
	assert_eq(input_insert(&inp, "f", 1), 1);
	CHECK_INPUT_WRITE(&inp, "eacdfb");
	assert_eq(input_cursor_forw(&inp), 1);
	assert_eq(input_cursor_forw(&inp), 0);
	assert_eq(input_insert(&inp, "g", 1), 1);
	CHECK_INPUT_WRITE(&inp, "eacdfbg");

	input_free(&inp);
}

static void
test_input_frame(void)
{
	struct input inp;

	input_init(&inp);

	assert_eq(input_insert(&inp, "1234567890", 10), 1);

	/* Test cursor fits */
	CHECK_INPUT_FRAME(&inp, "1234567890", 12, 10);

	/* Test cursor doesnt fit */
	CHECK_INPUT_FRAME(&inp, "567890", 11, 6);

	/* Test cursor back keeps cursor in view */
	CHECK_INPUT_FRAME(&inp, "890", 6, 3);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	CHECK_INPUT_FRAME(&inp, "56789", 6, 3);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	CHECK_INPUT_FRAME(&inp, "23456", 6, 3);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	CHECK_INPUT_FRAME(&inp, "12345", 6, 0);

	/* Test cursor forw keeps cursor in view */
	assert_eq(input_cursor_forw(&inp), 1);
	assert_eq(input_cursor_forw(&inp), 1);
	assert_eq(input_cursor_forw(&inp), 1);
	CHECK_INPUT_FRAME(&inp, "12345", 6, 3);
	assert_eq(input_cursor_forw(&inp), 1);
	assert_eq(input_cursor_forw(&inp), 1);
	assert_eq(input_cursor_forw(&inp), 1);
	CHECK_INPUT_FRAME(&inp, "45678", 6, 3);
	assert_eq(input_cursor_forw(&inp), 1);
	assert_eq(input_cursor_forw(&inp), 1);
	assert_eq(input_cursor_forw(&inp), 1);
	assert_eq(input_cursor_forw(&inp), 1);
	CHECK_INPUT_FRAME(&inp, "890", 6, 3);

	input_free(&inp);
}

static void
test_input_write(void)
{
	struct input inp;

	input_init(&inp);

	/* Test output is written correctly regardless of cursor position */
	assert_eq(input_insert(&inp, "abcde", 5), 1);
	CHECK_INPUT_WRITE(&inp, "abcde");
	assert_eq(input_cursor_back(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "abcde");
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	CHECK_INPUT_WRITE(&inp, "abcde");
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 0);
	CHECK_INPUT_WRITE(&inp, "abcde");

	/* Test output is always null terminated */
	assert_eq(input_insert(&inp, "fghijklmno", 10), 1);
	CHECK_INPUT_WRITE(&inp, "fghijklmnoabcde");

	/* Test truncated write */
	assert_eq(input_write(&inp, buf, 5, 0), 4);
	assert_strcmp(buf, "fghi");

	/* Test truncated, offset write */
	assert_eq(input_write(&inp, buf, 5, 3), 4);
	assert_strcmp(buf, "ijkl");

	input_free(&inp);
}

static void
test_input_complete(void)
{
	struct input inp;

	input_init(&inp);

	/* Test empty */
	assert_eq(input_complete(&inp, completion_rot1), 0);
	assert_eq(input_reset(&inp), 0);

	/* Test only space */
	assert_eq(input_insert(&inp, " ", 1), 1);
	assert_eq(input_complete(&inp, completion_rot1), 0);
	assert_eq(input_reset(&inp), 1);

	/* Test: ` abc `
	 *             ^ */
	assert_eq(input_insert(&inp, " abc ", 5), 1);
	assert_eq(input_complete(&inp, completion_rot1), 0);
	CHECK_INPUT_WRITE(&inp, " abc ");

	/* Test: ` abc `
	 *            ^ */
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(inp.text[inp.head], ' ');
	assert_eq(input_complete(&inp, completion_rot1), 1);
	assert_eq(inp.text[inp.head], ' ');
	CHECK_INPUT_WRITE(&inp, " bcd ");

	/* Test: ` bcd `
	 *           ^ */
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(inp.text[inp.head], 'd');
	assert_eq(input_complete(&inp, completion_rot1), 1);
	assert_eq(inp.text[inp.head], ' ');
	CHECK_INPUT_WRITE(&inp, " cde ");

	/* Test: ` cde `
	 *          ^ */
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(inp.text[inp.head], 'd');
	assert_eq(input_complete(&inp, completion_rot1), 1);
	assert_eq(inp.text[inp.head], ' ');
	CHECK_INPUT_WRITE(&inp, " def ");

	/* Test: ` def `
	 *         ^ */
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(inp.text[inp.head], 'd');
	assert_eq(input_complete(&inp, completion_rot1), 1);
	assert_eq(inp.text[inp.head], ' ');
	CHECK_INPUT_WRITE(&inp, " efg ");

	/* Test: ` efg `
	 *        ^ */
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(inp.text[inp.head], ' ');
	assert_eq(input_complete(&inp, completion_rot1), 0);
	assert_eq(inp.text[inp.head], ' ');
	CHECK_INPUT_WRITE(&inp, " efg ");
	assert_eq(input_reset(&inp), 1);

	/* Test start of line */
	assert_eq(input_insert(&inp, "x abc ", 6), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(inp.text[inp.head], 'x');
	assert_eq(input_complete(&inp, completion_rot1), 1);
	assert_eq(inp.text[inp.tail], ' ');
	CHECK_INPUT_WRITE(&inp, "y!! abc ");
	assert_eq(input_reset(&inp), 1);

	/* Test replacement word longer */
	assert_eq(input_insert(&inp, " abc ab", 7), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(inp.text[inp.head], 'c');
	assert_eq(input_complete(&inp, completion_l), 1);
	CHECK_INPUT_WRITE(&inp, " xyxyxy ab");
	assert_eq(inp.text[inp.tail], ' '); /* points to 'c' */
	assert_eq(input_reset(&inp), 1);

	/* Test replacement word shorter */
	assert_eq(input_insert(&inp, " abc ab ", 8), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(input_cursor_back(&inp), 1);
	assert_eq(inp.text[inp.head], 'c');
	assert_eq(input_complete(&inp, completion_s), 1);
	CHECK_INPUT_WRITE(&inp, " z ab ");
	assert_eq(inp.text[inp.tail], ' ');
	assert_eq(input_reset(&inp), 1);

	/* Test writing up to max chars */
	assert_eq(input_insert(&inp, "a", 1), 1);
	assert_eq(input_complete(&inp, completion_m), 1);
	CHECK_INPUT_WRITE(&inp, "xxxxxxxxxxxxxxxx");
	assert_eq((int)input_text_size(&inp), INPUT_LEN_MAX);

	input_free(&inp);
}

static void
test_input_text_size(void)
{
	struct input inp;

	input_init(&inp);

	/* Test size is correct from 0 -> max */
	for (int i = 0; i < INPUT_LEN_MAX; i++) {
		assert_eq((int)input_text_size(&inp), i);
		assert_eq(input_insert(&inp, "a", 1), 1);
	}

	assert_eq((int)input_text_size(&inp), INPUT_LEN_MAX);

	/* Test size is correct regardless of cursor position */
	for (int i = 0; i < INPUT_LEN_MAX; i++) {
		assert_eq(input_cursor_back(&inp), 1);
		assert_eq((int)input_text_size(&inp), INPUT_LEN_MAX);
	}

	input_free(&inp);
}

int
main(void)
{
	testcase tests[] = {
		TESTCASE(test_input_init),
		TESTCASE(test_input_reset),
		TESTCASE(test_input_ins),
		TESTCASE(test_input_del),
		TESTCASE(test_input_hist),
		TESTCASE(test_input_move),
		TESTCASE(test_input_frame),
		TESTCASE(test_input_write),
		TESTCASE(test_input_complete),
		TESTCASE(test_input_text_size)
	};

	return run_tests(tests);
}

M test/components/server.c => test/components/server.c +7 -33
@@ 1,41 1,16 @@
#include "test/test.h"
#include "src/components/server.c"
#include "src/components/mode.c"   /* mode_config_defaults */
#include "src/utils/utils.c"       /* skip_sp */

void
channel_set_current(struct channel *c)
{
	UNUSED(c);
}

struct channel*
new_channel(const char *n, struct server *s, enum channel_t t)
{
	/* FIXME: mock new_channel until channel() is implemented */

	UNUSED(n);
	UNUSED(s);
	UNUSED(t);
	return NULL;
}

struct connection*
connection(const void *s, const char *h, const char *p)
{
	/* Mock */

	UNUSED(s);
	UNUSED(h);
	UNUSED(p);
	return (void *)(s);
}
#include "src/components/channel.c"
#include "src/components/user.c"
#include "src/components/mode.c"
#include "src/components/input.c"
#include "src/components/buffer.c"
#include "src/utils/utils.c"

void
newline(struct channel *c, enum buffer_line_t t, const char *f, const char *m)
{
	/* Mock */

	UNUSED(c);
	UNUSED(t);
	UNUSED(f);


@@ 46,7 21,6 @@ void
newlinef(struct channel *c, enum buffer_line_t t, const char *f, const char *m, ...)
{
	/* Mock */

	UNUSED(c);
	UNUSED(t);
	UNUSED(f);


@@ 182,7 156,7 @@ test_server_set_nicks(void)
	server_nicks_next(s);
	assert_strncmp(s->nick, "rirc", 4);

	server_nicks_reset(s);
	server_reset(s);
	server_nicks_next(s);
	assert_strcmp(s->nick, "a_");


M test/components/user.c => test/components/user.c +7 -7
@@ 37,9 37,9 @@ test_user_list(void)
	if ((u3 = user_list_get(&ulist, "ccc", 0)) == NULL)
		abort_test("Failed to retrieve u3");

	assert_strcmp(u1->nick.str, "aaa");
	assert_strcmp(u2->nick.str, "bbb");
	assert_strcmp(u3->nick.str, "ccc");
	assert_strcmp(u1->nick, "aaa");
	assert_strcmp(u2->nick, "bbb");
	assert_strcmp(u3->nick, "ccc");

	/* Test retrieving by name prefix, failure */
	assert_null(user_list_get(&ulist, "z",  1));


@@ 54,9 54,9 @@ test_user_list(void)
	if ((u3 = user_list_get(&ulist, "ccc", 3)) == NULL)
		abort_test("Failed to retrieve u3 by prefix");

	assert_strcmp(u1->nick.str, "aaa");
	assert_strcmp(u2->nick.str, "bbb");
	assert_strcmp(u3->nick.str, "ccc");
	assert_strcmp(u1->nick, "aaa");
	assert_strcmp(u2->nick, "bbb");
	assert_strcmp(u3->nick, "ccc");

	/* Test replacing user in list, failure */
	assert_eq(user_list_rpl(&ulist, "zzz", "yyy"), USER_ERR_NOT_FOUND);


@@ 76,7 76,7 @@ test_user_list(void)
	assert_eq(u4->prfxmodes.upper, 0x456);
	assert_eq(u4->prfxmodes.prefix, '*');

	assert_strcmp(u4->nick.str, "ddd");
	assert_strcmp(u4->nick, "ddd");

	assert_null(user_list_get(&ulist, "ccc",  0));


M test/io.c => test/io.c +0 -2
@@ 80,8 80,6 @@ test_io_recv(void)
int
main(void)
{
	PT_CF(pthread_once(&init_once, io_init));

	testcase tests[] = {
		TESTCASE(test_io_recv),
	};

M test/test.h => test/test.h +17 -7
@@ 4,6 4,10 @@
#define TESTING

/* TODO: list the test types at the top of file with usage */
/* TODO: randomize testcase ordering */
/* TODO: colours */
/* TODO: verbose output */
/* TODO: number of tests ran, time */

#include <inttypes.h>
#include <setjmp.h>


@@ 27,7 31,8 @@ static jmp_buf _tc_fatal_expected_,
               _tc_fatal_unexpected_;

static int _assert_fatal_, _failures_, _failures_t_, _failure_printed_;
static char _tc_errbuf_[512];
static char _tc_errbuf_1[512];
static char _tc_errbuf_2[512];

static int _assert_strcmp(const char*, const char*);
static int _assert_strncmp(const char*, const char*, size_t);


@@ 73,17 78,20 @@ static void _print_testcase_name_(const char*);
 *   in normal operation should fatally exit the program
 *   in testing should be considered a failure but should NOT continue running the testcase */
#ifdef fatal
	#error "test.h" should be the first include within testcase files
#error "test.h" should be the first include within testcase files
#else
	#define fatal(M, E) \
#define _fatal(...) \
	do { \
		if (_assert_fatal_) { \
			longjmp(_tc_fatal_expected_, 1); \
		} else { \
			snprintf(_tc_errbuf_, sizeof(_tc_errbuf_) - 1, "FATAL in "__FILE__":%d - %s : '%s'", __LINE__, __func__, M); \
			snprintf(_tc_errbuf_1, sizeof(_tc_errbuf_1), "%s:%d:%s:", __FILE__, __LINE__, __func__); \
			snprintf(_tc_errbuf_2, sizeof(_tc_errbuf_2), __VA_ARGS__); \
			longjmp(_tc_fatal_unexpected_, 1); \
		} \
	} while (0)
#define fatal        _fatal
#define fatal_noexit _fatal
#endif

#define assert_fatal(X) \


@@ 172,7 180,7 @@ _assert_strncmp(const char *p1, const char *p2, size_t n)
static void
_print_testcase_name_(const char *name)
{
	/* Print the testcase name only one */
	/* Print the testcase name once */
	if (!_failure_printed_) {
		_failure_printed_ = 1;



@@ 186,7 194,7 @@ _print_testcase_name_(const char *name)
static int
_run_tests_(const char *filename, testcase testcases[], size_t len)
{
	/* Silence compiler warnings for test functions/vars that are included but not used */
	/* Silence compiler warnings for unused test functions/vars */
	((void)(_assert_strcmp));
	((void)(_assert_strncmp));
	((void)(_assert_fatal_));


@@ 205,7 213,9 @@ _run_tests_(const char *filename, testcase testcases[], size_t len)

		if (setjmp(_tc_fatal_unexpected_)) {
			_print_testcase_name_(tc->tc_str);
			printf("    %s - aborting testcase\n", _tc_errbuf_);
			printf("    Unexpected fatal error:\n");
			printf("    %s %s\n", _tc_errbuf_1,_tc_errbuf_2);
			printf("    -- aborting testcase --\n");
			_failures_++;
		} else
			(*tc->tc_ptr)();

M test/utils/utils.c => test/utils/utils.c +1 -15
@@ 402,19 402,6 @@ test_word_wrap(void)
		fail_test("seg1 should be advanced to end of string");
}

static void
test_string(void)
{
	struct string s;

	string(&s, "abcde");

	assert_strcmp(s.str, "abcde");
	assert_eq((int)s.len, 5);

	free((void*)s.str);
}

int
main(void)
{


@@ 426,8 413,7 @@ main(void)
		TESTCASE(test_irc_toupper),
		TESTCASE(test_parse_mesg),
		TESTCASE(test_skip_sp),
		TESTCASE(test_word_wrap),
		TESTCASE(test_string)
		TESTCASE(test_word_wrap)
	};

	return run_tests(tests);