~rcr/rirc

de92b13e9c055d430cfcdc1ab008b2ae01cf95c9 — Richard Robbins 11 days ago e54533b + c2f4b55
Merge branch 'dev' into static_analysis
R .builds/alpine.yml => .builds/compat/debian.yml +4 -9
@@ 1,23 1,18 @@
image: alpine/edge
image: debian/stable

packages:
  - cmake
  - curl
  - gperf
  - perl

sources:
  - https://git.sr.ht/~rcr/rirc

tasks:
  - setup: |
      cd rirc
      git submodule init
      git submodule update --recursive
      export MAKEFLAGS='-j $(nproc)'
  - build: |
      cd rirc
      make clean
      make all
  - check: |
      cd rirc
      make check

triggers:

A .builds/compat/fedora.yml => .builds/compat/fedora.yml +21 -0
@@ 0,0 1,21 @@
image: fedora/latest

packages:
  - curl
  - gperf

sources:
  - https://git.sr.ht/~rcr/rirc

tasks:
  - build: |
      cd rirc
      make all
  - check: |
      cd rirc
      make check

triggers:
  - action: email
    condition: failure
    to: mail+sourcehut+builds@rcr.io

A .builds/compat/freebsd.yml => .builds/compat/freebsd.yml +22 -0
@@ 0,0 1,22 @@
image: freebsd/latest

packages:
  - curl
  - gmake
  - gperf

sources:
  - https://git.sr.ht/~rcr/rirc

tasks:
  - build: |
      cd rirc
      gmake all
  - check: |
      cd rirc
      gmake check

triggers:
  - action: email
    condition: failure
    to: mail+sourcehut+builds@rcr.io

A .builds/compat/openbsd.yml => .builds/compat/openbsd.yml +22 -0
@@ 0,0 1,22 @@
image: openbsd/latest

packages:
  - curl
  - gmake
  - gperf

sources:
  - https://git.sr.ht/~rcr/rirc

tasks:
  - build: |
      cd rirc
      gmake all
  - check: |
      cd rirc
      gmake check

triggers:
  - action: email
    condition: failure
    to: mail+sourcehut+builds@rcr.io

R .builds/debian.yml => .builds/dev.yml +3 -9
@@ 1,10 1,8 @@
image: debian/stable
image: alpine/latest

packages:
  - cmake
  - curl
  - gperf
  - perl
  - unzip

sources:


@@ 15,15 13,11 @@ secrets:
  - a58d0951-f57f-44ef-8ef2-25f2f84f0e89

tasks:
  - setup: |
      cd rirc
      git submodule init
      git submodule update --recursive
      export MAKEFLAGS='-j $(nproc)'
  - build: |
      cd rirc
      make clean
      make all
  - check: |
      cd rirc
      make check
  - static-analysis: |
      cd rirc

A .gitattributes => .gitattributes +3 -0
@@ 0,0 1,3 @@
.builds        export-ignore
.gitignore     export-ignore
.gitattributes export-ignore

M .gitignore => .gitignore +5 -4
@@ 3,17 3,18 @@
*.gcov
*.gperf.out
*.o
*.out
*.t
*.td
.clangd
.cache
bld
compile_commands.json
build
config.h
coverage
lib/mbedtls-*
lib/mbedtls.sha256
rirc
rirc.1
rirc.debug
rirc.debug.address
rirc.debug.thread
rirc.out
tags

D .gitmodules => .gitmodules +0 -4
@@ 1,4 0,0 @@
[submodule "mbedtls"]
	path = lib/mbedtls
	url = https://github.com/ARMmbed/mbedtls.git
	ignore = dirty

M Makefile => Makefile +79 -98
@@ 1,123 1,104 @@
VERSION := 0.1.4

# Release and debug build executable names
BIN_R := rirc
BIN_D := rirc.debug

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

TLS_CONF := $(PWD)/lib/mbedtls.h
TLS_INCL := -I $(PWD)/lib/mbedtls/include/ -DMBEDTLS_CONFIG_FILE='<$(TLS_CONF)>'
TLS_LIBS := ./lib/mbedtls/library/libmbedtls.a \
            ./lib/mbedtls/library/libmbedx509.a \
            ./lib/mbedtls/library/libmbedcrypto.a

STDS := -std=c11 -D_POSIX_C_SOURCE=200809L

CC := cc
PP := cc -E
CFLAGS   := $(CC_EXT) $(STDS) $(TLS_INCL) -I. -DVERSION=\"$(VERSION)\" -Wall -Wextra -pedantic
CFLAGS_R := $(CFLAGS) -O2 -flto -DNDEBUG
CFLAGS_D := $(CFLAGS) -O0 -g
LDFLAGS  := $(LD_EXT) -lpthread

# Build, source, test source directories
DIR_B := bld
DIR_S := src
DIR_T := test

PWD  := $(shell pwd)

SRC     := $(shell find $(DIR_S) -name '*.c')
SUBDIRS += $(shell find $(DIR_S) -name '*.c' -exec dirname {} \; | sort -u)

SRC_G   := $(shell find $(DIR_S) -name '*.gperf')
SUBDIRS += $(shell find $(DIR_S) -name '*.gperf' -exec dirname {} \; | sort -u)

# Release, debug, testcase build objects
OBJS_D := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.db.o, $(SRC))
OBJS_R := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.o,    $(SRC))
OBJS_T := $(patsubst $(DIR_S)/%.c, $(DIR_B)/%.t,    $(SRC))
OBJS_T += $(DIR_B)/utils/tree.t # Header only file

# Gperf generated source files
OBJS_G := $(patsubst %.gperf, %.gperf.out, $(SRC_G))

# Release build executable
$(BIN_R): $(TLS_LIBS) $(OBJS_G) $(OBJS_R)
	@echo " CC  $@"
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_R) $(TLS_LIBS)

# Debug build executable
$(BIN_D): $(TLS_LIBS) $(OBJS_G) $(OBJS_D)
	@echo " CC  $@"
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_D) $(TLS_LIBS)

# Release build objects
$(DIR_B)/%.o: $(DIR_S)/%.c config.h | $(DIR_B)
	@echo " CC  $<"
	@$(PP) $(CFLAGS_R) -MM -MP -MT $@ -MF $(@:.o=.o.d) $<
	@$(CC) $(CFLAGS_R) -c -o $@ $<

# Debug build objects
$(DIR_B)/%.db.o: $(DIR_S)/%.c config.h | $(DIR_B)
	@echo " CC  $<"
	@$(PP) $(CFLAGS_D) -MM -MP -MT $@ -MF $(@:.o=.o.d) $<
	@$(CC) $(CFLAGS_D) -c -o $@ $<

# Testcases
$(DIR_B)/%.t: $(DIR_T)/%.c $(OBJS_G) | $(DIR_B)
	@$(PP) $(CFLAGS_D) -MM -MP -MT $@ -MF $(@:.t=.t.d) $<
	@$(CC) $(CFLAGS_D) -c -o $(@:.t=.t.o) $<
	@$(CC) $(CFLAGS_D) -o $@ $(@:.t=.t.o)
	@$(TEST_EXT) ./$@ || mv $@ $(@:.t=.td)
PREFIX   ?= /usr/local
PATH_BIN := $(DESTDIR)$(PREFIX)/bin
PATH_MAN := $(DESTDIR)$(PREFIX)/share/man/man1

PATH_BUILD := build
PATH_LIB   := lib
PATH_SRC   := src
PATH_TEST  := test

include lib/mbedtls.Makefile

CONFIG := config.h

CFLAGS ?= -O2 -flto
CFLAGS += -DNDEBUG

CFLAGS_DEBUG += -O0 -g3 -Wall -Wextra -Werror

RIRC_CFLAGS += -std=c11 -I. -DVERSION=\"$(VERSION)\"
RIRC_CFLAGS += -D_POSIX_C_SOURCE=200809L
RIRC_CFLAGS += -D_DARWIN_C_SOURCE

LDFLAGS += -lpthread

SRC       := $(shell find $(PATH_SRC) -name '*.c')
SRC_GPERF := $(patsubst %, %.out, $(shell find $(PATH_SRC) -name '*.gperf'))

# Release objects, debug objects, testcases
OBJS_R := $(patsubst $(PATH_SRC)/%.c, $(PATH_BUILD)/%.o,    $(SRC))
OBJS_D := $(patsubst $(PATH_SRC)/%.c, $(PATH_BUILD)/%.db.o, $(SRC))
OBJS_T := $(patsubst $(PATH_SRC)/%.c, $(PATH_BUILD)/%.t,    $(SRC))
OBJS_T += $(PATH_BUILD)/utils/tree.t # Header only file

rirc: $(RIRC_LIBS) $(SRC_GPERF) $(OBJS_R)
	@echo "  CC    $@"
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_R) $(RIRC_LIBS)

rirc.debug: $(RIRC_LIBS) $(SRC_GPERF) $(OBJS_D)
	@echo "  CC    $@"
	@$(CC) $(LDFLAGS) -o $@ $(OBJS_D) $(RIRC_LIBS)

$(PATH_BUILD)/%.o: $(PATH_SRC)/%.c $(CONFIG) | $(PATH_BUILD)
	@echo "  CC    $<"
	@$(CPP) $(CFLAGS) $(RIRC_CFLAGS) -MM -MP -MT $@ -MF $(@:.o=.o.d) $<
	@$(CC)  $(CFLAGS) $(RIRC_CFLAGS) -c -o $@ $<

$(PATH_BUILD)/%.db.o: $(PATH_SRC)/%.c $(CONFIG) | $(PATH_BUILD)
	@echo "  CC    $<"
	@$(CPP) $(CFLAGS_DEBUG) $(RIRC_CFLAGS) -MM -MP -MT $@ -MF $(@:.o=.o.d) $<
	@$(CC)  $(CFLAGS_DEBUG) $(RIRC_CFLAGS) -c -o $@ $<

$(PATH_BUILD)/%.t: $(PATH_TEST)/%.c $(SRC_GPERF) $(CONFIG) | $(PATH_BUILD)
	@$(CPP) $(CFLAGS_DEBUG) $(RIRC_CFLAGS) -MM -MP -MT $@ -MF $(@:.t=.t.d) $<
	@$(CC)  $(CFLAGS_DEBUG) $(RIRC_CFLAGS) -c -o $(@:.t=.t.o) $<
	@$(CC)  $(CFLAGS_DEBUG) $(RIRC_CFLAGS) -o $@ $(@:.t=.t.o)
	@./$@ || mv $@ $(@:.t=.td)
	@[ -f $@ ]

config.h:
$(PATH_BUILD):
	@mkdir -p $(patsubst $(PATH_SRC)%, $(PATH_BUILD)%, $(shell find $(PATH_SRC) -type d))

$(CONFIG):
	cp config.def.h config.h

%.gperf.out: %.gperf
	gperf --output-file=$@ $<

# Build directories
$(DIR_B):
	@for dir in $(patsubst $(DIR_S)/%, %, $(SUBDIRS)); do mkdir -p $(DIR_B)/$$dir; done

