aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile350
-rw-r--r--install.mk2
-rw-r--r--parsetest.mk2
-rw-r--r--src/atari.cfg52
-rw-r--r--src/cio.h5
-rw-r--r--src/cio.s8
-rw-r--r--src/cmd.c19
-rw-r--r--src/conio.c37
-rw-r--r--src/conio.h14
-rw-r--r--src/err.c37
-rw-r--r--src/err.h22
-rw-r--r--src/intr.s7
-rw-r--r--src/irc.c306
-rw-r--r--src/irc.h59
-rw-r--r--src/main.c311
-rw-r--r--src/nio.c183
-rw-r--r--src/nio.h79
-rw-r--r--src/sio.h7
-rw-r--r--src/sio.s17
-rw-r--r--src/ui.c73
20 files changed, 1590 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..febbae0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,350 @@
+###############################################################################
+### Generic Makefile for cc65 projects - full version with abstract options ###
+### V1.3.0(w) 2010 - 2013 Oliver Schmidt & Patryk "Silver Dream !" Ɓogiewa ###
+###############################################################################
+
+###############################################################################
+### In order to override defaults - values can be assigned to the variables ###
+###############################################################################
+
+# Space or comma separated list of cc65 supported target platforms to build for.
+# Default: c64 (lowercase!)
+TARGETS := atari
+
+# Name of the final, single-file executable.
+# Default: name of the current dir with target name appended
+PROGRAM := fnchat
+
+# Path(s) to additional libraries required for linking the program
+# Use only if you don't want to place copies of the libraries in SRCDIR
+# Default: none
+LIBS :=
+
+# Custom linker configuration file
+# Use only if you don't want to place it in SRCDIR
+# Default: none
+CONFIG :=
+
+# Additional C compiler flags and options.
+# Default: none
+CFLAGS = -Oris
+
+# Additional assembler flags and options.
+# Default: none
+ASFLAGS =
+
+# Additional linker flags and options.
+# Default: none
+LDFLAGS = $(LDFLAGS.$(TARGETS))
+LDFLAGS.atari = --mapfile $(PROGRAM).map
+
+# Path to the directory containing C and ASM sources.
+# Default: src
+SRCDIR :=
+
+# Path to the directory where object files are to be stored (inside respective target subdirectories).
+# Default: obj
+OBJDIR :=
+
+# Command used to run the emulator.
+# Default: depending on target platform. For default (c64) target: x64 -kernal kernal -VICIIdsize -autoload
+EMUCMD :=
+
+# Optional commands used before starting the emulation process, and after finishing it.
+# Default: none
+#PREEMUCMD := osascript -e "tell application \"System Events\" to set isRunning to (name of processes) contains \"X11.bin\"" -e "if isRunning is true then tell application \"X11\" to activate"
+#PREEMUCMD := osascript -e "tell application \"X11\" to activate"
+#POSTEMUCMD := osascript -e "tell application \"System Events\" to tell process \"X11\" to set visible to false"
+#POSTEMUCMD := osascript -e "tell application \"Terminal\" to activate"
+PREEMUCMD :=
+POSTEMUCMD :=
+
+# On Windows machines VICE emulators may not be available in the PATH by default.
+# In such case, please set the variable below to point to directory containing
+# VICE emulators.
+#VICE_HOME := "C:\Program Files\WinVICE-2.2-x86\"
+VICE_HOME :=
+
+# Options state file name. You should not need to change this, but for those
+# rare cases when you feel you really need to name it differently - here you are
+STATEFILE := Makefile.options
+
+###################################################################################
+#### DO NOT EDIT BELOW THIS LINE, UNLESS YOU REALLY KNOW WHAT YOU ARE DOING! ####
+###################################################################################
+
+###################################################################################
+### Mapping abstract options to the actual compiler, assembler and linker flags ###
+### Predefined compiler, assembler and linker flags, used with abstract options ###
+### valid for 2.14.x. Consult the documentation of your cc65 version before use ###
+###################################################################################
+
+# Compiler flags used to tell the compiler to optimise for SPEED
+define _optspeed_
+ CFLAGS += -Oris
+endef
+
+# Compiler flags used to tell the compiler to optimise for SIZE
+define _optsize_
+ CFLAGS += -Or
+endef
+
+# Compiler and assembler flags for generating listings
+define _listing_
+ CFLAGS += --listing $$(@:.o=.lst)
+ ASFLAGS += --listing $$(@:.o=.lst)
+ REMOVES += $(addsuffix .lst,$(basename $(OBJECTS)))
+endef
+
+# Linker flags for generating map file
+define _mapfile_
+ LDFLAGS += --mapfile $$@.map
+ REMOVES += $(PROGRAM).map
+endef
+
+# Linker flags for generating VICE label file
+define _labelfile_
+ LDFLAGS += -Ln $$@.lbl
+ REMOVES += $(PROGRAM).lbl
+endef
+
+# Linker flags for generating a debug file
+define _debugfile_
+ LDFLAGS += -Wl --dbgfile,$$@.dbg
+ REMOVES += $(PROGRAM).dbg
+endef
+
+###############################################################################
+### Defaults to be used if nothing defined in the editable sections above ###
+###############################################################################
+
+# Presume the C64 target like the cl65 compile & link utility does.
+# Set TARGETS to override.
+ifeq ($(TARGETS),)
+ TARGETS := c64
+endif
+
+# Presume we're in a project directory so name the program like the current
+# directory. Set PROGRAM to override.
+ifeq ($(PROGRAM),)
+ PROGRAM := $(notdir $(CURDIR))
+endif
+
+# Presume the C and asm source files to be located in the subdirectory 'src'.
+# Set SRCDIR to override.
+ifeq ($(SRCDIR),)
+ SRCDIR := src
+endif
+
+# Presume the object and dependency files to be located in the subdirectory
+# 'obj' (which will be created). Set OBJDIR to override.
+ifeq ($(OBJDIR),)
+ OBJDIR := obj
+endif
+TARGETOBJDIR := $(OBJDIR)/$(TARGETS)
+
+# On Windows it is mandatory to have CC65_HOME set. So do not unnecessarily
+# rely on cl65 being added to the PATH in this scenario.
+ifdef CC65_HOME
+ CC := $(CC65_HOME)/bin/cl65
+else
+ CC := cl65
+endif
+
+# Default emulator commands and options for particular targets.
+# Set EMUCMD to override.
+c64_EMUCMD := $(VICE_HOME)xscpu64 -VICIIdsize -autostart
+c128_EMUCMD := $(VICE_HOME)x128 -kernal kernal -VICIIdsize -autoload
+vic20_EMUCMD := $(VICE_HOME)xvic -kernal kernal -VICdsize -autoload
+pet_EMUCMD := $(VICE_HOME)xpet -Crtcdsize -autoload
+plus4_EMUCMD := $(VICE_HOME)xplus4 -TEDdsize -autoload
+# So far there is no x16 emulator in VICE (why??) so we have to use xplus4 with -memsize option
+c16_EMUCMD := $(VICE_HOME)xplus4 -ramsize 16 -TEDdsize -autoload
+cbm510_EMUCMD := $(VICE_HOME)xcbm2 -model 510 -VICIIdsize -autoload
+cbm610_EMUCMD := $(VICE_HOME)xcbm2 -model 610 -Crtcdsize -autoload
+atari_EMUCMD := atari800 -windowed -xl -pal -nopatchall -run
+
+ifeq ($(EMUCMD),)
+ EMUCMD = $($(CC65TARGET)_EMUCMD)
+endif
+
+###############################################################################
+### The magic begins ###
+###############################################################################
+
+# The "Native Win32" GNU Make contains quite some workarounds to get along with
+# cmd.exe as shell. However it does not provide means to determine that it does
+# actually activate those workarounds. Especially does $(SHELL) NOT contain the
+# value 'cmd.exe'. So the usual way to determine if cmd.exe is being used is to
+# execute the command 'echo' without any parameters. Only cmd.exe will return a
+# non-empy string - saying 'ECHO is on/off'.
+#
+# Many "Native Win32" prorams accept '/' as directory delimiter just fine. How-
+# ever the internal commands of cmd.exe generally require '\' to be used.
+#
+# cmd.exe has an internal command 'mkdir' that doesn't understand nor require a
+# '-p' to create parent directories as needed.
+#
+# cmd.exe has an internal command 'del' that reports a syntax error if executed
+# without any file so make sure to call it only if there's an actual argument.
+ifeq ($(shell echo),)
+ MKDIR = mkdir -p $1
+ RMDIR = rmdir $1
+ RMFILES = $(RM) $1
+else
+ MKDIR = mkdir $(subst /,\,$1)
+ RMDIR = rmdir $(subst /,\,$1)
+ RMFILES = $(if $1,del /f $(subst /,\,$1))
+endif
+COMMA := ,
+SPACE := $(N/A) $(N/A)
+define NEWLINE
+
+
+endef
+# Note: Do not remove any of the two empty lines above !
+
+TARGETLIST := $(subst $(COMMA),$(SPACE),$(TARGETS))
+
+ifeq ($(words $(TARGETLIST)),1)
+
+# Set PROGRAM to something like 'myprog.c64'.
+override PROGRAM := $(PROGRAM).xex
+
+# Set SOURCES to something like 'src/foo.c src/bar.s'.
+# Use of assembler files with names ending differently than .s is deprecated!
+SOURCES := $(wildcard $(SRCDIR)/*.c)
+SOURCES += $(wildcard $(SRCDIR)/*.s)
+SOURCES += $(wildcard $(SRCDIR)/*.asm)
+SOURCES += $(wildcard $(SRCDIR)/*.a65)
+
+# Add to SOURCES something like 'src/c64/me.c src/c64/too.s'.
+# Use of assembler files with names ending differently than .s is deprecated!
+SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.c)
+SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.s)
+SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.asm)
+SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.a65)
+
+# Set OBJECTS to something like 'obj/c64/foo.o obj/c64/bar.o'.
+OBJECTS := $(addsuffix .o,$(basename $(addprefix $(TARGETOBJDIR)/,$(notdir $(SOURCES)))))
+
+# Set DEPENDS to something like 'obj/c64/foo.d obj/c64/bar.d'.
+DEPENDS := $(OBJECTS:.o=.d)
+
+# Add to LIBS something like 'src/foo.lib src/c64/bar.lib'.
+LIBS += $(wildcard $(SRCDIR)/*.lib)
+LIBS += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.lib)
+
+# Add to CONFIG something like 'src/c64/bar.cfg src/foo.cfg'.
+CONFIG += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.cfg)
+CONFIG += $(wildcard $(SRCDIR)/*.cfg)
+
+# Select CONFIG file to use. Target specific configs have higher priority.
+ifneq ($(word 2,$(CONFIG)),)
+ CONFIG := $(firstword $(CONFIG))
+ $(info Using config file $(CONFIG) for linking)
+endif
+
+.SUFFIXES:
+.PHONY: all test clean zap love
+
+all: $(PROGRAM)
+
+-include $(DEPENDS)
+-include $(STATEFILE)
+
+# If OPTIONS are given on the command line then save them to STATEFILE
+# if (and only if) they have actually changed. But if OPTIONS are not
+# given on the command line then load them from STATEFILE. Have object
+# files depend on STATEFILE only if it actually exists.
+ifeq ($(origin OPTIONS),command line)
+ ifneq ($(OPTIONS),$(_OPTIONS_))
+ ifeq ($(OPTIONS),)
+ $(info Removing OPTIONS)
+ $(shell $(RM) $(STATEFILE))
+ $(eval $(STATEFILE):)
+ else
+ $(info Saving OPTIONS=$(OPTIONS))
+ $(shell echo _OPTIONS_=$(OPTIONS) > $(STATEFILE))
+ endif
+ $(eval $(OBJECTS): $(STATEFILE))
+ endif
+else
+ ifeq ($(origin _OPTIONS_),file)
+ $(info Using saved OPTIONS=$(_OPTIONS_))
+ OPTIONS = $(_OPTIONS_)
+ $(eval $(OBJECTS): $(STATEFILE))
+ endif
+endif
+
+# Transform the abstract OPTIONS to the actual cc65 options.
+$(foreach o,$(subst $(COMMA),$(SPACE),$(OPTIONS)),$(eval $(_$o_)))
+
+# Strip potential variant suffix from the actual cc65 target.
+CC65TARGET := $(firstword $(subst .,$(SPACE),$(TARGETLIST)))
+
+# The remaining targets.
+$(TARGETOBJDIR):
+ $(call MKDIR,$@)
+
+vpath %.c $(SRCDIR)/$(TARGETLIST) $(SRCDIR)
+
+$(TARGETOBJDIR)/%.o: %.c | $(TARGETOBJDIR)
+ $(CC) -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(CFLAGS) -o $@ $<
+
+vpath %.s $(SRCDIR)/$(TARGETLIST) $(SRCDIR)
+
+$(TARGETOBJDIR)/%.o: %.s | $(TARGETOBJDIR)
+ $(CC) -t $(CC65TARGET) -Wa -DDYN_DRV=0 -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $<
+
+vpath %.asm $(SRCDIR)/$(TARGETLIST) $(SRCDIR)
+
+$(TARGETOBJDIR)/%.o: %.asm | $(TARGETOBJDIR)
+ $(CC) -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $<
+
+vpath %.a65 $(SRCDIR)/$(TARGETLIST) $(SRCDIR)
+
+$(TARGETOBJDIR)/%.o: %.a65 | $(TARGETOBJDIR)
+ $(CC) -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $<
+
+$(PROGRAM): $(CONFIG) $(OBJECTS) $(LIBS)
+ $(CC) -t $(CC65TARGET) $(LDFLAGS) -o $@ $(patsubst %.cfg,-C %.cfg,$^)
+
+
+test: $(PROGRAM)
+ $(PREEMUCMD)
+ $(EMUCMD) $<
+ $(POSTEMUCMD)
+
+clean:
+ $(call RMFILES,$(OBJECTS))
+ $(call RMFILES,$(DEPENDS))
+ $(call RMFILES,$(REMOVES))
+ $(call RMFILES,$(PROGRAM))
+ $(call RMFILES,test.map)
+ $(call RMFILES,$(PROGRAM).map)
+ $(call RMFILES,test.atr)
+
+else # $(words $(TARGETLIST)),1
+
+all test clean:
+ $(foreach t,$(TARGETLIST),$(MAKE) TARGETS=$t $@$(NEWLINE))
+
+endif # $(words $(TARGETLIST)),1
+
+OBJDIRLIST := $(wildcard $(OBJDIR)/*)
+
+zap:
+ $(foreach o,$(OBJDIRLIST),-$(call RMFILES,$o/*.o $o/*.d $o/*.lst)$(NEWLINE))
+ $(foreach o,$(OBJDIRLIST),-$(call RMDIR,$o)$(NEWLINE))
+ -$(call RMDIR,$(OBJDIR))
+ -$(call RMFILES,$(basename $(PROGRAM)).* $(STATEFILE))
+
+love:
+ @echo "Not war, eh?"
+
+###################################################################
+### Place your additional targets in the additional Makefiles ###
+### in the same directory - their names have to end with ".mk"! ###
+###################################################################
+-include *.mk
diff --git a/install.mk b/install.mk
new file mode 100644
index 0000000..ca71538
--- /dev/null
+++ b/install.mk
@@ -0,0 +1,2 @@
+install:
+ cp irctest.xex /var/tnfs/
diff --git a/parsetest.mk b/parsetest.mk
new file mode 100644
index 0000000..4e9eb8e
--- /dev/null
+++ b/parsetest.mk
@@ -0,0 +1,2 @@
+parsetest: src/irc.c
+ rm -f parsetest; gcc -g -Wall -o parsetest src/irc.c
diff --git a/src/atari.cfg b/src/atari.cfg
new file mode 100644
index 0000000..ea190f7
--- /dev/null
+++ b/src/atari.cfg
@@ -0,0 +1,52 @@
+FEATURES {
+ STARTADDRESS: default = $2000;
+}
+SYMBOLS {
+ __EXEHDR__: type = import;
+ __AUTOSTART__: type = import; # force inclusion of autostart "trailer"
+ __STACKSIZE__: type = weak, value = $0800; # 2k stack
+ __STARTADDRESS__: type = export, value = %S;
+ __RESERVED_MEMORY__: type = weak, value = $0000;
+}
+MEMORY {
+ ZP: file = "", define = yes, start = $0082, size = $007E;
+
+# file header, just $FFFF
+ HEADER: file = %O, start = $0000, size = $0002;
+
+# "main program" load chunk
+ MAINHDR: file = %O, start = $0000, size = $0004;
+ MAIN: file = %O, define = yes, start = %S, size = $BC20 - __STACKSIZE__ - __RESERVED_MEMORY__ - %S;
+ TRAILER: file = %O, start = $0000, size = $0006;
+}
+SEGMENTS {
+ ZEROPAGE: load = ZP, type = zp;
+ EXTZP: load = ZP, type = zp, optional = yes;
+ EXEHDR: load = HEADER, type = ro;
+ MAINHDR: load = MAINHDR, type = ro;
+ STARTUP: load = MAIN, type = ro, define = yes;
+ LOWBSS: load = MAIN, type = rw, optional = yes; # not zero initialized
+ LOWCODE: load = MAIN, type = ro, define = yes, optional = yes;
+ ONCE: load = MAIN, type = ro, optional = yes;
+ CODE: load = MAIN, type = ro, define = yes;
+ RODATA: load = MAIN, type = ro;
+ DATA: load = MAIN, type = rw;
+ INIT: load = MAIN, type = rw, optional = yes;
+ BSS: load = MAIN, type = bss, define = yes;
+ AUTOSTRT: load = TRAILER, type = ro;
+}
+FEATURES {
+ CONDES: type = constructor,
+ label = __CONSTRUCTOR_TABLE__,
+ count = __CONSTRUCTOR_COUNT__,
+ segment = ONCE;
+ CONDES: type = destructor,
+ label = __DESTRUCTOR_TABLE__,
+ count = __DESTRUCTOR_COUNT__,
+ segment = RODATA;
+ CONDES: type = interruptor,
+ label = __INTERRUPTOR_TABLE__,
+ count = __INTERRUPTOR_COUNT__,
+ segment = RODATA,
+ import = __CALLIRQ__;
+}
diff --git a/src/cio.h b/src/cio.h
new file mode 100644
index 0000000..d07c875
--- /dev/null
+++ b/src/cio.h
@@ -0,0 +1,5 @@
+/**
+ * Function to call cio
+ */
+
+void ciov();
diff --git a/src/cio.s b/src/cio.s
new file mode 100644
index 0000000..69a789a
--- /dev/null
+++ b/src/cio.s
@@ -0,0 +1,8 @@
+ ;; Call CIO
+
+ .export _ciov
+
+_ciov: LDX #$00
+ JSR $E456
+ RTS
+
diff --git a/src/cmd.c b/src/cmd.c
new file mode 100644
index 0000000..3c7f482
--- /dev/null
+++ b/src/cmd.c
@@ -0,0 +1,19 @@
+#include <atari.h>
+#include <stdio.h>
+#include "irc.h"
+
+void cmd_chan_text(const char *cmd) {
+ txbuf_set_str("PRIVMSG ");
+ txbuf_append_str(channel);
+ txbuf_append_str(" :");
+ txbuf_append_str(cmd);
+ txbuf_send();
+}
+
+void cmd_command(const char *cmd) {
+ if(*cmd == '/')
+ txbuf_send_str(cmd + 1);
+ else if(channel[0])
+ cmd_chan_text(cmd);
+ else ui_print("*** You are not on a channel\n");
+}
diff --git a/src/conio.c b/src/conio.c
new file mode 100644
index 0000000..2742ab6
--- /dev/null
+++ b/src/conio.c
@@ -0,0 +1,37 @@
+/**
+ * Simple conio for E:
+ */
+
+#include <atari.h>
+#include <string.h>
+#include "cio.h"
+
+void printl(const char* c, int l)
+{
+ OS.iocb[0].buffer=c;
+ OS.iocb[0].buflen=l;
+ OS.iocb[0].command=IOCB_PUTCHR;
+ ciov();
+}
+
+void printc(char* c)
+{
+ OS.iocb[0].buffer=c;
+ OS.iocb[0].buflen=1;
+ OS.iocb[0].command=IOCB_PUTCHR;
+ ciov();
+}
+
+void print(const char* c)
+{
+ int l=strlen(c);
+ printl(c,l);
+}
+
+void get_line(char* buf, unsigned char len)
+{
+ OS.iocb[0].buffer=buf;
+ OS.iocb[0].buflen=len;
+ OS.iocb[0].command=IOCB_GETREC;
+ ciov();
+}
diff --git a/src/conio.h b/src/conio.h
new file mode 100644
index 0000000..25ca6c2
--- /dev/null
+++ b/src/conio.h
@@ -0,0 +1,14 @@
+/**
+ * conio
+ */
+
+#ifndef CONIO_H
+#define CONIO_H
+
+void print(const char* c);
+void printc(char* c);
+void printl(const char* c, unsigned short l);
+void get_line(char* buf, unsigned char len);
+char get_char(void);
+
+#endif /* CONIO_H */
diff --git a/src/err.c b/src/err.c
new file mode 100644
index 0000000..8d58670
--- /dev/null
+++ b/src/err.c
@@ -0,0 +1,37 @@
+/**
+ * FujiNet Tools for CLI
+ *
+ * Error output
+ *
+ * Author: Thomas Cherryhomes
+ * <thom.cherryhomes@gmail.com>
+ *
+ * Released under GPL, see COPYING
+ * for details
+ */
+
+#include <atari.h>
+#include "conio.h"
+
+const char error_138[]="FUJINET NOT RESPONDING\x9B";
+const char error_139[]="FUJINET NAK\x9b";
+const char error[]="SIO ERROR\x9b";
+
+/**
+ * Show error
+ */
+void err_sio(void)
+{
+ switch (OS.dcb.dstats)
+ {
+ case 138:
+ print(error_138);
+ break;
+ case 139:
+ print(error_139);
+ break;
+ default:
+ print(error);
+ break;
+ }
+}
diff --git a/src/err.h b/src/err.h
new file mode 100644
index 0000000..d3bf3d7
--- /dev/null
+++ b/src/err.h
@@ -0,0 +1,22 @@
+/**
+ * FujiNet Tools for CLI
+ *
+ * Error output
+ *
+ * Author: Thomas Cherryhomes
+ * <thom.cherryhomes@gmail.com>
+ *
+ * Released under GPL, see COPYING
+ * for details
+ */
+
+
+#ifndef ERR_H
+#define ERR_H
+
+/**
+ * Show error
+ */
+void err_sio(void);
+
+#endif /* ERR_H */
diff --git a/src/intr.s b/src/intr.s
new file mode 100644
index 0000000..00de34f
--- /dev/null
+++ b/src/intr.s
@@ -0,0 +1,7 @@
+ .export _ih
+ .import _trip
+
+_ih: LDA #$01
+ STA _trip
+ PLA
+ RTI
diff --git a/src/irc.c b/src/irc.c
new file mode 100644
index 0000000..735ca2f
--- /dev/null
+++ b/src/irc.c
@@ -0,0 +1,306 @@
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "irc.h"
+
+#ifdef __ATARI__
+#include <atari.h>
+#include <conio.h>
+#include "conio.h"
+#include "nio.h"
+#else
+#define CH_EOL '|'
+unsigned char rx_buf[MAX_IRC_MSG_LEN]; // RX buffer.
+unsigned short bw=0; // # of bytes waiting.
+#endif
+
+#define MAX_MSG 512
+
+char *msg_src, *msg_cmd, *msg_dest, *msg_text;
+char *msg_args[MAX_MSG_ARGS];
+int msg_argcount;
+
+static char msgbuf[MAX_MSG] = { 0 };
+static char *msg; /* with source removed */
+static int msgbuf_len = 0, msg_len = 0;
+
+static int joined = 0;
+
+#ifdef __ATARI__
+static void join_channel(void) {
+ ui_print("Joining channel...\n");
+ txbuf_set_str("JOIN ");
+ txbuf_append_str(channel);
+ txbuf_append_str("\n");
+ txbuf_send();
+ joined = 1;
+}
+
+static void do_pong(void) {
+ ui_putchar(CH_EOL);
+ ui_print("PING/PONG\n"); /* make hiding this a preference, or just ditch it */
+ txbuf_set_str("PONG ");
+ txbuf_append_str(msg_args[0]);
+ txbuf_send();
+}
+
+static void do_privmsg(void) {
+ static char chan;
+
+ chan = (*msg_dest == '#');
+
+ if(chan) {
+ ui_putchar('<');
+ } else {
+ ui_putchar('*');
+ }
+
+ ui_print(msg_src);
+
+ if(chan) {
+ ui_putchar('>');
+ } else {
+ ui_putchar('*');
+ }
+
+ ui_putchar(' ');
+ ui_print(msg_text);
+}
+
+static void do_catchall(void) {
+ int i;
+ if(msg_src) {
+ ui_print(msg_src);
+ ui_putchar(' ');
+ }
+ ui_print(msg_cmd);
+ for(i = 0; i < msg_argcount; i++) {
+ ui_putchar(' ');
+ ui_print(msg_args[i]);
+ }
+ if(msg_text) {
+ ui_putchar(' ');
+ ui_print(msg_text);
+ }
+}
+
+static void do_numeric(void) {
+ do_catchall();
+
+ /* RPL_ENDOFMOTD or RPL_NOMOTD */
+ if(!joined && (streq(msg_cmd, "372") || streq(msg_cmd, "422"))) {
+ join_channel();
+ }
+}
+
+static void invalid_msg(char type) {
+ ui_print("??? unknown, type ");
+ ui_putchar(type);
+ ui_putchar('\n');
+}
+#else
+static void do_pong(void) { }
+static void invalid_msg(char type) {
+ printf("??? unknown, type %c\n", type);
+}
+#endif
+
+/* msgbuf contains a complete message from the server, whose
+ length is msgbuf_len. the last character *must* be CH_EOL,
+ and the last argument ends with CH_EOL. */
+static void parse_msg(void) {
+ char *p;
+
+#ifndef __ATARI__
+ printf("\ngot message:\n");
+ for(msg = msgbuf; *msg != CH_EOL; msg++)
+ putchar(*msg);
+ putchar('\n');
+ putchar('\n');
+#endif
+
+ msg_cmd = msg_text = msg_src = msg_dest = 0;
+ msg = msgbuf;
+
+ /* ignore empty message */
+ if(*msg == CH_EOL) return;
+
+ /* if there's a final multiword arg... */
+ /* FIXME: channel names can have colons, which breaks this... */
+ p = strstr(msg + 1, " :"); /* +1 to skip leading colon in msg source */
+ if(p) {
+ msg_text = p + 2;
+ *p = 0;
+ }
+
+ /* first token is either the source (with a :) or a command (without) */
+ p = strtok(msg, " ");
+ if(!p) {
+ invalid_msg('1');
+ return;
+ }
+
+ if(*p == ':') {
+ msg_src = p; /* generally :irc.example.com or :nick!user@host */
+ msg_cmd = strtok(0, " ");
+ } else {
+ msg_src = 0; /* no source supplied */
+ msg_cmd = p;
+ }
+
+ if(!msg_cmd) {
+ invalid_msg('2');
+ return;
+ }
+
+ /* special case for ping, treat as 1 arg, even if it has space and no : */
+ if(streq_i(msg_cmd, "PING")) {
+ msg_argcount = 1;
+ msg_args[0] = msg_cmd + 6;
+ do_pong();
+ return;
+ } else {
+ for(msg_argcount = 0; msg_argcount < MAX_MSG_ARGS; msg_argcount++) {
+ p = strtok(0, " ");
+ if(p) {
+ msg_args[msg_argcount] = p;
+ } else {
+ break;
+ }
+ }
+ }
+ if(msg_argcount) msg_dest = msg_args[0];
+
+ if(msg_src) {
+ if((p = strstr(msg_src, "!"))) {
+ msg_src++;
+ *p = '\0';
+ } else {
+ msg_src = 0;
+ }
+ }
+
+#ifdef __ATARI__
+ OS.crsinh = 1;
+ ui_start_msg();
+ if(streq_i(msg_cmd, "PRIVMSG")) {
+ do_privmsg();
+ } else if(isdigit(msg_cmd[0])) {
+ do_numeric();
+ } else {
+ do_catchall();
+ }
+ ui_end_msg();
+#else
+ {
+ int i;
+ printf("src: %s\n", msg_src ? msg_src : "<none>");
+ printf("cmd: %s\n", msg_cmd ? msg_cmd : "<none>");
+ printf("args: %d\n", msg_argcount);
+ for(i = 0; i < msg_argcount; i++)
+ printf(" %d: %s\n", i, msg_args[i]);
+ printf("text: %s\n", msg_text ? msg_text : "<none>");
+ }
+#endif
+}
+
+static void irc_parse(void) {
+ int i;
+ char *p = rx_buf;
+
+#ifndef __ATARI__
+ printf("irc_parse() called, bw == %d\n", bw);
+#endif
+
+ for(i = 0; i < bw; i++) {
+ msgbuf[msgbuf_len] = *p;
+ if(*p == CH_EOL) {
+ msgbuf[msgbuf_len + 1] = '\0';
+ parse_msg();
+ msgbuf_len = 0;
+ } else {
+ msgbuf_len++;
+ }
+ p++;
+ }
+}
+
+#ifdef __ATARI__
+bool irc_read(void) {
+ if(!trip) return 1;
+
+ err = nstatus(url);
+
+ if(err == 136) {
+ ui_print("Disconnected, press any key...\n");
+ cgetc();
+ return 0;
+ } else if(err != 1) {
+ print_error(err);
+ return 0;
+ }
+
+ // Get # of bytes waiting, no more than size of rx_buf
+ bw = OS.dvstat[1] * 256 + OS.dvstat[0];
+
+ if(bw > sizeof(rx_buf))
+ bw = sizeof(rx_buf);
+
+ if(bw > 0) {
+ err = nread(url, rx_buf, bw);
+ if(err != 1) {
+ ui_print("READ ERROR: ");
+ print_error(err);
+ return 0;
+ }
+
+ trip = 0;
+ PIA.pactl |= 1; // Flag interrupt as serviced, ready for next one.
+
+ irc_parse();
+ }
+
+ return 1;
+}
+
+/* modern.ircdocs.horse say to do this IMMEDIATELY upon TCP
+ connection, without waiting for anything from the server. */
+void irc_register(void) {
+ txbuf_init();
+ txbuf_append_str("USER ");
+ txbuf_append_str(usernick); /* local (UNIX) username, just use the nick */
+ txbuf_append_str(" 0 * :FujiNetChat User\n"); /* "real" name (make it a pref?) */
+ txbuf_send();
+
+ txbuf_init();
+ txbuf_append_str("NICK ");
+ txbuf_append_str(usernick);
+ txbuf_append_str("\n");
+ txbuf_send();
+}
+
+/* only exits on error (e.g. connection closed, which might be via /QUIT). */
+void irc_loop(void) {
+ while(1) {
+ if(!irc_read()) return;
+
+ if(kbhit())
+ if(joined)
+ ui_keystroke();
+ else join_channel();
+ }
+}
+
+#else // !defined(__ATARI__)
+/* parsetest */
+int main(int argc, char **argv) {
+ strcpy((char *)rx_buf, argv[1]);
+ bw = strlen(rx_buf);
+ irc_parse();
+ /*
+ */
+ return 0;
+}
+#endif
diff --git a/src/irc.h b/src/irc.h
new file mode 100644
index 0000000..77510d1
--- /dev/null
+++ b/src/irc.h
@@ -0,0 +1,59 @@
+#define FNET_TRANSLATION 3
+#define MAX_IRC_MSG_LEN 512
+
+#define streq(x,y) !strcmp(x,y)
+#define streq_i(x,y) !strcasecmp(x,y)
+
+/**** main.c */
+extern char url[256];
+extern char usernick[32];
+extern char channel[32];
+extern unsigned char rx_buf[MAX_IRC_MSG_LEN];
+extern unsigned short bw;
+extern unsigned char err;
+extern unsigned char trip;
+
+extern unsigned int txbuflen;
+extern char tx_buf[MAX_IRC_MSG_LEN];
+
+/* clears the transmit buffer. */
+void txbuf_init(void);
+
+/* appends a string to the transmit buffer, updates txbuflen. */
+void txbuf_append_str(const char *str);
+
+/* clears the transmit buffer, then appends a string to it. */
+void txbuf_set_str(const char *str);
+
+/* sends whatever's in the transmit buffer, then clears it. if nothing was
+ in the buffer, nothing gets sent. */
+void txbuf_send(void);
+
+/* sends a string. clears transmit buffer first, then clears it again on exit. */
+void txbuf_send_str(const char *str);
+
+void print_error(unsigned char err);
+
+/**** irc.c */
+#define MAX_MSG_ARGS 8
+extern char *msg_src, *msg_cmd, *msg_dest, *msg_text;
+extern char *msg_args[MAX_MSG_ARGS];
+extern int msg_argcount;
+
+/* call this once, right after TCP connection is established. */
+void irc_register(void);
+
+/* does all the work. doesn't return until we get disconnected from
+ the IRC server (via /quit or error). */
+void irc_loop(void);
+
+/**** ui.c */
+void ui_init(void);
+void ui_start_msg(void);
+void ui_end_msg(void);
+void ui_keystroke(void);
+void ui_print(const char *str);
+void ui_putchar(char c);
+
+/**** cmd.c */
+void cmd_command(const char *cmd);
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..81f598e
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,311 @@
+/* FujiNetChat, an IRC client. Based on NetCat and the old FujiChat. */
+
+#define SELF "FujiNetChat"
+#define VERSION "0.0"
+#define BANNER SELF " v" VERSION " (B. Watson)\n"
+
+#define DEF_URL "N:TCP://irc.libera.chat:6667"
+#define DEF_NICK "FNChatTest"
+#define DEF_CHANNEL "##atari"
+
+#include <atari.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <conio.h> // for kbhit() and cgetc()
+#include "conio.h" // our local one.
+#include "nio.h"
+#include "irc.h"
+
+char url[256] = DEF_URL; // URL
+char usernick[32] = DEF_NICK;
+char tmp[8]; // temporary # to string
+unsigned char err; // error code of last operation.
+unsigned char trip=0; // if trip=1, fujinet is asking us for attention.
+bool old_enabled=false; // were interrupts enabled for old vector
+void* old_vprced; // old PROCEED vector, restored on exit.
+unsigned short bw=0; // # of bytes waiting.
+unsigned char rx_buf[MAX_IRC_MSG_LEN]; // RX buffer.
+unsigned char tx_buf[MAX_IRC_MSG_LEN]; // TX buffer.
+unsigned int txbuflen; // TX buffer length
+char channel[32] = DEF_CHANNEL;
+
+/* TODO: user modes (default +iw), fg/bg color... */
+
+extern void ih(); // defined in intr.s
+
+static void strcpy_to_eol(char *dst, const char *src) {
+ while(*src && (*src != CH_EOL)) {
+ *dst++ = *src++;
+ }
+ *dst = '\0';
+}
+
+/**
+ * Get URL from user.
+ */
+void get_config(void) {
+ OS.crsinh = 0;
+
+ putchar(CH_CLR);
+ print(BANNER);
+
+ while(1) {
+ print("\nURL [");
+ print(url);
+ print("]?\n");
+ get_line(tx_buf, sizeof(url) - 1);
+ if(tx_buf[0] != CH_EOL) strcpy_to_eol(url, tx_buf);
+
+ print("Nick [");
+ print(usernick);
+ print("]? ");
+ get_line(tx_buf, sizeof(usernick) - 1);
+ if(tx_buf[0] != CH_EOL) strcpy_to_eol(usernick, tx_buf);
+
+ print("Channel [");
+ print(channel);
+ print("]? ");
+ get_line(tx_buf, sizeof(channel) - 1);
+ if(tx_buf[0] != CH_EOL) strcpy_to_eol(channel, tx_buf);
+
+ /*
+ print("\n\nURL: ");
+ print(url);
+ print("\nNick: ");
+ print(usernick);
+ print("\nChannel: ");
+ print(channel);
+ */
+
+ print("\n\nAre these settings OK [Y/n]? ");
+ if(tolower(cgetc()) != 'n') break;
+ }
+
+ // print("Press Return to connect\n");
+ // cgetc();
+}
+
+/**
+ * Print error
+ */
+void print_error(unsigned char err) {
+ itoa(err, tmp, 10);
+ print(tmp);
+ print("\n");
+}
+
+void txbuf_init(void) {
+ txbuflen = tx_buf[0] = 0;
+}
+
+void txbuf_append_str(const char *str) {
+ while(*str) {
+ tx_buf[txbuflen++] = *str++;
+ }
+}
+
+void txbuf_set_str(const char *str) {
+ txbuf_init();
+ txbuf_append_str(str);
+}
+
+void txbuf_send(void) {
+ if(!txbuflen) return;
+ nwrite(url, tx_buf, txbuflen);
+ txbuf_init();
+}
+
+void txbuf_send_str(const char *str) {
+ txbuf_init();
+ txbuf_append_str(str);
+ txbuf_send();
+}
+
+int fn_connect(void) {
+ print("\n" "Connecting to: ");
+ print(url);
+ print("\n");
+
+ err = nopen(url, FNET_TRANSLATION);
+
+ if(err != SUCCESS) {
+ print("Connection failed: ");
+ print_error(err);
+ return 0;
+ }
+
+ // Open successful, set up interrupt
+ old_vprced = OS.vprced; // save the old interrupt vector
+ old_enabled = PIA.pactl & 1; // keep track of old interrupt state
+ PIA.pactl &= (~1); // Turn off interrupts before changing vector
+ OS.vprced = ih; // Set PROCEED interrupt vector to our interrupt handler.
+ PIA.pactl |= 1; // Indicate to PIA we are ready for PROCEED interrupt.
+
+ return 1;
+}
+
+void fn_disconnect(void) {
+ // Restore old PROCEED interrupt.
+ PIA.pactl &= ~1; // disable interrupts
+ OS.vprced=old_vprced;
+ PIA.pactl |= old_enabled;
+}
+
+int main(void) {
+ OS.lmargn = 0; // Set left margin to 0
+ OS.shflok = 0; // turn off shift-lock.
+ OS.soundr = 0; // Turn off SIO beeping sound
+ cursor(1); // Keep cursor on
+
+ while(1) {
+ get_config();
+ if(fn_connect()) {
+ irc_register();
+ irc_loop();
+ fn_disconnect();
+ }
+ }
+
+ OS.soundr = 3; // Restore SIO beeping sound
+ return 0;
+}
+
+/* cruft from netcat: */
+/**
+ * Main entrypoint
+ */
+#if 0
+int main(int argc, char* argv[])
+{
+ OS.soundr=0; // Turn off SIO beeping sound
+ cursor(1); // Keep cursor on
+
+ while (running==true)
+ {
+ if (get_url(argc, argv))
+ nc();
+ else
+ running=false;
+ }
+
+ OS.soundr=3; // Restore SIO beeping sound
+ return 0;
+}
+#endif
+
+#if 0
+/**
+ * NetCat
+ */
+void nc()
+{
+ OS.lmargn=0; // Set left margin to 0
+ OS.shflok=0; // turn off shift-lock.
+
+ // Attempt open.
+ print("\x9bOpening:\x9b");
+ print(url);
+ print("\x9b");
+
+ err=nopen(url,trans);
+
+ if (err != SUCCESS)
+ {
+ print("OPEN ERROR: ");
+ print_error(err);
+ return;
+ }
+
+ // Open successful, set up interrupt
+ old_vprced = OS.vprced; // save the old interrupt vector
+ old_enabled = PIA.pactl & 1; // keep track of old interrupt state
+ PIA.pactl &= (~1); // Turn off interrupts before changing vector
+ OS.vprced = ih; // Set PROCEED interrupt vector to our interrupt handler.
+ PIA.pactl |= 1; // Indicate to PIA we are ready for PROCEED interrupt.
+
+ // MAIN LOOP ///////////////////////////////////////////////////////////
+
+ while (running==true)
+ {
+ // If key pressed, send it.
+ while (kbhit())
+ {
+ tx_buf[txbuflen++]=cgetc();
+ }
+
+ if (txbuflen>0)
+ {
+ if (echo==true)
+ for (i=0;i<txbuflen;i++)
+ printc(&tx_buf[i]);
+
+ err=nwrite(url,tx_buf,txbuflen); // Send character.
+
+ if (err!=1)
+ {
+ print("WRITE ERROR: ");
+ print_error(err);
+ running=false;
+ continue;
+ }
+ txbuflen=0;
+ }
+
+ if (trip==0) // is nothing waiting for us?
+ continue;
+
+ // Something waiting for us, get status and bytes waiting.
+ err=nstatus(url);
+
+ if (err==136)
+ {
+ print("DISCONNECTED.\x9b");
+ running=false;
+ continue;
+ }
+ else if (err!=1)
+ {
+ print("STATUS ERROR: ");
+ print_error(err);
+ running=false;
+ continue;
+ }
+
+ // Get # of bytes waiting, no more than size of rx_buf
+ bw=OS.dvstat[1]*256+OS.dvstat[0];
+
+ if (bw>sizeof(rx_buf))
+ bw=sizeof(rx_buf);
+
+ if (bw>0)
+ {
+ err=nread(url,rx_buf,bw);
+
+ if (err!=1)
+ {
+ print("READ ERROR: ");
+ print_error(err);
+ running=false;
+ continue;
+ }
+
+ // Print the buffer to screen.
+ printl(rx_buf,bw);
+
+ trip=0;
+ PIA.pactl |= 1; // Flag interrupt as serviced, ready for next one.
+ } // if bw > 0
+ } // while running
+
+ // END MAIN LOOP ///////////////////////////////////////////////////////
+
+ // Restore old PROCEED interrupt.
+ PIA.pactl &= ~1; // disable interrupts
+ OS.vprced=old_vprced;
+ PIA.pactl |= old_enabled;
+
+}
+#endif
diff --git a/src/nio.c b/src/nio.c
new file mode 100644
index 0000000..1a5b14c
--- /dev/null
+++ b/src/nio.c
@@ -0,0 +1,183 @@
+/**
+ * N: I/O
+ */
+
+#include "nio.h"
+#include "sio.h"
+#include <atari.h>
+#include <stddef.h>
+
+#define TIMEOUT 0x1f /* approx 30 seconds */
+
+unsigned char nunit(char* devicespec)
+{
+ unsigned char unit=1;
+
+ // Set unit to 1 unless explicitly specified.
+ if (devicespec[1]==':')
+ unit=1;
+ else if (devicespec[2]==':')
+ unit=devicespec[1]-0x30; // convert from alpha to integer.
+ else
+ unit=1;
+
+ return unit;
+}
+
+unsigned char nopen(char* devicespec, unsigned char trans)
+{
+ unsigned char unit=nunit(devicespec);
+
+ OS.dcb.ddevic = DFUJI; // Fuji Device Identifier
+ OS.dcb.dunit = unit; // Unit number integer 1 through 4
+ OS.dcb.dcomnd = 'O'; // Open
+ OS.dcb.dstats = DWRITE; // sending to to SIO device
+ OS.dcb.dbuf = devicespec; // eg: N:TCP//
+ OS.dcb.dtimlo = TIMEOUT; // approximately 30 second timeout
+ OS.dcb.dbyt = 256; // max size of our device spec
+ OS.dcb.daux1 = OUPDATE; // Read and write
+ OS.dcb.daux2 = trans; // CR/LF translation
+ siov();
+
+ if (OS.dcb.dstats!=SUCCESS)
+ {
+ // something went wrong
+ // do we need to return extended status?
+ if (OS.dcb.dstats==DERROR)
+ {
+ nstatus(devicespec);
+ return OS.dvstat[DVSTAT_EXTENDED_ERROR]; // return extended error.
+ }
+ }
+ return OS.dcb.dstats; // Return SIO error or success
+}
+
+unsigned char nclose(char* devicespec)
+{
+ unsigned char unit=nunit(devicespec);
+
+ OS.dcb.ddevic = DFUJI;
+ OS.dcb.dunit = unit;
+ OS.dcb.dcomnd = 'C'; // Close
+ OS.dcb.dstats = 0x00;
+ OS.dcb.dbuf = NULL;
+ OS.dcb.dtimlo = TIMEOUT;
+ OS.dcb.dbyt = 0;
+ OS.dcb.daux = 0;
+ siov();
+
+ if (OS.dcb.dstats!=SUCCESS)
+ {
+ // something went wrong
+ // do we need to return extended status?
+ if (OS.dcb.dstats==DERROR)
+ {
+ nstatus(devicespec);
+ return OS.dvstat[DVSTAT_EXTENDED_ERROR]; // return extended error.
+ }
+ }
+ return OS.dcb.dstats; // Return SIO error or success.
+}
+
+unsigned char nstatus(char* devicespec)
+{
+ unsigned char unit=nunit(devicespec);
+
+ OS.dcb.ddevic = DFUJI;
+ OS.dcb.dunit = unit;
+ OS.dcb.dcomnd = 'S'; // status
+ OS.dcb.dstats = DREAD;
+ OS.dcb.dbuf = OS.dvstat;
+ OS.dcb.dtimlo = TIMEOUT;
+ OS.dcb.dbyt = sizeof(OS.dvstat);
+ OS.dcb.daux = 0;
+ siov();
+
+ return OS.dvstat[DVSTAT_EXTENDED_ERROR]; // return extended status
+}
+
+unsigned char nread(char* devicespec, unsigned char* buf, unsigned short len)
+{
+ unsigned char unit=nunit(devicespec);
+
+ OS.dcb.ddevic = DFUJI;
+ OS.dcb.dunit = unit;
+ OS.dcb.dcomnd = 'R'; // read
+ OS.dcb.dstats = DREAD;
+ OS.dcb.dbuf = buf;
+ OS.dcb.dtimlo = TIMEOUT;
+ OS.dcb.dbyt = OS.dcb.daux = len; // Set the buffer size AND daux with length
+ siov();
+
+ if (OS.dcb.dstats!=SUCCESS)
+ {
+ // something went wrong
+ // do we need to return extended status?
+ if (OS.dcb.dstats==DERROR)
+ {
+ nstatus(devicespec);
+ return OS.dvstat[DVSTAT_EXTENDED_ERROR]; // return extended error.
+ }
+ }
+ return OS.dcb.dstats; // Return SIO error or success.
+}
+
+unsigned char nwrite(char* devicespec, unsigned char* buf, unsigned short len)
+{
+ unsigned char unit=nunit(devicespec);
+
+ OS.dcb.ddevic = DFUJI;
+ OS.dcb.dunit = unit;
+ OS.dcb.dcomnd = 'W'; // write
+ OS.dcb.dstats = DWRITE;
+ OS.dcb.dbuf = buf;
+ OS.dcb.dtimlo = TIMEOUT;
+ OS.dcb.dbyt = OS.dcb.daux = len;
+ siov();
+
+ if (OS.dcb.dstats!=SUCCESS)
+ {
+ // something went wrong
+ // do we need to return extended status?
+ if (OS.dcb.dstats==DERROR)
+ {
+ nstatus(devicespec);
+ return OS.dvstat[DVSTAT_EXTENDED_ERROR]; // return extended error.
+ }
+ }
+ return OS.dcb.dstats; // Return SIO error or success.
+}
+
+unsigned char nlogin(char* devicespec, char *login, char *password)
+{
+ unsigned char unit=nunit(devicespec);
+
+ OS.dcb.ddevic=0x71;
+ OS.dcb.dunit=unit;
+ OS.dcb.dcomnd=0xFD;
+ OS.dcb.dstats=0x80;
+ OS.dcb.dbuf=login;
+ OS.dcb.dtimlo=0x1f;
+ OS.dcb.dbyt=256;
+ OS.dcb.daux=0;
+ siov();
+
+ if (OS.dcb.dstats!=1)
+ {
+ nstatus(devicespec);
+ return OS.dvstat[DVSTAT_EXTENDED_ERROR]; // return ext err
+ }
+
+ OS.dcb.dcomnd=0xFE;
+ OS.dcb.dstats=0x80;
+ OS.dcb.dbuf=password;
+ siov();
+
+ if (OS.dcb.dstats!=1)
+ {
+ nstatus(devicespec);
+ return OS.dvstat[DVSTAT_EXTENDED_ERROR]; // return ext err
+ }
+
+ return OS.dcb.dstats;
+}
diff --git a/src/nio.h b/src/nio.h
new file mode 100644
index 0000000..df992a2
--- /dev/null
+++ b/src/nio.h
@@ -0,0 +1,79 @@
+/**
+ * N: I/O
+ */
+
+#ifndef NIO_H
+#define NIO_H
+
+#define DFUJI 0x71
+#define DREAD 0x40
+#define DWRITE 0x80
+#define DUPDATE 0xC0
+
+#define OREAD 0x04
+#define OWRITE 0x08
+#define OUPDATE 0x0C
+
+#define SUCCESS 1
+#define DERROR 144
+
+#define DVSTAT_BYTES_WATING_LO 0
+#define DVSTAT_BYTES_WATING_HI 1
+#define DVSTAT_PROTOCOL 2
+#define DVSTAT_EXTENDED_ERROR 3
+
+/**
+ * Open N: device with devicespec
+ * @param devicespec - an N: device spec, e.g. N:TCP://FOO.COM:1234/
+ * @param translation mode, 0=none, 1=cr, 2=lf, 3=cr/lf
+ * @return error code, or 1 if successful.
+ */
+unsigned char nopen(char* devicespec, unsigned char trans);
+
+/**
+ * Close N: device with devicespec
+ * @param devicespec - an N: device spec to close (the unit number is extracted)
+ * @return error code, or 1 if successful.
+ */
+unsigned char nclose(char* devicespec);
+
+/**
+ * Get status of specific N: device
+ * @param devicespec - an N: device spec to status (the unit number is extracted)
+ * @return error code, or 1 if successful, DVSTAT is also filled with status info.
+ *
+ * Format of DVSTAT:
+ * OS.dcb.dvstat[0] = # of bytes waiting LO
+ * OS.dcb.dvstat[1] = # of bytes waiting HI
+ * OS.dcb.dvstat[2] = reserved
+ * OS.dcb.dvstat[3] = Error code of last I/O operation. !1 = error.
+ */
+unsigned char nstatus(char* devicespec);
+
+/**
+ * Read # of bytes from specific N: device.
+ * @param devicespec - an N: device spec to read bytes from.
+ * @param buf - The buffer to read into, must be at least as big as len.
+ * @param len - The # of bytes to read (up to 65535)
+ * @return error code, or 1 if successful, buf is filled with data.
+ */
+unsigned char nread(char* devicespec, unsigned char* buf, unsigned short len);
+
+/**
+ * Write # of bytes to specific N: device.
+ * @param devicespec - an N: device spec to write to.
+ * @param buf - The buffer to write to device, should be at least as big as len.
+ * @param len - The # of bytes to write (up to 65535)
+ * @return error code, or 1 if successful, buf is filled with data.
+ */
+unsigned char nwrite(char* devicespec, unsigned char* buf, unsigned short len);
+
+/**
+ * Send username and password credentials
+ * @param devicespec - The devicespec.
+ * @param login - The username to send
+ * @param password - The password to send
+ */
+unsigned char nlogin(char* devicespec, char* login, char* password);
+
+#endif /* NIO_H */
diff --git a/src/sio.h b/src/sio.h
new file mode 100644
index 0000000..87b787e
--- /dev/null
+++ b/src/sio.h
@@ -0,0 +1,7 @@
+/**
+ * Function to call sio
+ */
+
+void siov();
+void rtclr();
+void cold_start();
diff --git a/src/sio.s b/src/sio.s
new file mode 100644
index 0000000..3dd9191
--- /dev/null
+++ b/src/sio.s
@@ -0,0 +1,17 @@
+ ;; Call SIO
+
+ .export _siov
+ .export _rtclr
+ .export _cold_start
+
+_siov: JSR $E459
+ RTS
+
+_rtclr: LDA #$00
+ STA $12
+ STA $13
+ STA $14
+ RTS
+
+_cold_start:
+ JMP $E477
diff --git a/src/ui.c b/src/ui.c
new file mode 100644
index 0000000..68f32a0
--- /dev/null
+++ b/src/ui.c
@@ -0,0 +1,73 @@
+#include <atari.h>
+#include <stdio.h>
+#include "irc.h"
+#include <conio.h>
+#include "conio.h"
+
+#define MAX_INPUT_LEN 119
+
+static char input_buffer[MAX_INPUT_LEN + 1];
+static short inbuf_len = 0;
+
+void ui_init(void) {
+ OS.escflg = 0;
+ inbuf_len = input_buffer[0] = 0;
+}
+
+void ui_start_msg() {
+ /* TODO: use msg_src and msg_dest to decide which window to
+ print to (when we have multi-window support) */
+ OS.crsinh = 1;
+ putchar(CH_DELLINE);
+}
+
+void ui_end_msg(void) {
+ OS.crsinh = 0;
+ // putchar(CH_EOL); // NO!
+ if(inbuf_len) print(input_buffer);
+}
+
+void ui_print(const char *str) {
+ OS.escflg = 0x80;
+ print(str);
+ OS.escflg = 0;
+}
+
+void ui_putchar(char c) {
+ putchar(c);
+}
+
+void ui_keystroke(void) {
+ char c;
+
+ OS.escflg = 0x80;
+
+ /* pressing ctrl-3 (aka EOF) crashes cc65-compiled binaries *hard*,
+ so don't allow it. */
+ if(OS.ch == (KEY_3 | KEY_CTRL)) {
+ OS.ch = KEY_NONE;
+ return;
+ }
+
+ c = cgetc();
+
+ if(c == CH_EOL && !inbuf_len)
+ return; /* ignore empty message */
+
+ if(c == CH_DEL && inbuf_len) {
+ OS.escflg = 0;
+ putchar(c);
+ OS.escflg = 0x80;
+ input_buffer[inbuf_len--] = 0;
+ } else if(inbuf_len == MAX_INPUT_LEN) {
+ return; /* ignore */
+ } else {
+ putchar(c);
+ input_buffer[inbuf_len++] = c;
+ input_buffer[inbuf_len] = 0;
+ if(c == CH_EOL) {
+ cmd_command(input_buffer);
+ ui_init();
+ }
+ }
+}