# TLS libraries
$(TLS_LIBS): $(TLS_CONF)
	@CFLAGS="$(TLS_INCL)" $(MAKE) --silent -C ./lib/mbedtls clean
	@CFLAGS="$(TLS_INCL)" $(MAKE) --silent -C ./lib/mbedtls lib

all:
	@$(MAKE) --silent $(BIN_R)
	@$(MAKE) --silent $(BIN_D)
	@$(MAKE) --silent rirc
	@$(MAKE) --silent rirc.debug

check:
	@$(MAKE) --silent $(OBJS_T)

clean:
	@rm -rf $(DIR_B)
	@rm -vf $(BIN_R) $(BIN_D) $(OBJS_G)
	@rm -rfv rirc rirc.debug $(SRC_GPERF) $(PATH_BUILD)

libs: $(TLS_LIBS)
libs:
	@$(MAKE) --silent $(RIRC_LIBS)

install: $(BIN_R)
	@echo installing executable to $(BIN_DIR)
	@echo installing manual page to $(MAN_DIR)
	@mkdir -p $(BIN_DIR)
	@mkdir -p $(MAN_DIR)
	@cp -f rirc $(BIN_DIR)
	@chmod 755 $(BIN_DIR)/rirc
	@sed "s/VERSION/$(VERSION)/g" < rirc.1 > $(MAN_DIR)/rirc.1
install: rirc
	@sed "s/VERSION/$(VERSION)/g" < docs/rirc.1 > rirc.1
	mkdir -p $(PATH_BIN)
	mkdir -p $(PATH_MAN)
	cp -f rirc   $(PATH_BIN)
	cp -f rirc.1 $(PATH_MAN)
	chmod 755 $(PATH_BIN)/rirc
	chmod 644 $(PATH_MAN)/rirc.1

uninstall:
	rm -f $(BIN_DIR)/rirc
	rm -f $(MAN_DIR)/rirc.1
	rm -f $(PATH_BIN)/rirc
	rm -f $(PATH_MAN)/rirc.1

-include $(OBJS_R:.o=.o.d)
-include $(OBJS_D:.o=.o.d)
-include $(OBJS_T:.t=.t.d)

.DEFAULT_GOAL := rirc

.PHONY: all check clean libs install uninstall

.SUFFIXES:

M README.md => README.md +12 -22
@@ 30,40 30,30 @@ A minimalistic irc client written in C.

Connections are TLS enabled over port 6697 by default.

### Configuring:
## Building, installing:

Configure rirc by editing `config.h`. Defaults are in `config.def.h`

### Building:

rirc requires the latest version of GNU gperf to compile.
Building rirc from source requires a c11 compiler, GNU gperf and GNU make.

See: https://www.gnu.org/software/gperf/

Build rirc:
 - https://www.gnu.org/software/gperf/
 - https://www.gnu.org/software/make/

```
git submodule init
git submodule update --recursive
make
make install
```

### Installing:

Default install path:
The build toolchain and install paths can be configured via standard
environment variables, i.e.:

```
BIN_DIR = /usr/local/bin
MAN_DIR = /usr/local/share/man/man1
CC, CFLAGS, LDFLAGS, DESTDIR, PREFIX
```

Edit `Makefile` to alter install path if needed, then:
## Configuring:

```
make install
```
Configure rirc by editing `config.h`. Defaults are in `config.def.h`

### Usage:
## Usage:

```
rirc [-hv] [-s server [...]]


@@ 114,6 104,6 @@ Keys:
   ↓ : input history forward
```

### More info:
## More info:

[https://rcr.io/rirc/](http://rcr.io/rirc/)

M config.def.h => config.def.h +28 -10
@@ 3,11 3,11 @@
 * Colours can be set [0, 255], Any other value (e.g. -1) will set
 * the default terminal foreground/background */

/* Default comma separated set of Nicks to try on connection
/* Comma separated set of default nicks to try on connection
 *   String
 *   ("": defaults to effective user id name)
 */
#define DEFAULT_NICK_SET ""
#define DEFAULT_NICKS ""

/* Default Username and Realname sent during connection
 *   String


@@ 30,13 30,14 @@
#define DEFAULT_QUIT_MESG "rirc v" VERSION
#define DEFAULT_PART_MESG "rirc v" VERSION

#define BUFFER_LINE_HEADER_FG_NEUTRAL 239

/* Buffer colours */
#define BUFFER_LINE_HEADER_FG         239
#define BUFFER_LINE_HEADER_BG         -1
#define BUFFER_LINE_HEADER_FG_PINGED  250
#define BUFFER_LINE_HEADER_BG_PINGED  1

#define BUFFER_LINE_TEXT_FG_NEUTRAL 250
#define BUFFER_LINE_TEXT_FG_GREEN   113
#define BUFFER_TEXT_FG 250;
#define BUFFER_TEXT_BG -1;

/* Number of buffer lines to keep in history, must be power of 2 */
#define BUFFER_LINES_MAX (1 << 10)


@@ 54,10 55,17 @@

#define NAV_CURRENT_CHAN 255

/* Characters */
#define QUOTE_CHAR '>'
#define HORIZONTAL_SEPARATOR "-"
#define VERTICAL_SEPARATOR "~"
/* Separator characters */
#define SEP_HORZ "─"
#define SEP_VERT "~"

/* Separator colours */
#define SEP_FG 239
#define SEP_BG -1

/* Status bar colours */
#define STATUS_FG -1
#define STATUS_BG -1

/* Prefix string for the input line and colours */
#define INPUT_PREFIX " >>> "


@@ 72,6 80,16 @@
#define INPUT_FG 250
#define INPUT_BG -1

/* Buffer text quoting
 *   ("": no text quoting) */
#define QUOTE_LEADER ">"
#define QUOTE_TEXT_FG 2
#define QUOTE_TEXT_BG -1

/* Control character print colour */
#define CTRL_FG 0
#define CTRL_BG 9

/* BUFFER_PADDING:
 * How the buffer line headers will be padded [0, 1]
 *

R rirc.1 => docs/rirc.1 +1 -1
@@ 112,7 112,7 @@ IRC commands:
  /me;<message>
  /nick;[nick]
  /part;[target [targets...]] [part message]
  /priv;<target> <message>
  /privmsg;<target> <message>
  /quit;[quit message]
  /raw;<message>
.TE

D lib/mbedtls => lib/mbedtls +0 -1
@@ 1,1 0,0 @@
Subproject commit 1c54b5410fd48d6bcada97e30cac417c5c7eea67

A lib/mbedtls.Makefile => lib/mbedtls.Makefile +38 -0
@@ 0,0 1,38 @@
MBEDTLS_VER     := 2.25.0
MBEDTLS_VER_SHA := f838f670f51070bc6b4ebf0c084affd9574652ded435b064969f36ce4e8b586d

MBEDTLS_CFG := $(abspath $(PATH_LIB)/mbedtls.h)
MBEDTLS_SHA := $(abspath $(PATH_LIB)/mbedtls.sha256)
MBEDTLS_SRC := $(abspath $(PATH_LIB)/mbedtls-$(MBEDTLS_VER))
MBEDTLS_TAR := $(abspath $(PATH_LIB)/mbedtls-$(MBEDTLS_VER).tar.gz)
MBEDTLS_URL := https://github.com/ARMmbed/mbedtls/archive/v$(MBEDTLS_VER).tar.gz

MBEDTLS_LIBS := \
	$(MBEDTLS_SRC)/library/libmbedtls.a \
	$(MBEDTLS_SRC)/library/libmbedx509.a \
	$(MBEDTLS_SRC)/library/libmbedcrypto.a

$(MBEDTLS_LIBS): $(MBEDTLS_CFG) $(MBEDTLS_SRC)
	@$(MAKE) --silent -C $(MBEDTLS_SRC) clean
	@$(MAKE) --silent -C $(MBEDTLS_SRC) CFLAGS="$(CFLAGS) -DMBEDTLS_CONFIG_FILE='<$(MBEDTLS_CFG)>'" lib

$(MBEDTLS_SRC): $(MBEDTLS_TAR)
	@tar -xmf $(MBEDTLS_TAR) --directory $(PATH_LIB)

$(MBEDTLS_TAR):
	@echo "$(MBEDTLS_TAR)..."
	@curl -LfsS $(MBEDTLS_URL) -o $(MBEDTLS_TAR)
	@eval $(MBEDTLS_SHA_FILE)
	@eval $(MBEDTLS_SHA_CHECK)

ifneq ($(shell command -v shasum 2>/dev/null),)
MBEDTLS_SHA_FILE  := 'echo "$(MBEDTLS_VER_SHA)  $(MBEDTLS_TAR)" > $(MBEDTLS_SHA)'
MBEDTLS_SHA_CHECK := 'shasum -c $(MBEDTLS_SHA)'
endif

RIRC_CFLAGS += -I$(MBEDTLS_SRC)/include/
RIRC_CFLAGS += -DMBEDTLS_CONFIG_FILE='<$(MBEDTLS_CFG)>'

RIRC_LIBS += $(MBEDTLS_LIBS)

.DELETE_ON_ERROR:

A scripts/build.sh => scripts/build.sh +27 -0
@@ 0,0 1,27 @@
#!/bin/bash

# Development build script.
#
#  Usage:
#
#   $ ./scripts/build.sh [make targets]

set -e

export CC=clang
export LDFLAGS="-fuse-ld=lld"

if [ -x "$(command -v entr)" ]; then
	ENTR="entr -c"
fi

if [ -x "$(command -v bear)" ]; then
	BEAR="bear --append --output ./build/compile_commands.json --"
fi

make clean
make build

find -name '*.c' \
  -o -name '*.h' \
  -o -name Makefile | grep -v './lib/' | $ENTR $BEAR make -j $(nproc) "$@"

D scripts/compile_commands.sh => scripts/compile_commands.sh +0 -10
@@ 1,10 0,0 @@
#!/bin/bash

set -e

export CC=clang
export CC_EXT="-Wno-empty-translation-unit"

rm -f compile_commands.json

bear make clean rirc.debug

M scripts/coverage.sh => scripts/coverage.sh +1 -2
@@ 5,8 5,7 @@ set -e
CDIR="coverage"

export CC=gcc
export CC_EXT="-fprofile-arcs -ftest-coverage -fprofile-abs-path"
export LD_EXT="-fprofile-arcs"
export CFLAGS_DEBUG="-fprofile-arcs -ftest-coverage -fprofile-abs-path"

export MAKEFLAGS="-e -j $(nproc)"


M scripts/sa_coverity_get.sh => scripts/sa_coverity_get.sh +3 -3
@@ 21,9 21,9 @@ mkdir -p "$DIR"

echo "*" > "$DIR/.gitignore"

curl -fs --show-error https://scan.coverity.com/download/linux64 -o "$COVERITY_MD5" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc&md5=1"
curl -fs --show-error https://scan.coverity.com/download/linux64 -o "$COVERITY_TGZ" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc"
curl -fsS https://scan.coverity.com/download/linux64 -o "$COVERITY_MD5" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc&md5=1"
curl -fsS https://scan.coverity.com/download/linux64 -o "$COVERITY_TGZ" --data "token=$COVERITY_TOKEN&project=rcr%2Frirc"

printf "%s\t$COVERITY_TGZ" "$(cat "$COVERITY_MD5")" | md5sum --quiet -c -

tar xzf "$COVERITY_TGZ" -C "$DIR" --strip-components 1
tar -xzf "$COVERITY_TGZ" -C "$DIR" --strip-components 1

M scripts/sa_coverity_run.sh => scripts/sa_coverity_run.sh +1 -1
@@ 30,7 30,7 @@ make libs

cov-build --dir "$COVERITY_OUT" make all check

tar czf "$COVERITY_TAR" "$COVERITY_OUT"
tar -czf "$COVERITY_TAR" "$COVERITY_OUT"

curl https://scan.coverity.com/builds?project=rcr%2Frirc \
	--form description="$VERSION" \

M scripts/sa_sonarcloud_get.sh => scripts/sa_sonarcloud_get.sh +3 -3
@@ 22,9 22,9 @@ mkdir -p "$1"

echo "*" > "$1/.gitignore"

curl -fs --show-error "$BUILD_ZIP_URL" -o "$BUILD_ZIP"
curl -fs --show-error "$SONAR_ZIP_URL" -o "$SONAR_ZIP"
curl -fs --show-error "$SONAR_MD5_URL" -o "$SONAR_MD5"
curl -fsS "$BUILD_ZIP_URL" -o "$BUILD_ZIP"
curl -fsS "$SONAR_ZIP_URL" -o "$SONAR_ZIP"
curl -fsS "$SONAR_MD5_URL" -o "$SONAR_MD5"

printf "%s\t$SONAR_ZIP" "$(cat "$SONAR_MD5")" | md5sum --quiet -c -


M scripts/sanitizers_build.sh => scripts/sanitizers_build.sh +4 -4
@@ 4,15 4,15 @@ set -e

export CC=clang

export CC_EXT="-fsanitize=address,undefined -fno-omit-frame-pointer"
export LD_EXT="-fsanitize=address,undefined"
export CFLAGS_DEBUG="-fsanitize=address,undefined -fno-omit-frame-pointer"
export LDFLAGS="-fsanitize=address,undefined -fuse-ld=lld"

make -e clean rirc.debug

mv rirc.debug rirc.debug.address

export CC_EXT="-fsanitize=thread,undefined -fno-omit-frame-pointer"
export LD_EXT="-fsanitize=thread,undefined"
export CFLAGS_DEBUG="-fsanitize=thread,undefined -fno-omit-frame-pointer"
export LDFLAGS="-fsanitize=thread,undefined -fuse-ld=lld"

make -e clean rirc.debug


M scripts/sanitizers_test.sh => scripts/sanitizers_test.sh +2 -2
@@ 3,8 3,8 @@
set -e

export CC=clang
export CC_EXT="-fsanitize=address,undefined -fno-omit-frame-pointer"
export LD_EXT="-fsanitize=address,undefined -fuse-ld=lld"
export CFLAGS_DEBUG="-fsanitize=address,undefined -fno-omit-frame-pointer"
export LDFLAGS="-fsanitize=address,undefined -fuse-ld=lld"

# for core dumps:
# export ASAN_OPTIONS="abort_on_error=1:disable_coredump=0"

M scripts/stack_usage.sh => scripts/stack_usage.sh +2 -2
@@ 3,8 3,8 @@
set -e

export CC=gcc
export CC_EXT="-fstack-usage"
export CFLAGS_DEBUG="-fstack-usage"

make -e clean rirc.debug

find . -name "*.su" -exec cat "{}" ";" | sort -n -k2 | column -t
find bld -name "*.su" -exec cat "{}" ";" | sort -n -k2 | column -t

M src/components/buffer.c => src/components/buffer.c +11 -11
@@ 12,18 12,18 @@
#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*);
static inline unsigned buffer_full(struct buffer*);
static inline unsigned buffer_size(struct buffer*);

static struct buffer_line* buffer_push(struct buffer*);

static inline unsigned int
static inline unsigned
buffer_full(struct buffer *b)
{
	return buffer_size(b) == BUFFER_LINES_MAX;
}

static inline unsigned int
static inline unsigned
buffer_size(struct buffer *b)
{
	return b->head - b->tail;


@@ 68,7 68,7 @@ buffer_tail(struct buffer *b)
}

struct buffer_line*
buffer_line(struct buffer *b, unsigned int i)
buffer_line(struct buffer *b, unsigned i)
{
	/* Return the buffer line indexed by i */



@@ 99,8 99,8 @@ buffer_line(struct buffer *b, unsigned int i)
	return &b->buffer_lines[BUFFER_MASK(i)];
}

unsigned int
buffer_line_rows(struct buffer_line *line, unsigned int w)
unsigned
buffer_line_rows(struct buffer_line *line, unsigned w)
{
	/* Return the number of times a buffer line will wrap within w columns */



@@ 172,7 172,7 @@ buffer_newline(
	}
}

float
unsigned
buffer_scrollback_status(struct buffer *b)
{
	/* Return the buffer scrollback status as a number between [0, 100] */


@@ 180,7 180,7 @@ buffer_scrollback_status(struct buffer *b)
	if (buffer_line(b, b->scrollback) == buffer_head(b))
		return 0;

	return (float)(b->head - b->scrollback) / (float)(buffer_size(b));
	return (100 * (float)(b->head - b->scrollback) / (float)(buffer_size(b)));
}

void


@@ 199,7 199,7 @@ buffer_line_split(
	unsigned cols,
	unsigned pad)
{
	unsigned _head_w = sizeof(" HH:MM   "VERTICAL_SEPARATOR" ");
	unsigned _head_w = sizeof(" HH:MM  ");

	if (BUFFER_PADDING)
		_head_w += pad;


@@ 216,5 216,5 @@ buffer_line_split(
		*head_w = _head_w;

	if (text_w)
		*text_w = cols - _head_w + 1;
		*text_w = cols - _head_w;
}

M src/components/buffer.h => src/components/buffer.h +12 -12
@@ 35,34 35,34 @@ struct buffer_line
	size_t text_len;
	time_t time;
	struct {
		unsigned int colour; /* Cached colour of `from` text */
		unsigned int rows;   /* Cached number of rows occupied when wrapping on w columns */
		unsigned int w;      /* Cached width for rows */
		unsigned int initialized : 1;
		unsigned colour; /* Cached colour of `from` text */
		unsigned rows;   /* Cached number of rows occupied when wrapping on w columns */
		unsigned w;      /* Cached width for rows */
		unsigned initialized : 1;
	} cached;
};

struct buffer
{
	unsigned int head;
	unsigned int tail;
	unsigned int scrollback; /* Index of the current line between [tail, head) for scrollback */
	unsigned head;
	unsigned tail;
	unsigned scrollback; /* Index of the current line between [tail, head) for scrollback */
	size_t pad;              /* Pad 'from' when printing to be at least this wide */
	struct buffer_line buffer_lines[BUFFER_LINES_MAX];
};

float buffer_scrollback_status(struct buffer*);
unsigned buffer_scrollback_status(struct buffer*);

int buffer_page_back(struct buffer*, unsigned int, unsigned int);
int buffer_page_forw(struct buffer*, unsigned int, unsigned int);
int buffer_page_back(struct buffer*, unsigned, unsigned);
int buffer_page_forw(struct buffer*, unsigned, unsigned);

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

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);
struct buffer_line* buffer_line(struct buffer*, unsigned);

void buffer_newline(
	struct buffer*,

M src/components/channel.h => src/components/channel.h +2 -2
@@ 40,8 40,8 @@ struct channel
	struct mode_str chanmodes_str;
	struct server *server;
	struct user_list users;
	unsigned int parted : 1;
	unsigned int joined : 1;
	unsigned parted : 1;
	unsigned joined : 1;
	char _[];
};


M src/components/input.c => src/components/input.c +20 -14
@@ 2,6 2,7 @@

#include "src/utils/utils.h"

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



@@ 82,15 83,21 @@ input_delete_forw(struct input *inp)
int
input_insert(struct input *inp, const char *c, size_t count)
{
	/* TODO: may want to discard control characters */
	if (input_text_isfull(inp))
		return 0;

	while (!input_text_isfull(inp) && count--) {

	size_t i = count;
		if (iscntrl(*c))
			inp->text[inp->head++] = ' ';

	while (!input_text_isfull(inp) && i--) {
		inp->text[inp->head++] = *c++;
		if (isprint(*c))
			inp->text[inp->head++] = *c;

		c++;
	}

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

int


@@ 279,24 286,23 @@ input_frame(struct input *inp, char *buf, uint16_t max)
uint16_t
input_write(struct input *inp, char *buf, uint16_t max, uint16_t pos)
{
	uint16_t i = pos,
	         j = 0;
	uint16_t buf_len = 0;

	while (max > 1 && i < inp->head) {
		buf[j++] = inp->text[i++];
	while (max > 1 && pos < inp->head) {
		buf[buf_len++] = inp->text[pos++];
		max--;
	}

	i = inp->tail;
	pos = inp->tail;

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

	buf[j] = 0;
	buf[buf_len] = 0;

	return j;
	return buf_len;
}

static char*

M src/components/mode.c => src/components/mode.c +1 -1
@@ 664,7 664,7 @@ mode_cfg_modes(struct mode_cfg *cfg, const char *str)
{
	/* Parse and configure MODES, valid values are numeric strings [1-99] */

	unsigned int modes = 0;
	unsigned modes = 0;

	for (; modes < 100 && *str; str++) {
		if (isdigit(*str))

M src/components/mode.h => src/components/mode.h +1 -1
@@ 94,7 94,7 @@ struct mode

struct mode_cfg
{
	unsigned int MODES;    /* Numeric 005 MODES */
	unsigned MODES;        /* Numeric 005 MODES */
	struct mode chanmodes; /* Numeric 004 chanmodes string */
	struct mode usermodes; /* Numeric 004 usermodes string */
	struct

M src/components/server.c => src/components/server.c +91 -41
@@ 155,6 155,97 @@ server_free(struct server *s)
	free(s);
}

int
server_set_chans(struct server *s, const char *str)
{
	char *dup;
	char *p1;
	char *p2;
	size_t n_chans = 0;

	p2 = dup = strdup(str);

	do {
		n_chans++;

		p1 = p2;
		p2 = strchr(p2, ',');

		if (p2)
			*p2++ = 0;

		if (!irc_ischan(p1) && !irc_isnick(p1)) {
			free(dup);
			return -1;
		}
	} while (p2);

	for (const char *chan = dup; n_chans; n_chans--) {

		struct channel *c;

		if (channel_list_get(&s->clist, chan, s->casemapping))
			continue;

		if (irc_ischan(chan))
			c = channel(chan, CHANNEL_T_CHANNEL);
		else
			c = channel(chan, CHANNEL_T_PRIVATE);

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

		chan = strchr(chan, 0) + 1;
	}

	free(dup);

	return 0;
}

int
server_set_nicks(struct server *s, const char *str)
{
	char *dup;
	char *p1;
	char *p2;
	size_t n_nicks = 0;

	p2 = dup = strdup(str);

	do {
		n_nicks++;

		p1 = p2;
		p2 = strchr(p2, ',');

		if (p2)
			*p2++ = 0;

		if (!irc_isnick(p1)) {
			free(dup);
			return -1;
		}
	} while (p2);

	free((void *)s->nicks.base);
	free((void *)s->nicks.set);

	s->nicks.next = 0;
	s->nicks.size = n_nicks;
	s->nicks.base = dup;

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

	for (const char **set = s->nicks.set; n_nicks; n_nicks--, set++) {
		*set = dup;
		dup = strchr(dup, 0) + 1;
	}

	return 0;
}

void
server_set_004(struct server *s, char *str)
{


@@ 221,47 312,6 @@ server_set_005(struct server *s, char *str)
	}
}

int
server_set_nicks(struct server *s, const char *nicks)
{
	char *p1, *p2, *base;
	size_t n = 0;

	p2 = base = strdup(nicks);

	do {
		n++;

		p1 = p2;
		p2 = strchr(p2, ',');

		if (p2)
			*p2++ = 0;

		if (!irc_isnick(p1)) {
			free(base);
			return -1;
		}
	} while (p2);

	free((void *)s->nicks.base);
	free((void *)s->nicks.set);

	s->nicks.next = 0;
	s->nicks.size = n;
	s->nicks.base = base;

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

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

	return 0;
}

static int
server_cmp(const struct server *s, const char *host, const char *port)
{

M src/components/server.h => src/components/server.h +2 -1
@@ 64,9 64,10 @@ 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*);

int server_set_chans(struct server*, const char*);
int server_set_nicks(struct server*, const char*);
void server_set_004(struct server*, char*);
void server_set_005(struct server*, char*);
int server_set_nicks(struct server*, const char*);

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

M src/components/user.h => src/components/user.h +1 -1
@@ 24,7 24,7 @@ struct user
struct user_list
{
	TREE_HEAD(user);
	unsigned int count;
	unsigned count;
};

enum user_err user_list_add(struct user_list*, enum casemapping, const char*, struct mode);

M src/draw.c => src/draw.c +409 -338
@@ 7,7 7,7 @@
#include "src/state.h"
#include "src/utils/utils.h"

#include <alloca.h>
#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>


@@ 16,35 16,34 @@
/* Control sequence initiator */
#define CSI "\x1b["

#define ATTR_FG(X)   CSI "38;5;"#X"m"
#define ATTR_BG(X)   CSI "48;5;"#X"m"
#define ATTR_RESET   CSI "0m"

#define CLEAR_FULL   CSI "2J"
#define CLEAR_LINE   CSI "2K"

#define C_MOVE(X, Y) CSI ""#X";"#Y"H"
#define C_SAVE       CSI "s"
#define C_RESTORE    CSI "u"
#define ATTR_BG(X)         CSI "48;5;"#X"m"
#define ATTR_FG(X)         CSI "38;5;"#X"m"
#define ATTR_RESET         CSI "0m"
#define ATTR_RESET_BG      CSI "49m"
#define ATTR_RESET_FG      CSI "39m"
#define CLEAR_FULL         CSI "2J"
#define CLEAR_LINE         CSI "2K"
#define CURSOR_POS(X, Y)   CSI #X";"#Y"H"
#define CURSOR_POS_RESTORE CSI "u"
#define CURSOR_POS_SAVE    CSI "s"

/* Minimum rows or columns to safely draw */
#define COLS_MIN 5
#define ROWS_MIN 5

/* Size of a full colour string for purposes of pre-formating text to print */
#define COLOUR_SIZE sizeof(ATTR_RESET ATTR_FG(255) ATTR_BG(255))

#ifndef BUFFER_PADDING
#define BUFFER_PADDING 1
#endif

#define UTF8_CONT(C) (((unsigned char)(C) & 0xC0) == 0x80)

/* Terminal coordinate row/column boundaries (inclusive)
 * for objects being drawn. The origin for terminal
 * coordinates is in the top left, indexed from 1
 *
 *   \ c0     cN
 *   \ c1     cN
 *    +---------+
 *  r0|         |
 *  r1|         |
 *    |         |
 *    |         |
 *  rN|         |


@@ 53,41 52,69 @@

struct coords
{
	unsigned c0;
	unsigned c1;
	unsigned cN;
	unsigned r0;
	unsigned r1;
	unsigned rN;
};

struct draw_state
static struct
{
	union {
		struct {
			unsigned buffer : 1;
			unsigned input  : 1;
			unsigned nav    : 1;
			unsigned status : 1;
			unsigned separators : 1;
			unsigned buffer     : 1;
			unsigned input      : 1;
			unsigned nav        : 1;
			unsigned status     : 1;
		};
		unsigned all;
	} bits;
	unsigned bell : 1;
};
} draw_state;

static struct coords coords(unsigned, unsigned, unsigned, unsigned);
static unsigned nick_col(char*);
static unsigned drawf(unsigned*, const char*, ...);

static void draw_bits(void);
static void draw_buffer(struct buffer*, struct coords);
static void draw_buffer_line(struct buffer_line*, struct coords, unsigned, unsigned, unsigned, unsigned);
static void draw_input(struct input*, struct coords);
static void draw_nav(struct channel*);
static void draw_separators(void);
static void draw_status(struct channel*);

static char* draw_colour(int, int);
static int draw_fmt(char**, size_t*, size_t*, int, const char*, ...);
static unsigned nick_col(char*);
static void check_coords(struct coords);
static void draw_attr_bg(int);
static void draw_attr_fg(int);
static void draw_attr_reset(void);
static void draw_char(int);
static void draw_clear_full(void);
static void draw_clear_line(void);
static void draw_cursor_pos(int, int);
static void draw_cursor_pos_restore(void);
static void draw_cursor_pos_save(void);

static int actv_colours[ACTIVITY_T_SIZE] = ACTIVITY_COLOURS
static int bg_last = -1;
static int fg_last = -1;
static int nick_colours[] = NICK_COLOURS
static struct draw_state draw_state;

static int drawing;

void
draw_init(void)
{
	drawing = 1;
}

void
draw_term(void)
{
	drawing = 0;

	draw(DRAW_CLEAR);
}

void
draw(enum draw_bit bit)


@@ 117,8 144,8 @@ draw(enum draw_bit bit)
			draw_state.bits.all = -1;
			break;
		case DRAW_CLEAR:
			printf(ATTR_RESET);
			printf(CLEAR_FULL);
			draw_attr_reset();
			draw_clear_full();
			break;
		default:
			fatal("unknown draw bit");


@@ 128,53 155,57 @@ draw(enum draw_bit bit)
static void
draw_bits(void)
{
	if (!drawing)
		return;

	if (draw_state.bell && BELL_ON_PINGED)
		putchar('\a');

	if (!draw_state.bits.all)
		return;

	struct coords coords;
	struct channel *c = current_channel();

	if (state_cols() < COLS_MIN || state_rows() < ROWS_MIN) {
		printf(CLEAR_FULL C_MOVE(1, 1) "rirc");
		fflush(stdout);
		return;
	unsigned cols = state_cols();
	unsigned rows = state_rows();

	draw_cursor_pos_save();

	if (cols < COLS_MIN || rows < ROWS_MIN) {
		draw_clear_full();
		draw_cursor_pos(1, 1);
		goto flush;
	}

	printf(C_SAVE);
	if (draw_state.bits.separators) {
		draw_attr_reset();
		draw_separators();
	}

	if (draw_state.bits.buffer) {
		printf(ATTR_RESET);
		coords.c0 = 1;
		coords.cN = state_cols();
		coords.r0 = 3;
		coords.rN = state_rows() - 2;
		draw_buffer(&c->buffer, coords);
		draw_attr_reset();
		draw_buffer(&c->buffer, coords(1, cols, 3, rows - 2));
	}

	if (draw_state.bits.input) {
		printf(ATTR_RESET);
		coords.c0 = 1;
		coords.cN = state_cols();
		coords.r0 = state_rows();
		coords.rN = state_rows();
		draw_input(&c->input, coords);
		draw_attr_reset();
		draw_input(&c->input, coords(1, cols, rows, rows));
	}

	if (draw_state.bits.nav) {
		printf(ATTR_RESET);
		draw_attr_reset();
		draw_nav(c);
	}

	if (draw_state.bits.status) {
		printf(ATTR_RESET);
		draw_attr_reset();
		draw_status(c);
	}

	printf(ATTR_RESET);
	printf(C_RESTORE);
flush:

	draw_attr_reset();
	draw_cursor_pos_restore();

	fflush(stdout);
}


@@ 192,7 223,7 @@ draw_buffer(struct buffer *b, struct coords coords)
	 * Rows are numbered from the top down, 1 to term_rows, so for term_rows = N,
	 * the drawable area for the buffer is bounded [r3, rN-2]:
	 *      __________________________
	 * r0   |         (nav)          |
	 * r1   |         (nav)          |
	 * r2   |------------------------|
	 * r3   |    ::buffer start::    |
	 *      |                        |


@@ 217,21 248,19 @@ draw_buffer(struct buffer *b, struct coords coords)
	 *    is encountered
	 */

	check_coords(coords);

	unsigned row,
	         row_count = 0,
	         row_total = coords.rN - coords.r0 + 1;

	unsigned col_total = coords.cN - coords.c0 + 1;

	unsigned buffer_i = b->scrollback,
	         head_w,
	         text_w;
	unsigned buffer_i = b->scrollback;
	unsigned col_total = coords.cN - coords.c1 + 1;
	unsigned row;
	unsigned row_count = 0;
	unsigned row_total = coords.rN - coords.r1 + 1;
	unsigned head_w;
	unsigned text_w;

	/* Clear the buffer area */
	for (row = coords.r0; row <= coords.rN; row++)
		printf(C_MOVE(%d, 1) CLEAR_LINE, row);
	for (row = coords.r1; row <= coords.rN; row++) {
		draw_cursor_pos(row, 1);
		draw_clear_line();
	}

	struct buffer_line *line = buffer_line(b, buffer_i);



@@ 271,7 300,7 @@ draw_buffer(struct buffer *b, struct coords coords)
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);

		coords.r0 += buffer_line_rows(line, text_w) - (row_count - row_total);
		coords.r1 += buffer_line_rows(line, text_w) - (row_count - row_total);

		if (line == head)
			return;


@@ 280,7 309,7 @@ draw_buffer(struct buffer *b, struct coords coords)
	}

	/* Draw all remaining lines */
	while (coords.r0 <= coords.rN) {
	while (coords.r1 <= coords.rN) {

		buffer_line_split(line, &head_w, &text_w, col_total, b->pad);



@@ 293,7 322,7 @@ draw_buffer(struct buffer *b, struct coords coords)
			BUFFER_PADDING ? (b->pad - line->from_len) : 0
		);

		coords.r0 += buffer_line_rows(line, text_w);
		coords.r1 += buffer_line_rows(line, text_w);

		if (line == head)
			return;


@@ 302,9 331,6 @@ draw_buffer(struct buffer *b, struct coords coords)
	}
}

/* FIXME: works except when it doesn't.
 *
 * Fails when line headers are very long compared to text. tests/draw.c needed */
static void
draw_buffer_line(
		struct buffer_line *line,


@@ 314,12 340,11 @@ draw_buffer_line(
		unsigned skip,
		unsigned pad)
{
	check_coords(coords);
	char *p1 = line->text;
	char *p2 = line->text + line->text_len;

	char *print_p1,
	     *print_p2,
	     *p1 = line->text,
	     *p2 = line->text + line->text_len;
	unsigned head_col = coords.c1;
	unsigned text_col = coords.c1 + head_w;

	if (!line->cached.initialized) {
		/* Initialize static cached properties of drawn lines */


@@ 329,178 354,175 @@ draw_buffer_line(

	if (skip == 0) {

		/* Print the line header
		 *
		 * Since formatting codes don't occupy columns, enough space
		 * should be allocated for all such sequences
		 * */
		char header[head_w + COLOUR_SIZE * 4 + 1];
		char *header_ptr = header;
		/* Print the line header */

		size_t buff_n = sizeof(header) - 1,
		       text_n = head_w - 1;
		char buf_h[3] = {0};
		char buf_m[3] = {0};
		int from_bg;
		int from_fg;
		unsigned head_cols = head_w;

		struct tm *line_tm = localtime(&line->time);
		struct tm *tm = localtime(&line->time);

		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
				draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
			goto print_header;
		(void) snprintf(buf_h, sizeof(buf_h), "%02d", tm->tm_hour);
		(void) snprintf(buf_m, sizeof(buf_h), "%02d", tm->tm_min);

		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 1,
				" %02d:%02d ", line_tm->tm_hour, line_tm->tm_min))
			goto print_header;
		draw_cursor_pos(coords.r1, head_col);

		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 1,
				"%*s", pad, ""))
			goto print_header;
		if (!drawf(&head_cols, " %b%f%s:%s%a ",
				BUFFER_LINE_HEADER_BG,
				BUFFER_LINE_HEADER_FG,
				buf_h,
				buf_m))
			goto print_text;

		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0, ATTR_RESET))
			goto print_header;
		while (pad--) {
			if (!drawf(&head_cols, "%s", " "))
				goto print_text;
		}

		switch (line->type) {
			case BUFFER_LINE_OTHER:
			case BUFFER_LINE_SERVER_INFO:
			case BUFFER_LINE_SERVER_ERROR:
			case BUFFER_LINE_JOIN:
			case BUFFER_LINE_NICK:
			case BUFFER_LINE_PART:
			case BUFFER_LINE_QUIT:
				if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1)))
					goto print_header;
				break;

			case BUFFER_LINE_CHAT:
				if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						draw_colour(line->cached.colour, -1)))
					goto print_header;
				from_bg = BUFFER_LINE_HEADER_BG;
				from_fg = line->cached.colour;
				break;

			case BUFFER_LINE_PINGED:
				if (!draw_fmt(&header_ptr, &buff_n, &text_n, 0,
						draw_colour(BUFFER_LINE_HEADER_FG_PINGED, BUFFER_LINE_HEADER_BG_PINGED)))
					goto print_header;
				from_bg = BUFFER_LINE_HEADER_BG_PINGED;
				from_fg = BUFFER_LINE_HEADER_FG_PINGED;
				break;
			default:
				from_bg = BUFFER_LINE_HEADER_BG;
				from_fg = BUFFER_LINE_HEADER_FG;
				break;

			case BUFFER_LINE_T_SIZE:
				fatal("Invalid line type");
		}

		if (!draw_fmt(&header_ptr, &buff_n, &text_n, 1,
				"%s", line->from))
			goto print_header;

print_header:
		/* Print the line header */
		printf(C_MOVE(%d, 1) "%s " ATTR_RESET, coords.r0, header);
		if (!drawf(&head_cols, "%b%f%s%a ",
				from_bg,
				from_fg,
				line->from))
			goto print_text;
	}

print_text:

	while (skip--)
		irc_strwrap(text_w, &p1, p2);

	unsigned text_bg = BUFFER_TEXT_BG;
	unsigned text_fg = BUFFER_TEXT_FG;

	if (strlen(QUOTE_LEADER) && line->type == BUFFER_LINE_CHAT) {
		if (!strncmp(line->text, QUOTE_LEADER, strlen(QUOTE_LEADER))) {
			text_bg = QUOTE_TEXT_BG;
			text_fg = QUOTE_TEXT_FG;
		}
	}

	do {
		char *sep = " "VERTICAL_SEPARATOR" ";
		unsigned text_cols = text_w;

		draw_cursor_pos(coords.r1, text_col);

		if ((coords.cN - coords.c0) >= sizeof(*sep) + text_w) {
			printf(C_MOVE(%d, %d), coords.r0, (int)(coords.cN - (sizeof(*sep) + text_w + 1)));
			fputs(draw_colour(BUFFER_LINE_HEADER_FG_NEUTRAL, -1), stdout);
			fputs(sep, stdout);
		if (!drawf(&text_cols, "%b%f%s%a ",
				BUFFER_LINE_HEADER_BG,
				BUFFER_LINE_HEADER_FG,
				SEP_VERT)) {
			coords.r1++;
			continue;
		}

		if (*p1) {
			printf(C_MOVE(%d, %d), coords.r0, head_w);
			const char *text_p1 = p1;
			const char *text_p2 = irc_strwrap(text_cols, &p1, p2);

			print_p1 = p1;
			print_p2 = irc_strwrap(text_w, &p1, p2);
			draw_attr_bg(text_bg);
			draw_attr_fg(text_fg);

			fputs(draw_colour(line->text[0] == QUOTE_CHAR
					? BUFFER_LINE_TEXT_FG_GREEN
					: BUFFER_LINE_TEXT_FG_NEUTRAL,
					-1),
				stdout);
			for (unsigned i = 0; i < (text_p2 - text_p1); i++)
				draw_char(text_p1[i]);

			printf("%.*s", (int)(print_p2 - print_p1), print_p1);
			draw_attr_reset();
		}

		coords.r0++;
		coords.r1++;

	} while (*p1 && coords.r0 <= coords.rN);
	} while (*p1 && coords.r1 <= coords.rN);
}

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

	check_coords(coords);
	unsigned cols = state_cols();

	unsigned cols_t = coords.cN - coords.c0 + 1,
	         cursor = coords.c0;
	draw_cursor_pos(2, 1);

	printf(C_MOVE(%d, 1) CLEAR_LINE, coords.rN);
	printf(C_SAVE);
	draw_attr_bg(SEP_BG);
	draw_attr_fg(SEP_FG);

	/* Insufficient columns for meaningful input drawing */
	if (cols_t < 3)
		return;
	while (drawf(&cols, "%s", SEP_HORZ))
		;
}

	char input[cols_t + COLOUR_SIZE * 2 + 1];
	char *input_ptr = input;
static void
draw_input(struct input *inp, struct coords coords)
{
	/* Draw the input line, or the current action message */

	size_t buff_n = sizeof(input) - 1,
	       text_n = cols_t;
	const char *action;
	unsigned cols = coords.cN - coords.c1 + 1;
	unsigned cursor_row = coords.r1;
	unsigned cursor_col = coords.cN;

	if (sizeof(INPUT_PREFIX)) {
	draw_cursor_pos(coords.r1, coords.c1);

		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
				"%s", draw_colour(INPUT_PREFIX_FG, INPUT_PREFIX_BG)))
			goto print_input;
	if ((action = action_message())) {
		if (!drawf(&cols, "%b%f%s%b%f-- %s --",
				INPUT_PREFIX_BG,
				INPUT_PREFIX_FG,
				INPUT_PREFIX,
				ACTION_BG,
				ACTION_FG,
				action))
			goto cursor;

		cursor = coords.c0 + sizeof(INPUT_PREFIX) - 1;
		cursor_col = coords.cN - coords.c1 - cols + 3;
	} else {
		char input[INPUT_LEN_MAX];
		unsigned cursor_pre;
		unsigned cursor_inp;

		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 1,
		if (!drawf(&cols, "%b%f%s",
				INPUT_PREFIX_BG,
				INPUT_PREFIX_FG,
				INPUT_PREFIX))
			goto print_input;
	}

	if (action_message()) {
			goto cursor;

		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
				"%s", draw_colour(ACTION_FG, ACTION_BG)))
			goto print_input;
		cursor_pre = coords.cN - coords.c1 - cols + 1;
		cursor_inp = input_frame(inp, input, cols);

		cursor = coords.cN;
		if (!drawf(&cols, "%b%f%s",
				INPUT_BG,
				INPUT_FG,
				input))
			goto cursor;

		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 1, "-- %s --", action_message()))
			goto print_input;
		cursor_col = cursor_pre + cursor_inp + 1;
	}

		cursor = cols_t - text_n + 1;
	draw_attr_reset();

	} else {
		if (!draw_fmt(&input_ptr, &buff_n, &text_n, 0,
				"%s", draw_colour(INPUT_FG, INPUT_BG)))
			goto print_input;
	while (cols--)
		draw_char(' ');

		cursor += input_frame(inp, input_ptr, text_n);
	}
cursor:

print_input:
	cursor_row = MIN(cursor_row, coords.rN);
	cursor_col = MIN(cursor_col, coords.cN);

	fputs(input, stdout);
	printf(C_MOVE(%d, %d), coords.rN, (cursor >= coords.c0 && cursor <= coords.cN) ? cursor : coords.cN);
	printf(C_SAVE);
	draw_cursor_pos(cursor_row, cursor_col);
	draw_cursor_pos_save();
}

/* TODO
 *
 * | [server-name[:port]] *#chan |
 *
 * - Disconnected/parted channels are printed (#chan)
 * - Servers with non-standard ports are printed: server-name:port
 * - Channels that won't fit are printed at a minimum: #...
 *     - eg: | ...chan #chan2 chan3 |   Right printing
 *           | #chan1 #chan2 #ch... |   Left printing
 * */
static void
draw_nav(struct channel *c)
{


@@ 511,7 533,8 @@ draw_nav(struct channel *c)
	 *  - The nav is kept framed between the first and last channels
	 */

	printf(C_MOVE(1, 1) CLEAR_LINE);
	draw_cursor_pos(1, 1);
	draw_clear_line();

	static struct channel *frame_prev,
	                      *frame_next;


@@ 520,10 543,12 @@ draw_nav(struct channel *c)
	               *c_last = channel_get_last(),
	               *tmp;

	unsigned cols = state_cols();

	c->activity = ACTIVITY_DEFAULT;

	/* By default assume drawing starts towards the next channel */
	int colour, nextward = 1;
	int nextward = 1;

	size_t len, total_len = 0;



@@ 597,12 622,9 @@ draw_nav(struct channel *c)
	/* Draw coloured channel names, from frame to frame */
	for (tmp = frame_prev; ; tmp = channel_get_next(tmp)) {

		colour = (tmp == c) ? NAV_CURRENT_CHAN : actv_colours[tmp->activity];
		int fg = (tmp == c) ? NAV_CURRENT_CHAN : actv_colours[tmp->activity];

		if (fputs(draw_colour(colour, -1), stdout) < 0)
			break;

		if (printf(" %s ", tmp->name) < 0)
		if (!drawf(&cols, "%f %s ", fg, tmp->name))
			break;

		if (tmp == frame_next)


@@ 613,119 635,91 @@ draw_nav(struct channel *c)
static void
draw_status(struct channel *c)
{
	/* TODO: channel modes, channel type_flag, servermodes */

	/* server / private chat:
	 * |-[usermodes]-(ping)---...|
	/* server buffer:
	 *  -[usermodes]-(ping)-(scrollback)
	 *
	 * private buffer:
	 *  -[usermodes]-[priv]-(ping)-(scrollback)
	 *
	 * channel:
	 * |-[usermodes]-[chancount chantype chanmodes]/[priv]-(ping)---...|
	 * channel buffer:
	 *  -[usermodes]-[chantype chanmodes chancount]-(ping)-(scrollback)
	 */

	float sb;
	int ret;
	unsigned col = 0;
	#define STATUS_SEP_HORZ \
		"%b%f" SEP_HORZ "%b%f", SEP_BG, SEP_FG, STATUS_BG, STATUS_FG

	unsigned cols = state_cols();
	unsigned rows = state_rows();
	unsigned scrollback;

	/* Insufficient columns for meaningful status */
	if (cols < 3)
	if (!cols || !(rows > 1))
		return;

	printf(C_MOVE(2, 1));
	printf("%.*s", cols, (char *)(memset(alloca(cols), *HORIZONTAL_SEPARATOR, cols)));

	printf(C_MOVE(%d, 1) CLEAR_LINE, rows - 1);

	/* Print status to temporary buffer */
	char status_buff[cols + 1];

	memset(status_buff, 0, cols + 1);
	draw_cursor_pos(rows - 1, 1);

	/* -[usermodes] */
	if (c->server && *(c->server->mode_str.str)) {
		ret = snprintf(status_buff + col, cols - col + 1, "%s", HORIZONTAL_SEPARATOR "[+");
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;

		ret = snprintf(status_buff + col, cols - col + 1, "%s", c->server->mode_str.str);
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;

		ret = snprintf(status_buff + col, cols - col + 1, "%s", "]");
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "[+%s]", c->server->mode_str.str))
			return;
	}

	/* If private chat buffer:
	 * -[priv] */
	/* -[priv] */
	if (c->type == CHANNEL_T_PRIVATE) {
		ret = snprintf(status_buff + col, cols - col + 1, "%s", HORIZONTAL_SEPARATOR "[priv]");
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "[priv]"))
			return;
	}

	/* If IRC channel buffer:
	 * -[chancount chantype chanmodes] */
	/* -[chantype chanmodes chancount] */
	if (c->type == CHANNEL_T_CHANNEL) {

		ret = snprintf(status_buff + col, cols - col + 1,
				HORIZONTAL_SEPARATOR "[%d", c->users.count);
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;

		if (c->chanmodes.prefix) {
			ret = snprintf(status_buff + col, cols - col + 1, " %c", c->chanmodes.prefix);
			if (ret < 0 || (col += ret) >= cols)
				goto print_status;
		}

		if (*(c->chanmodes_str.str)) {
			ret = snprintf(status_buff + col, cols - col + 1, " +%s", c->chanmodes_str.str);
			if (ret < 0 || (col += ret) >= cols)
				goto print_status;
		}

		ret = snprintf(status_buff + col, cols - col + 1, "%s", "]");
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "[%c %s %u]",
				c->chanmodes.prefix,
				c->chanmodes_str.str,
				c->users.count))
			return;
	}

	/* -(ping) */
	if (c->server && c->server->ping) {
		ret = snprintf(status_buff + col, cols - col + 1,
				HORIZONTAL_SEPARATOR "(%llds)", (long long) c->server->ping);
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "(%us)", c->server->ping))
			return;
	}

	/* -(scrollback%) */
	if ((sb = buffer_scrollback_status(&c->buffer))) {
		ret = snprintf(status_buff + col, cols - col + 1,
				HORIZONTAL_SEPARATOR "(%02d%%)", (int)(sb * 100));
		if (ret < 0 || (col += ret) >= cols)
			goto print_status;
	/* -(scrollback) */
	if ((scrollback = buffer_scrollback_status(&c->buffer))) {
		if (!drawf(&cols, STATUS_SEP_HORZ))
			return;
		if (!drawf(&cols, "(%u%s)", scrollback, "%"))
			return;
	}

print_status:

	fputs(status_buff, stdout);
	draw_attr_bg(SEP_BG);
	draw_attr_fg(SEP_FG);

	/* Trailing separator */
	while (col++ < cols)
		printf(HORIZONTAL_SEPARATOR);
	while (drawf(&cols, "%s", SEP_HORZ))
		;
}

static void
check_coords(struct coords coords)
static struct coords
coords(unsigned c1, unsigned cN, unsigned r1, unsigned rN)
{
	/* Check coordinate validity before drawing, ensure at least one row, column */
	unsigned cols = state_cols();
	unsigned rows = state_rows();

	if (!c1 || c1 > cN || cN > cols)
		fatal("Invalid coordinates: cols: %u %u %u", cols, c1, cN);

	if (coords.r0 > coords.rN)
		fatal("row coordinates invalid (%u > %u)", coords.r0, coords.rN);
	if (!r1 || r1 > rN || rN > rows)
		fatal("Invalid coordinates: rows: %u %u %u", rows, r1, rN);

	if (coords.c0 > coords.cN)
		fatal("col coordinates invalid (%u > %u)", coords.c0, coords.cN);
	return (struct coords) { .c1 = c1, .cN = cN, .r1 = r1, .rN = rN };
}

static unsigned


@@ 739,74 733,151 @@ nick_col(char *nick)
	return nick_colours[colour % sizeof(nick_colours) / sizeof(nick_colours[0])];
}

static char*
draw_colour(int fg, int bg)
static unsigned
drawf(unsigned *cols_p, const char *fmt, ...)
{
	/* Set terminal foreground and background colours to a value [0, 255],
	 * or reset colour if given anything else */

	static char buf[COLOUR_SIZE + 1] = ATTR_RESET;
	size_t len = sizeof(ATTR_RESET) - 1;
	int ret = 0;
	/* Draw formatted text up to a given number of
	 * columns. Returns number of unused columns.
	 *
	 *  %a -- attribute reset
	 *  %b -- set background colour attribute
	 *  %f -- set foreground colour attribute
	 *  %c -- output char
	 *  %d -- output signed integer
	 *  %u -- output unsigned integer
	 *  %s -- output string
	 */

	if (fg >= 0 && fg <= 255) {
		if ((ret = snprintf(buf + len, sizeof(buf) - len, ATTR_FG(%d), fg)) < 0)
			buf[len] = 0;
		else
			len += ret;
	char buf[64];
	char c;
	va_list arg;
	unsigned cols;

	if (!(cols = *cols_p))
		return 0;

	va_start(arg, fmt);

	while (cols && (c = *fmt++)) {
		if (c == '%') {
			switch ((c = *fmt++)) {
				case 'a':
					draw_attr_reset();
					break;
				case 'b':
					draw_attr_bg(va_arg(arg, int));
					break;
				case 'f':
					draw_attr_fg(va_arg(arg, int));
					break;
				case 'c':
					draw_char(va_arg(arg, int));
					cols--;
					break;
				case 'd':
					(void) snprintf(buf, sizeof(buf), "%d", va_arg(arg, int));
					cols -= (unsigned) printf("%.*s", cols, buf);
					break;
				case 'u':
					(void) snprintf(buf, sizeof(buf), "%u", va_arg(arg, unsigned));
					cols -= (unsigned) printf("%.*s", cols, buf);
					break;
				case 's':
					for (const char *str = va_arg(arg, const char*); *str && cols; cols--) {
						do {
							draw_char(*str++);
						} while (UTF8_CONT(*str));
					}
					break;
				case '%':
				default:
					fatal("unknown drawf format character '%c'", c);
			}
		} else {
			cols--;
			draw_char(c);
			while (UTF8_CONT(*fmt))
				draw_char(*fmt++);
		}
	}

	if (bg >= 0 && bg <= 255) {
		if ((snprintf(buf + len, sizeof(buf) - len, ATTR_BG(%d), bg)) < 0)
			buf[len] = 0;
	}
	va_end(arg);

	return buf;
	return (*cols_p = cols);
}

static int
draw_fmt(char **ptr, size_t *buff_n, size_t *text_n, int txt, const char *fmt, ...)
static void
draw_attr_bg(int bg)
{
	/* Write formatted text to a buffer for purposes of preparing an object to be drawn
	 * to the terminal.
	 *
	 * Calls to this function should distinguish between formatting and printed text
	 * with the txt flag.
	 *
	 *  - ptr    : pointer to location in buffer being printed to
	 *  - buff_n : remaining bytes available in buff
	 *  - text_n : remaining columns available for text
	 *  - txt    : flag set true if bytes being written are printable text
	 *
	 *  returns 0 on error, or if no more prints to this buffer can occur
	 */
	if (bg == -1)
		printf(ATTR_RESET_BG);

	int ret;
	va_list ap;
	if (bg >= 0 && bg <= 255)
		printf(ATTR_BG(%d), bg);

	va_start(ap, fmt);
	ret = vsnprintf(*ptr, *buff_n, fmt, ap);
	va_end(ap);
	bg_last = bg;
}

	if (ret < 0)
		return (**ptr = 0);
static void
draw_attr_fg(int fg)
{
	if (fg == -1)
		printf(ATTR_RESET_FG);

	size_t _ret = (size_t) ret;
	if (fg >= 0 && fg <= 255)
		printf(ATTR_FG(%d), fg);

	if (!txt && _ret >= *buff_n)
		return (**ptr = 0);
	fg_last = fg;
}

	if (txt) {
		if (*text_n > _ret)
			*text_n -= _ret;
		else {
			*ptr += *text_n;
			**ptr = 0;
			return (*text_n = 0);
		}
static void
draw_attr_reset(void)
{
	printf(ATTR_RESET);
}

static void
draw_clear_full(void)
{
	printf(CLEAR_FULL);
}

static void
draw_clear_line(void)
{
	printf(CLEAR_LINE);
}

static void
draw_char(int c)
{
	if (iscntrl(c)) {
		int ctrl_bg_last = bg_last;
		int ctrl_fg_last = fg_last;
		draw_attr_bg(CTRL_BG);
		draw_attr_fg(CTRL_FG);
		putchar((c | 0x40));
		draw_attr_bg(ctrl_bg_last);
		draw_attr_fg(ctrl_fg_last);
	} else {
		putchar(c);
	}
}

	*ptr += _ret;
static void
draw_cursor_pos(int row, int col)
{
	printf(CURSOR_POS(%d, %d), row, col);
}

static void
draw_cursor_pos_save(void)
{
	printf(CURSOR_POS_SAVE);
}

	return 1;
static void
draw_cursor_pos_restore(void)
{
	printf(CURSOR_POS_RESTORE);
}

M src/draw.h => src/draw.h +3 -0
@@ 14,6 14,9 @@ enum draw_bit
	DRAW_CLEAR,  /* clear the terminal */
};

void draw_init(void);
void draw_term(void);

void draw(enum draw_bit);

#endif

M src/handlers/irc_recv.c => src/handlers/irc_recv.c +2 -31
@@ 1037,7 1037,6 @@ recv_notice(struct server *s, struct irc_message *m)

	char *message;
	char *target;
	int urgent = 0;
	struct channel *c;

	if (!m->from)


@@ 1055,38 1054,10 @@ recv_notice(struct server *s, struct irc_message *m)
	if (IS_CTCP(message))
		return ctcp_response(s, m->from, target, message);

	if (!strcmp(target, "*")) {
	if (!(c = channel_list_get(&(s->clist), m->from, s->casemapping)))
		c = s->channel;
	} else if (!strcmp(target, s->nick)) {

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

		if (c != current_channel())
			urgent = 1;

	} else if ((c = channel_list_get(&s->clist, target, s->casemapping)) == NULL) {
		failf(s, "NOTICE: channel '%s' not found", target);
	}

	if (irc_pinged(s->casemapping, message, s->nick)) {

		if (c != current_channel())
			urgent = 1;

		newlinef(c, BUFFER_LINE_PINGED, m->from, "%s", message);
	} else {
		newlinef(c, BUFFER_LINE_CHAT, m->from, "%s", message);
	}

	if (urgent) {
		c->activity = ACTIVITY_PINGED;
		draw(DRAW_BELL);
		draw(DRAW_NAV);
	}
	newlinef(c, BUFFER_LINE_CHAT, m->from, "%s", message);

	return 0;
}

M src/rirc.c => src/rirc.c +112 -85
@@ 13,18 13,17 @@
#include <string.h>
#include <unistd.h>

#define MAX_CLI_SERVERS 16
#define MAX_CLI_SERVERS 64

#define arg_error(...) \
	do { fprintf(stderr, "%s ", runtime_name); \
	     fprintf(stderr, __VA_ARGS__); \
	     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 int parse_args(int, char**);
static const char* rirc_opt_str(char);
static const char* rirc_pw_name(void);
static int rirc_parse_args(int, char**);

#ifdef CA_CERT_PATH
const char *ca_cert_path = CA_CERT_PATH;


@@ 32,10 31,10 @@ const char *ca_cert_path = CA_CERT_PATH;
#error "CA_CERT_PATH required"
#endif

#ifdef DEFAULT_NICK_SET
const char *default_nick_set = DEFAULT_NICK_SET;
#ifdef DEFAULT_NICKS
const char *default_nicks = DEFAULT_NICKS;
#else
const char *default_nick_set;
const char *default_nicks;
#endif

#ifdef DEFAULT_USERNAME


@@ 90,7 89,7 @@ static const char *const rirc_version =
#endif

static const char*
opt_arg_str(char c)
rirc_opt_str(char c)
{
	switch (c) {
		case 's': return "-s/--server";


@@ 111,9 110,9 @@ opt_arg_str(char c)
}

static const char*
getpwuid_pw_name(void)
rirc_pw_name(void)
{
	static struct passwd *passwd;
	static const struct passwd *passwd;

	errno = 0;



@@ 124,23 123,35 @@ getpwuid_pw_name(void)
}

static int
parse_args(int argc, char **argv)
rirc_parse_args(int argc, char **argv)
{
	int opt_c = 0,
	    opt_i = 0;
	int opt_c = 0;
	int opt_i = 0;

	size_t n_servers = 0;

	opterr = 0;
	struct cli_server {
		const char *host;
		const char *port;
		const char *pass;
		const char *username;
		const char *realname;
		const char *nicks;
		const char *chans;
		int ipv;
		int tls;
		int tls_vrfy;
		struct server *s;
	} cli_servers[MAX_CLI_SERVERS];

	struct option long_opts[] = {
		{"server",      required_argument, 0, 's'},
		{"port",        required_argument, 0, 'p'},
		{"pass",        required_argument, 0, 'w'},
		{"nicks",       required_argument, 0, 'n'},
		{"chans",       required_argument, 0, 'c'},
		{"username",    required_argument, 0, 'u'},
		{"realname",    required_argument, 0, 'r'},
		{"nicks",       required_argument, 0, 'n'},
		{"chans",       required_argument, 0, 'c'},
		{"help",        no_argument,       0, 'h'},
		{"version",     no_argument,       0, 'v'},
		{"ipv4",        no_argument,       0, '4'},


@@ 150,49 161,45 @@ parse_args(int argc, char **argv)
		{0, 0, 0, 0}
	};

	struct cli_server {
		const char *host;
		const char *port;
		const char *pass;
		const char *nicks;
		const char *chans;
		const char *username;
		const char *realname;
		int ipv;
		int tls;
		int tls_vrfy;
		struct server *s;
	} cli_servers[MAX_CLI_SERVERS];
	opterr = 0;

	while (0 < (opt_c = getopt_long(argc, argv, ":s:p:w:n:c:r:u:hv", long_opts, &opt_i))) {
	while (0 < (opt_c = getopt_long(argc, argv, ":s:p:w:r:u:n:c:hv", long_opts, &opt_i))) {

		switch (opt_c) {

			case 's': /* Connect to server */

				if (*optarg == '-')
				if (*optarg == '-') {
					arg_error("-s/--server requires an argument");
					return -1;
				}

				if (++n_servers == MAX_CLI_SERVERS)
				if (++n_servers == MAX_CLI_SERVERS) {
					arg_error("exceeded maximum number of servers (%d)", MAX_CLI_SERVERS);
					return -1;
				}

				cli_servers[n_servers - 1].host     = optarg;
				cli_servers[n_servers - 1].port     = NULL;
				cli_servers[n_servers - 1].pass     = NULL;
				cli_servers[n_servers - 1].nicks    = NULL;
				cli_servers[n_servers - 1].username = default_username;
				cli_servers[n_servers - 1].realname = default_realname;
				cli_servers[n_servers - 1].nicks    = default_nicks;
				cli_servers[n_servers - 1].chans    = NULL;
				cli_servers[n_servers - 1].username = NULL;
				cli_servers[n_servers - 1].realname = NULL;
				cli_servers[n_servers - 1].ipv      = IO_IPV_UNSPEC;
				cli_servers[n_servers - 1].tls      = IO_TLS_ENABLED;
				cli_servers[n_servers - 1].tls_vrfy = IO_TLS_VRFY_REQUIRED;
				break;

			#define CHECK_SERVER_OPTARG(OPT_C, REQ) \
				if ((REQ) && *optarg == '-') \
					arg_error("option '%s' requires an argument", opt_arg_str((OPT_C))); \
				if (n_servers == 0) \
					arg_error("option '%s' requires a server argument first", opt_arg_str((OPT_C)));
				if ((REQ) && *optarg == '-') { \
					arg_error("option '%s' requires an argument", rirc_opt_str((OPT_C))); \
					return -1; \
				} \
				if (n_servers == 0) { \
					arg_error("option '%s' requires a server argument first", rirc_opt_str((OPT_C))); \
					return -1; \
				}

			case 'p': /* Connect using port */
				CHECK_SERVER_OPTARG(opt_c, 1);


@@ 204,24 211,24 @@ parse_args(int argc, char **argv)
				cli_servers[n_servers - 1].pass = optarg;
				break;

			case 'n': /* Comma separated list of nicks to use */
			case 'u': /* Connect using username */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].nicks = optarg;
				cli_servers[n_servers - 1].username = optarg;
				break;

			case 'c': /* Comma separated list of channels to join */
			case 'r': /* Connect using realname */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].chans = optarg;
				cli_servers[n_servers - 1].realname = optarg;
				break;

			case 'u': /* Connect using username */
			case 'n': /* Comma separated list of nicks to use */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].username = optarg;
				cli_servers[n_servers - 1].nicks = optarg;
				break;

			case 'r': /* Connect using realname */
			case 'c': /* Comma separated list of channels to join */
				CHECK_SERVER_OPTARG(opt_c, 1);
				cli_servers[n_servers - 1].realname = optarg;
				cli_servers[n_servers - 1].chans = optarg;
				break;

			case '4': /* Connect using ipv4 only */


@@ 253,7 260,8 @@ parse_args(int argc, char **argv)
					cli_servers[n_servers -1].tls_vrfy = IO_TLS_VRFY_REQUIRED;
					break;
				}
				arg_error("option '--tls-verify' mode must be 'disabled', 'optional', or 'required'");
				arg_error("invalid option for '--tls-verify' '%s'", optarg);
				return -1;

			#undef CHECK_SERVER_OPTARG



@@ 266,29 274,23 @@ parse_args(int argc, char **argv)
				exit(EXIT_SUCCESS);

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

			case ':':
				arg_error("option '%s' requires an argument", opt_arg_str(optopt));
				arg_error("option '%s' requires an argument", rirc_opt_str(optopt));
				return -1;

			default:
				arg_error("unknown opt error");
				return -1;
		}
	}

	if (optind < argc)
	if (optind < argc) {
		arg_error("unused option '%s'", argv[optind]);

	if (!default_nick_set || !default_nick_set[0])
		default_nick_set = getpwuid_pw_name();

	if (!default_username || !default_username[0])
		default_username = getpwuid_pw_name();

	if (!default_realname || !default_realname[0])
		default_realname = getpwuid_pw_name();

	state_init();
		return -1;
	}

	for (size_t i = 0; i < n_servers; i++) {



@@ 300,34 302,45 @@ parse_args(int argc, char **argv)
		if (cli_servers[i].port == NULL)
			cli_servers[i].port = (cli_servers[i].tls == IO_TLS_ENABLED) ? "6697" : "6667";

		struct server *s = server(
		cli_servers[i].s = server(
			cli_servers[i].host,
			cli_servers[i].port,
			cli_servers[i].pass,
			(cli_servers[i].username ? cli_servers[i].username : default_username),
			(cli_servers[i].realname ? cli_servers[i].realname : default_realname)
			cli_servers[i].username,
			cli_servers[i].realname
		);

		s->connection = connection(s, cli_servers[i].host, cli_servers[i].port, flags);
		cli_servers[i].s->connection = connection(
			cli_servers[i].s,
			cli_servers[i].host,
			cli_servers[i].port,
			flags);

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

		if (cli_servers[i].chans && state_server_set_chans(s, cli_servers[i].chans))
			arg_error("invalid chans: '%s'", cli_servers[i].chans);

		if (server_set_nicks(s, (cli_servers[i].nicks ? cli_servers[i].nicks : default_nick_set)))
			arg_error("invalid nicks: '%s'", cli_servers[i].nicks);
		if (cli_servers[i].nicks && server_set_nicks(cli_servers[i].s, cli_servers[i].nicks)) {
			arg_error("invalid %s: '%s'", rirc_opt_str('n'), cli_servers[i].nicks);
			return -1;
		}

		cli_servers[i].s = s;
		if (cli_servers[i].chans && server_set_chans(cli_servers[i].s, cli_servers[i].chans)) {
			arg_error("invalid %s: '%s'", rirc_opt_str('c'), cli_servers[i].chans);
			return -1;
		}

		channel_set_current(s->channel);
		channel_set_current(cli_servers[i].s->channel);
	}

	io_init();
	for (size_t i = 0; i < n_servers; i++) {

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

		if ((ret = io_cx(cli_servers[i].s->connection)))
			server_error(cli_servers[i].s, "failed to connect: %s", io_err(ret));
	}

	return 0;
}


@@ 336,19 349,33 @@ parse_args(int argc, char **argv)
int
main(int argc, char **argv)
{
	int ret;

	if (argc > 0)
	if (argc)
		runtime_name = argv[0];

	if (!default_username || !default_username[0])
		default_username = rirc_pw_name();

	if (!default_realname || !default_realname[0])
		default_realname = rirc_pw_name();

	if (!default_nicks || !default_nicks[0])
		default_nicks = rirc_pw_name();

	srand(time(NULL));

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

	if (rirc_parse_args(argc, argv)) {
		state_term();
		draw(DRAW_CLEAR);
		return EXIT_FAILURE;
	}

	return ret;
	draw_init();
	io_start();
	draw_term();
	state_term();

	return EXIT_SUCCESS;
}
#endif

M src/state.c => src/state.c +14 -48
@@ 94,7 94,7 @@ static const char *cmd_list[] = {
void
state_init(void)
{
	state.default_channel = state.current_channel = channel("rirc", CHANNEL_T_RIRC);
	state.default_channel = channel("rirc", CHANNEL_T_RIRC);

	newlinef(state.default_channel, 0, FROM_INFO, "      _");
	newlinef(state.default_channel, 0, FROM_INFO, " _ __(_)_ __ ___");


@@ 107,6 107,8 @@ state_init(void)
#ifndef NDEBUG
	newlinef(state.default_channel, 0, FROM_INFO, " - compiled with DEBUG flags");
#endif

	channel_set_current(state.default_channel);
}

void


@@ 215,42 217,6 @@ _newline(struct channel *c, enum buffer_line_type type, const char *from, const 
	}
}

int
state_server_set_chans(struct server *s, const char *chans)
{
	char *p1, *p2, *base;
	size_t n = 0;

	p2 = base = strdup(chans);

	do {
		n++;

		p1 = p2;
		p2 = strchr(p2, ',');

		if (p2)
			*p2++ = 0;

		if (!irc_ischan(p1)) {
			free(base);
			return -1;
		}
	} while (p2);

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

	free(base);

	return 0;
}

static int
state_input_action(const char *input, size_t len)
{


@@ 374,7 340,7 @@ state_channel_close(int action_confirm)

		if (s->connected && c->type == CHANNEL_T_CHANNEL && !c->parted) {
			if ((ret = io_sendf(s->connection, "PART %s :%s", c->name, DEFAULT_PART_MESG)))
				newlinef(s->channel, 0, FROM_ERROR, "sendf fail: %s", io_err(ret));
				server_error(s, "sendf fail: %s", io_err(ret));
		}

		channel_set_current(c->next);


@@ 387,7 353,7 @@ state_channel_close(int action_confirm)

		if (s->connected) {
			if ((ret = io_sendf(s->connection, "QUIT :%s", DEFAULT_QUIT_MESG)))
				newlinef(s->channel, 0, FROM_ERROR, "sendf fail: %s", io_err(ret));
				server_error(s, "sendf fail: %s", io_err(ret));
			io_dx(s->connection);
		}



@@ 408,11 374,11 @@ buffer_scrollback_back(struct channel *c)

	struct buffer *b = &c->buffer;

	unsigned int buffer_i = b->scrollback,
	             count = 0,
	             text_w = 0,
	             cols = state_tty_cols,
	             rows = state_tty_rows - 4;
	unsigned buffer_i = b->scrollback,
	         count = 0,
	         text_w = 0,
	         cols = state_tty_cols,
	         rows = state_tty_rows - 4;

	struct buffer_line *line = buffer_line(b, buffer_i);



@@ 451,10 417,10 @@ buffer_scrollback_forw(struct channel *c)
{
	/* Scroll a buffer forward one page */

	unsigned int count = 0,
	             text_w = 0,
	             cols = state_tty_cols,
	             rows = state_tty_rows - 4;
	unsigned count = 0,
	         text_w = 0,
	         cols = state_tty_cols,
	         rows = state_tty_rows - 4;

	struct buffer *b = &c->buffer;


M src/state.h => src/state.h +0 -1
@@ 26,7 26,6 @@ unsigned state_cols(void);
unsigned state_rows(void);

const char *action_message(void);
int state_server_set_chans(struct server*, const char*);
struct channel* channel_get_first(void);
struct channel* channel_get_last(void);
struct channel* channel_get_next(struct channel*);

M src/utils/utils.c => src/utils/utils.c +15 -5
@@ 399,11 399,21 @@ irc_ischanchar(char c, int first)
	 * channelid  = 5( %x41-5A / digit )   ; 5( A-Z / 0-9 )
	 */

	/* TODO: CHANTYPES */
	(void)c;
	(void)first;

	return 1;
	if (first)
		return (c == '#' || c == '+' || c == '&');

	switch (c) {
		case 0x00: /* NUL */
		case 0x07: /* BEL */
		case 0x0D: /* CR */
		case 0x0A: /* LF */
		case ' ':
		case ',':
		case ':':
			return 0;
		default:
			return 1;
	}
}

static inline int

M test/components/buffer.c => test/components/buffer.c +10 -10
@@ 238,19 238,19 @@ test_buffer_scrollback_status(void)
	assert_true(buffer_full(&b));

	b.scrollback = b.tail;
	assert_ueq((100 * buffer_scrollback_status(&b)), 100);
	assert_ueq((buffer_scrollback_status(&b)), 100);

	b.scrollback = b.tail + (BUFFER_LINES_MAX / 2);
	assert_ueq((100 * buffer_scrollback_status(&b)), 50);
	assert_ueq((buffer_scrollback_status(&b)), 50);

	b.scrollback = b.head - 1;
	assert_ueq((100 * buffer_scrollback_status(&b)), 0);
	assert_ueq((buffer_scrollback_status(&b)), 0);
}

static void
test_buffer_index_overflow(void)
{
	/* Test masked indexing after unsigned integer overflow */
	/* Test masked indexing after unsigned overflow */

	struct buffer b;



@@ 284,12 284,12 @@ test_buffer_line_overlength(void)
	buffer(&b);

	/* Indices to first and last positions of lines, total length = 2.5 times the maximum */
	unsigned int f1 = 0,
	             l1 = TEXT_LENGTH_MAX - 1,
	             f2 = TEXT_LENGTH_MAX,
	             l2 = TEXT_LENGTH_MAX * 2 - 1,
	             f3 = TEXT_LENGTH_MAX * 2,
	             l3 = TEXT_LENGTH_MAX * 2 + TEXT_LENGTH_MAX / 2 - 1;
	unsigned f1 = 0,
	         l1 = TEXT_LENGTH_MAX - 1,
	         f2 = TEXT_LENGTH_MAX,
	         l2 = TEXT_LENGTH_MAX * 2 - 1,
	         f3 = TEXT_LENGTH_MAX * 2,
	         l3 = TEXT_LENGTH_MAX * 2 + TEXT_LENGTH_MAX / 2 - 1;

	/* Add a line that's 2.5 times the maximum length */
	char text[(l3 + 1) + 1];

M test/components/server.c => test/components/server.c +49 -0
@@ 112,6 112,54 @@ test_server_list(void)
}

static void
test_server_set_chans(void)
{
	struct channel *c;
	struct server *s;

	s = server("host1", "port", NULL, "", "");
	assert_eq(s->clist.count, 1);

	/* empty values, invalid formats */
	assert_eq(server_set_chans(s, ""), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, ","), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, ",,,"), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, ",#a,#b,c"), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, "#a,#b,c,"), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, "#a,#b,c "), -1);
	assert_eq(s->clist.count, 1);
	assert_eq(server_set_chans(s, "#a b #c"), -1);
	assert_eq(s->clist.count, 1);

	/* valid */
	assert_eq(server_set_chans(s, "#a,b,c,#d"), 0);
	assert_eq(s->clist.count, 5);

	if (!(c = channel_list_get(&(s->clist), "#a", s->casemapping)))
		test_abort("failed to find channel '#a'");
	assert_eq(c->type, CHANNEL_T_CHANNEL);

	if (!(c = channel_list_get(&(s->clist), "b", s->casemapping)))
		test_abort("failed to find channel 'b'");
	assert_eq(c->type, CHANNEL_T_PRIVATE);

	if (!(c = channel_list_get(&(s->clist), "c", s->casemapping)))
		test_abort("failed to find channel '#c'");
	assert_eq(c->type, CHANNEL_T_PRIVATE);

	if (!(c = channel_list_get(&(s->clist), "#d", s->casemapping)))
		test_abort("failed to find channel 'd'");
	assert_eq(c->type, CHANNEL_T_CHANNEL);

	server_free(s);
}

static void
test_server_set_nicks(void)
{
	struct server *s = server("host", "port", NULL, "", "");


@@ 251,6 299,7 @@ main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_server_list),
		TESTCASE(test_server_set_chans),
		TESTCASE(test_server_set_nicks),
		TESTCASE(test_parse_005)
	};

M test/rirc.c => test/rirc.c +5 -4
@@ 17,19 17,20 @@
#include "test/io.mock.c"

static void
test_STUB(void)
test_rirc_parse_args(void)
{
	; /* TODO */
	/* TODO */
	(void)rirc_parse_args;
}

int
main(void)
{
	/* FIXME: */
	(void)parse_args;
	(void)rirc_pw_name;

	struct testcase tests[] = {
		TESTCASE(test_STUB)
		TESTCASE(test_rirc_parse_args)
	};

	return run_tests(tests);

M test/test.h => test/test.h +12 -0
@@ 30,6 30,7 @@
 *    - test_failf(M, ...)
 *    - test_abort(M)
 *    - test_abortf(M, ...)
 *    - test_abort_main(M)
 */

#include <inttypes.h>


@@ 192,6 193,17 @@
		return; \
	} while (0)

#define test_abortf(M, ...) \
	do { \
		_print_testcase_name_(__func__); \
		printf("    %d: ", __LINE__); \
		printf((M), __VA_ARGS__); \
		printf("\n"); \
		printf("    ---Testcase aborted---\n"); \
		_failures_++; \
		return; \
	} while (0)

#define test_abort_main(M) \
	do { \
		printf("    %d: " M "\n", __LINE__); \

M test/utils/utils.c => test/utils/utils.c +14 -0
@@ 2,6 2,18 @@
#include "src/utils/utils.c"

static void
test_irc_ischan(void)
{
	/* TODO */
}

static void
test_irc_isnick(void)
{
	/* TODO */
}

static void
test_irc_message_param(void)
{
	char *param;


@@ 646,6 658,8 @@ int
main(void)
{
	struct testcase tests[] = {
		TESTCASE(test_irc_ischan),
		TESTCASE(test_irc_isnick),
		TESTCASE(test_irc_message_param),
		TESTCASE(test_irc_message_parse),
		TESTCASE(test_irc_message_split),