$ git clone http://tcclient.ion.nu/tc_client.git
commit 9aeeee6721914080d2b90a9f1920fd60a40639b6
Author: Alicia <...>
Date:   Tue Apr 7 06:49:01 2015 +0200

    Version 0.30

diff --git a/ChangeLog b/ChangeLog
index e64b4f4..d887e2f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,14 @@
+0.30:
+Adjusted the configure script for compatibility with shells that still interpret escaped characters in single-quotes (contributed by Jade)
+Added a /topic command to set the channel topic.
+tc_client-gtk: allow scrollbars around the cams, otherwise resizing the window to a smaller size will be blocked.
+tc_client-gtk: added optional notification sounds and a settings window to configure it.
+tc_client-gtk: added optional logging.
+tc_client-gtk: added a startup dialog instead of requiring commandline arguments.
+tc_client-gtk: display the color of our outgoing messages.
+tc_client-gtk: added an option to run a command for youtube videos being played.
+cursedchat: keep track of our own nickname and print that when we send a message instead of "You:" (contributed by Pamela)
+cursedchat: added timestamps for outgoing messages.
 0.29:
 Added testbuilds.sh to automatically try building the project with different configurations to verify that the latest changes did not break it in some cases.
 Check for endianness macros in the configure script and if they are missing/broken, test endianness and add our own macros.
diff --git a/Makefile b/Makefile
index 19a173a..fd09f32 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.29
+VERSION=0.30
 CFLAGS=-g3 -Wall $(shell curl-config --cflags)
 LIBS=-g3 $(shell curl-config --libs)
 ifneq ($(wildcard config.mk),)
@@ -9,7 +9,7 @@ IRCHACK_OBJ=utilities/irchack/irchack.o utilities/compat.o
 MODBOT_OBJ=utilities/modbot/modbot.o utilities/list.o utilities/modbot/queue.o
 CAMVIEWER_OBJ=utilities/camviewer/camviewer.o
 CURSEDCHAT_OBJ=utilities/cursedchat/cursedchat.o utilities/cursedchat/buffer.o utilities/compat.o utilities/list.o
-TC_CLIENT_GTK_OBJ=utilities/gtk/camviewer.o utilities/gtk/userlist.o utilities/gtk/media.o utilities/gtk/compat.o
+TC_CLIENT_GTK_OBJ=utilities/gtk/camviewer.o utilities/gtk/userlist.o utilities/gtk/media.o utilities/gtk/compat.o utilities/gtk/config.o utilities/gtk/gui.o utilities/stringutils.o utilities/gtk/logging.o
 UTILS=irchack modbot
 ifdef GTK_LIBS
 ifdef AVCODEC_LIBS
@@ -62,5 +62,12 @@ tc_client-gtk: $(TC_CLIENT_GTK_OBJ)
 clean:
  rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) $(CURSEDCHAT_OBJ) $(TC_CLIENT_GTK_OBJ) tc_client irchack modbot camviewer cursedchat tc_client-gtk
 
+SOURCES=Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c media.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h media.h LICENSE README ChangeLog crossbuild.sh testbuilds.sh configure
+SOURCES+=utilities/irchack/irchack.c
+SOURCES+=utilities/modbot/modbot.c utilities/modbot/queue.c utilities/modbot/queue.h utilities/modbot/commands.html
+SOURCES+=utilities/camviewer/camviewer.c
+SOURCES+=utilities/cursedchat/cursedchat.c utilities/cursedchat/buffer.c utilities/cursedchat/buffer.h
+SOURCES+=utilities/gtk/camviewer.c utilities/gtk/userlist.c utilities/gtk/media.c utilities/gtk/compat.c utilities/gtk/config.c utilities/gtk/gui.c utilities/gtk/logging.c utilities/gtk/userlist.h utilities/gtk/media.h utilities/gtk/compat.h utilities/gtk/config.h utilities/gtk/gui.h utilities/gtk/logging.h gtkgui.glade
+SOURCES+=utilities/compat.c utilities/compat.h utilities/list.c utilities/list.h utilities/stringutils.c utilities/stringutils.h
 tarball:
- tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c media.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h media.h LICENSE README ChangeLog crossbuild.sh testbuilds.sh utilities/irchack/irchack.c utilities/modbot/modbot.c utilities/modbot/queue.c utilities/modbot/queue.h utilities/modbot/commands.html utilities/camviewer/camviewer.c utilities/cursedchat/cursedchat.c utilities/cursedchat/buffer.c utilities/cursedchat/buffer.h utilities/gtk/camviewer.c utilities/gtk/userlist.c utilities/gtk/media.c utilities/gtk/compat.c utilities/gtk/userlist.h utilities/gtk/media.h utilities/gtk/compat.h utilities/compat.c utilities/compat.h utilities/list.c utilities/list.h gtkgui.glade configure
+ tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' $(SOURCES)
diff --git a/README b/README
index b4aa87e..9977e08 100644
--- a/README
+++ b/README
@@ -17,6 +17,7 @@ Commands supported by tc_client:
 /camup          = open an audio/video stream for broadcasting your video
 /video <length> = send a <length> bytes long encoded frame, send the frame data after this line
 /camdown        = close the audio/video stream
+/topic <topic>  = set the channel topic
 /help           = list these commands at runtime
 
 Current commands sent by the TC servers that tc_client doesn't know how to handle:
@@ -24,8 +25,15 @@ notice (some, notice is used for many functions)
 joinsdone
 pros
 
+Some notes about the code:
+In tc_client (and the utilities) we often use &string[offset], which can be confusing to some.
+We use it to get the part of the string after the first <offset> characters, e.g. we have a string that says "Connection ID: 562", we do &string[15] to get only the "562" part.
+The order of operations is as follows: &(string[offset])
+&string[offset] is equivalent to string+offset
+
 Included applications that rely on tc_client (type 'make utils' to build):
-*irchack    = a minimal IRC server that translates between IRC and tc_client's commands
-*modbot     = a bot that handles youtube video requests with a queue and an approval system to keep inappropriate videos from being played. Supports the following commands: !help, !request, !queue (show queue status), for mods only: !playnext (try playing next in queue without marking it yet), !approve, !badvid (Note: modbot depends on youtube-dl to find out the length of videos)
-*camviewer  = an example application for viewing cam streams
-*cursedchat = a curses chat interface
+*irchack       = a minimal IRC server that translates between IRC and tc_client's commands
+*modbot        = a bot that handles youtube video requests with a queue and an approval system to keep inappropriate videos from being played. Supports the following commands: !help, !request, !queue (show queue status), for mods only: !playnext (try playing next in queue without marking it yet), !approve, !badvid (Note: modbot depends on youtube-dl to find out the length of videos)
+*camviewer     = an example application for viewing cam streams
+*cursedchat    = a curses chat interface
+*tc_client-gtk = a more complete and full-featured interface based on camviewer
diff --git a/client.c b/client.c
index 1f55248..f207e31 100644
--- a/client.c
+++ b/client.c
@@ -89,16 +89,16 @@ char* gethost(char *channel, char *password)
   int urllen;
   if(password)
   {
-    urllen=strlen("http://tinychat.com/api/find.room/?site=tinychat&password=0")+strlen(channel)+strlen(password);
+    urllen=strlen("http://apl.tinychat.com/api/find.room/?site=tinychat&password=0")+strlen(channel)+strlen(password);
   }else{
-    urllen=strlen("http://tinychat.com/api/find.room/?site=tinychat0")+strlen(channel);
+    urllen=strlen("http://apl.tinychat.com/api/find.room/?site=tinychat0")+strlen(channel);
   }
   char url[urllen];
   if(password)
   {
-    sprintf(url, "http://tinychat.com/api/find.room/%s?site=tinychat&password=%s", channel, password);
+    sprintf(url, "http://apl.tinychat.com/api/find.room/%s?site=tinychat&password=%s", channel, password);
   }else{
-    sprintf(url, "http://tinychat.com/api/find.room/%s?site=tinychat", channel);
+    sprintf(url, "http://apl.tinychat.com/api/find.room/%s?site=tinychat", channel);
   }
   char* response=http_get(url, 0);
   if(!response){exit(-1);}
@@ -122,8 +122,8 @@ char* gethost(char *channel, char *password)
 
 char* getkey(const char* id, const char* channel)
 {
-  char url[strlen("http://tinychat.com/api/captcha/check.php?guest%5Fid=&room=tinychat%5E0")+strlen(id)+strlen(channel)];
-  sprintf(url, "http://tinychat.com/api/captcha/check.php?guest%%5Fid=%s&room=tinychat%%5E%s", id, channel);
+  char url[strlen("http://apl.tinychat.com/api/captcha/check.php?guest%5Fid=&room=tinychat%5E0")+strlen(id)+strlen(channel)];
+  sprintf(url, "http://apl.tinychat.com/api/captcha/check.php?guest%%5Fid=%s&room=tinychat%%5E%s", id, channel);
   char* response=http_get(url, 0);
   char* key=strstr(response, "\"key\":\"");
 
@@ -145,8 +145,8 @@ char* getkey(const char* id, const char* channel)
 char* getbroadcastkey(const char* channel, const char* nick)
 {
   unsigned int id=idlist_get(nick);
-  char url[strlen("http://tinychat.com/api/broadcast.pw?name=&site=tinychat&nick=&id=0")+128+strlen(channel)+strlen(nick)];
-  sprintf(url, "http://tinychat.com/api/broadcast.pw?name=%s&site=tinychat&nick=%s&id=%u", channel, nick, id);
+  char url[strlen("http://apl.tinychat.com/api/broadcast.pw?name=&site=tinychat&nick=&id=0")+128+strlen(channel)+strlen(nick)];
+  sprintf(url, "http://apl.tinychat.com/api/broadcast.pw?name=%s&site=tinychat&nick=%s&id=%u", channel, nick, id);
   char* response=http_get(url, 0);
   char* key=strstr(response, " token='");
 
@@ -444,7 +444,8 @@ int main(int argc, char** argv)
                  "/push2talk      = puts all non-operators in push2talk mode\n"
                  "/camup          = open an audio/video stream for broadcasting your video\n"
                  "/camdown        = close the broadcasting stream\n"
-                 "/video <length> = send a <length> bytes long encoded frame, send the frame data after this line\n");
+                 "/video <length> = send a <length> bytes long encoded frame, send the frame data after this line\n"
+                 "/topic <topic>  = set the channel topic\n");
           fflush(stdout);
           continue;
         }
@@ -614,6 +615,17 @@ int main(int argc, char** argv)
           stream_sendvideo(sock, buf, len);
           continue;
         }
+        else if(!strncmp(buf, "/topic ", 7))
+        {
+          amfinit(&amf, 3);
+          amfstring(&amf, "topic");
+          amfnum(&amf, 0);
+          amfnull(&amf);
+          amfstring(&amf, &buf[7]);
+          amfstring(&amf, "");
+          amfsend(&amf, sock);
+          continue;
+        }
       }
       char* msg=tonumlist(buf, len);
       amfinit(&amf, 3);
diff --git a/configure b/configure
index 78aaae3..6d37dbf 100755
--- a/configure
+++ b/configure
@@ -170,9 +170,10 @@ else
   echo no
   printf 'Checking endianness and writing our own macros... '
   echo '#include <stdio.h>' > endiantest.c
-  echo 'int main(){int num=1;char* x=(char*)&num;if(x[0]){printf("CFLAGS+=-D__ORDER_LITTLE_ENDIAN__=1 -D__ORDER_BIG_ENDIAN__=2 -D__BYTE_ORDER__=1\n");}else{printf("CFLAGS+=-D__ORDER_LITTLE_ENDIAN__=1 -D__ORDER_BIG_ENDIAN__=2 -D__BYTE_ORDER__=2\n");}return 0;}' >> endiantest.c
+  echo 'int main(){int num=1;char* x=(char*)&num;if(x[0]){printf("CFLAGS+=-D__ORDER_LITTLE_ENDIAN__=1 -D__ORDER_BIG_ENDIAN__=2 -D__BYTE_ORDER__=1");}else{printf("CFLAGS+=-D__ORDER_LITTLE_ENDIAN__=1 -D__ORDER_BIG_ENDIAN__=2 -D__BYTE_ORDER__=2");}return 0;}' >> endiantest.c
   "$CC" endiantest.c -o endiantest > /dev/null 2> /dev/null
   ./endiantest >> config.mk
+  echo >> config.mk
   echo done
 fi
 rm -f endiantest.c endiantest
diff --git a/gtkgui.glade b/gtkgui.glade
index 9661ca8..8a12566 100644
--- a/gtkgui.glade
+++ b/gtkgui.glade
@@ -2,11 +2,48 @@
 <!-- Generated with glade 3.18.3 -->
 <interface>
   <requires lib="gtk+" version="3.0"/>
+  <object class="GtkWindow" id="channelpasswordwindow">
+    <property name="can_focus">False</property>
+    <property name="title" translatable="yes">Channel password?</property>
+    <signal name="destroy" handler="gtk_main_quit" swapped="no"/>
+    <child>
+      <object class="GtkBox" id="box4">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <child>
+          <object class="GtkEntry" id="channelpassword">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="visibility">False</property>
+            <property name="input_purpose">password</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="channelpasswordbutton">
+            <property name="label" translatable="yes">Enter</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
   <object class="GtkWindow" id="main">
     <property name="can_focus">False</property>
     <property name="title" translatable="yes">tc_client</property>
-    <property name="default_width">200</property>
-    <property name="default_height">300</property>
+    <property name="default_width">300</property>
+    <property name="default_height">400</property>
     <signal name="destroy" handler="gtk_main_quit" swapped="no"/>
     <child>
       <object class="GtkBox" id="vbox1">
@@ -38,6 +75,28 @@
                 </child>
               </object>
             </child>
+            <child>
+              <object class="GtkMenuItem" id="menuitem1">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">Options</property>
+                <property name="use_underline">True</property>
+                <child type="submenu">
+                  <object class="GtkMenu" id="menu1">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkMenuItem" id="menuitem_options_settings">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="yes">Settings</property>
+                        <property name="use_underline">True</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
           </object>
           <packing>
             <property name="expand">False</property>
@@ -54,8 +113,6 @@
               <object class="GtkScrolledWindow" id="camerascroll">
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
-                <property name="hscrollbar_policy">never</property>
-                <property name="vscrollbar_policy">never</property>
                 <child>
                   <object class="GtkViewport" id="viewport1">
                     <property name="visible">True</property>
@@ -142,6 +199,7 @@
                   <object class="GtkEntry" id="inputfield">
                     <property name="visible">True</property>
                     <property name="can_focus">True</property>
+                    <property name="has_focus">True</property>
                   </object>
                   <packing>
                     <property name="expand">False</property>
@@ -165,4 +223,526 @@
       </object>
     </child>
   </object>
+  <object class="GtkWindow" id="settings">
+    <property name="can_focus">False</property>
+    <property name="title" translatable="yes">tc_client settings</property>
+    <property name="default_width">350</property>
+    <property name="default_height">200</property>
+    <signal name="delete-event" handler="gtk_widget_hide_on_delete" swapped="no"/>
+    <child>
+      <object class="GtkBox" id="box1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkNotebook" id="pager">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="tab_pos">left</property>
+            <property name="scrollable">True</property>
+            <child>
+              <object class="GtkScrolledWindow" id="scrolledwindow1">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="shadow_type">in</property>
+                <child>
+                  <object class="GtkViewport" id="viewport2">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkBox" id="box2">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <child>
+                          <object class="GtkCheckButton" id="enable_logging">
+                            <property name="label" translatable="yes">Enable logging</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="halign">start</property>
+                            <property name="xalign">0</property>
+                            <property name="draw_indicator">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="label3">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="label" translatable="yes">Path for channel: (%h=home directory, %c=channelname)</property>
+                            <property name="wrap">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="logpath_channel">
+                            <property name="visible">True</property>
+                            <property name="sensitive">False</property>
+                            <property name="can_focus">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">2</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="label4">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="label" translatable="yes">Path for PMs: (%h=home directory, %c=channelname, %n=nickname)</property>
+                            <property name="wrap">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">3</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="logpath_pm">
+                            <property name="visible">True</property>
+                            <property name="sensitive">False</property>
+                            <property name="can_focus">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">4</property>
+                          </packing>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child type="tab">
+              <object class="GtkLabel" id="label1">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">Logging</property>
+              </object>
+              <packing>
+                <property name="tab_fill">False</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="scrolledwindow2">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="shadow_type">in</property>
+                <child>
+                  <object class="GtkViewport" id="viewport3">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkBox" id="box3">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <child>
+                          <object class="GtkRadioButton" id="soundradio_none">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="halign">start</property>
+                            <property name="xalign">0</property>
+                            <property name="active">True</property>
+                            <property name="draw_indicator">True</property>
+                            <child>
+                              <object class="GtkLabel" id="label14">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="label" translatable="yes">Do not play notification sounds</property>
+                                <property name="wrap">True</property>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkRadioButton" id="soundradio_cmd">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="halign">start</property>
+                            <property name="xalign">0</property>
+                            <property name="draw_indicator">True</property>
+                            <property name="group">soundradio_none</property>
+                            <child>
+                              <object class="GtkLabel" id="label13">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="label" translatable="yes">Run a command to play sounds:</property>
+                                <property name="wrap">True</property>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="soundcmd">
+                            <property name="visible">True</property>
+                            <property name="sensitive">False</property>
+                            <property name="can_focus">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">2</property>
+                          </packing>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child type="tab">
+              <object class="GtkLabel" id="label2">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">Sound</property>
+              </object>
+              <packing>
+                <property name="position">1</property>
+                <property name="tab_fill">False</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="scrolledwindow3">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="shadow_type">in</property>
+                <child>
+                  <object class="GtkViewport" id="viewport4">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkBox" id="box5">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <child>
+                          <object class="GtkRadioButton" id="youtuberadio_none">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="halign">start</property>
+                            <property name="xalign">0</property>
+                            <property name="active">True</property>
+                            <property name="draw_indicator">True</property>
+                            <child>
+                              <object class="GtkLabel" id="label12">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="halign">start</property>
+                                <property name="label" translatable="yes">Do not play youtube videos</property>
+                                <property name="wrap">True</property>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkRadioButton" id="youtuberadio_cmd">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="halign">start</property>
+                            <property name="xalign">0</property>
+                            <property name="active">True</property>
+                            <property name="draw_indicator">True</property>
+                            <property name="group">youtuberadio_none</property>
+                            <child>
+                              <object class="GtkLabel" id="label11">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="halign">start</property>
+                                <property name="label" translatable="yes">Run a command to play youtube videos (%i=video ID, %t=offset in milliseconds):</property>
+                                <property name="wrap">True</property>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="youtubecmd">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">2</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="label15">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="label" translatable="yes">Example: youtube-dl -o - -- '%i' | mplayer -ss "`expr '%t' / 1000`" -</property>
+                            <property name="wrap">True</property>
+                            <property name="selectable">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">3</property>
+                          </packing>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="position">2</property>
+              </packing>
+            </child>
+            <child type="tab">
+              <object class="GtkLabel" id="label10">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">Youtube</property>
+              </object>
+              <packing>
+                <property name="position">2</property>
+                <property name="tab_fill">False</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="save_settings">
+            <property name="label" translatable="yes">OK</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="halign">end</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+  <object class="GtkWindow" id="startwindow">
+    <property name="can_focus">False</property>
+    <property name="title" translatable="yes">tc_client</property>
+    <property name="resizable">False</property>
+    <signal name="destroy" handler="gtk_main_quit" swapped="no"/>
+    <child>
+      <object class="GtkBox" id="box6">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkExpander" id="expander1">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <child>
+              <object class="GtkBox" id="box7">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="orientation">vertical</property>
+                <child>
+                  <object class="GtkLabel" id="label8">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">Username:</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="acc_username">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="label9">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">Password:</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="acc_password">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="visibility">False</property>
+                    <property name="input_purpose">password</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkCheckButton" id="start_rememberacc">
+                    <property name="label" translatable="yes">Remember account</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">False</property>
+                    <property name="xalign">0</property>
+                    <property name="draw_indicator">True</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">4</property>
+                  </packing>
+                </child>
+              </object>
+            </child>
+            <child type="label">
+              <object class="GtkLabel" id="label5">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">Account</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label6">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">start</property>
+            <property name="label" translatable="yes">Nickname:</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEntry" id="start_nick">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label7">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">start</property>
+            <property name="label" translatable="yes">Channel:</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">3</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEntry" id="start_channel">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">4</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkCheckButton" id="start_rememberchan">
+            <property name="label" translatable="yes">Remember nickname and channel</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">False</property>
+            <property name="xalign">0</property>
+            <property name="draw_indicator">True</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">5</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="connectbutton">
+            <property name="label" translatable="yes">Connect</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">6</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
 </interface>
diff --git a/utilities/cursedchat/cursedchat.c b/utilities/cursedchat/cursedchat.c
index c61bc98..6e5385c 100644
--- a/utilities/cursedchat/cursedchat.c
+++ b/utilities/cursedchat/cursedchat.c
@@ -1,6 +1,7 @@
 /*
     cursedchat, a simple curses interface for tc_client
     Copyright (C) 2015  alicia@ion.nu
+    Copyright (C) 2015  Pamela Hiatt
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU Affero General Public License as published by
@@ -35,6 +36,7 @@ char* channeltopic;
 WINDOW* input;
 int to_app;
 struct list userlist={0,0};
+char* nickname=0;
 
 // Translate ANSI escape codes to curses commands and write the text to a window
 void waddansi(WINDOW* w, char* str)
@@ -188,8 +190,9 @@ void gotline(char* line)
   }
   write(to_app, line, strlen(line));
   write(to_app, "\n", 1);
-// TODO: grab user's nick for this
-  wprintw(buffers[currentbuf].pad, "\n%s: %s", "You", line);
+  time_t timestamp=time(0);
+  struct tm* t=localtime(&timestamp);
+  wprintw(buffers[currentbuf].pad, "\n[%02i:%02i] %s: %s", t->tm_hour, t->tm_min, nickname, line);
   drawchat();
 }
 
@@ -391,6 +394,12 @@ int main(int argc, char** argv)
         channeltopic=strdup(&buf[12]);
         drawtopic();
       }
+      else if(!strncmp(buf, "Connection ID: ", 15)) // Our initial nickname is "guest-" plus our connection ID
+      {
+        unsigned int length=strlen(&buf[15]);
+        nickname=malloc(length+strlen("guest-")+1);
+        sprintf(nickname, "guest-%s", &(buf[15]));
+      }
       else if(!strncmp(buf, "Currently online: ", 18))
       {
         // Populate the userlist
@@ -436,8 +445,14 @@ int main(int argc, char** argv)
           msg[0]=0;
           // Update name in userlist
           list_switch(&userlist, nick, &msg[21]);
-          unsigned int i;
+          // If it was us, keep track of the new nickname
+          if(!strcmp(nickname, nick))
+          {
+            free(nickname);
+            nickname=strdup(&msg[21]);
+          }
           // Prevent duplicate names for buffers, and all the issues that would bring
+          unsigned int i;
           if((i=findbuffer(&msg[21])))
           {
             renamebufferunique(i);
diff --git a/utilities/gtk/camviewer.c b/utilities/gtk/camviewer.c
index c90ca37..dd65a8d 100644
--- a/utilities/gtk/camviewer.c
+++ b/utilities/gtk/camviewer.c
@@ -19,6 +19,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <sys/prctl.h>
+#include <sys/wait.h>
 #include <ctype.h>
 #include <libavcodec/avcodec.h>
 #include <libswscale/swscale.h>
@@ -46,6 +47,10 @@
 #include "userlist.h"
 #include "media.h"
 #include "compat.h"
+#include "config.h"
+#include "gui.h"
+#include "logging.h"
+#include "../stringutils.h"
 
 struct viddata
 {
@@ -65,27 +70,13 @@ struct viddata
 #endif
   GtkTextBuffer* buffer; // TODO: struct buffer array, for PMs
   GtkAdjustment* scroll;
+  GtkBuilder* gui;
 };
 
 int tc_client[2];
 int tc_client_in[2];
-
-char autoscroll_before(GtkAdjustment* scroll)
-{
-  // Figure out if we're at the bottom and should autoscroll with new content
-  int upper=gtk_adjustment_get_upper(scroll);
-  int size=gtk_adjustment_get_page_size(scroll);
-  int value=gtk_adjustment_get_value(scroll);
-  return (value+size==upper);
-}
-
-void autoscroll_after(GtkAdjustment* scroll)
-{
-  while(gtk_events_pending()){gtk_main_iteration();} // Make sure the textview's new size affects scroll's "upper" value first
-  int upper=gtk_adjustment_get_upper(scroll);
-  int size=gtk_adjustment_get_page_size(scroll);
-  gtk_adjustment_set_value(scroll, upper-size);
-}
+const char* channel=0;
+const char* mycolor=0;
 
 void updatescaling(struct viddata* data, unsigned int width, unsigned int height)
 {
@@ -134,10 +125,26 @@ void printchat(struct viddata* data, const char* text)
   if(bottom){autoscroll_after(data->scroll);}
 }
 
+void printchat_color(struct viddata* data, const char* text, const char* color, unsigned int offset)
+{
+  char bottom=autoscroll_before(data->scroll);
+  // Insert new content
+  GtkTextIter end;
+  gtk_text_buffer_get_end_iter(data->buffer, &end);
+  gtk_text_buffer_insert(data->buffer, &end, "\n", -1);
+  int startnum=gtk_text_iter_get_offset(&end);
+  gtk_text_buffer_insert(data->buffer, &end, text, -1);
+  // Set color if there was one
+  GtkTextIter start;
+  gtk_text_buffer_get_iter_at_offset(data->buffer, &start, startnum+offset);
+  gtk_text_buffer_apply_tag_by_name(data->buffer, color, &start, &end);
+  if(bottom){autoscroll_after(data->scroll);}
+}
+
 char buf[1024];
-gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
+gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer datap)
 {
-  int fd=g_io_channel_unix_get_fd(channel);
+  int fd=g_io_channel_unix_get_fd(iochannel);
   struct viddata* data=datap;
   unsigned int i;
   for(i=0; i<1023; ++i)
@@ -174,6 +181,28 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     }
     return 1;
   }
+  if(!strcmp(buf, "Password required"))
+  {
+    wait(0); // Reap the previous process
+    gtk_widget_hide(GTK_WIDGET(gtk_builder_get_object(data->gui, "main")));
+    gtk_widget_show_all(GTK_WIDGET(gtk_builder_get_object(data->gui, "channelpasswordwindow")));
+    return 1;
+  }
+  if(buf[0]=='/') // For the /help text
+  {
+    printchat(data, buf);
+    return 1;
+  }
+  // Remove escape codes and pick up the text color while we're at it
+  char* color=0;
+  char* esc;
+  char* escend;
+  while((esc=strchr(buf, '\x1b'))&&(escend=strchr(esc, 'm')))
+  {
+    escend[0]=0;
+    if(!color && strcmp(&esc[1], "[0")){color=strdup(&esc[1]);}
+    memmove(esc, &escend[1], strlen(&escend[1])+1);
+  }
   char* space=strchr(buf, ' ');
   // Timestamped events
   if(buf[0]=='['&&isdigit(buf[1])&&isdigit(buf[2])&&buf[3]==':'&&isdigit(buf[4])&&isdigit(buf[5])&&buf[6]==']'&&buf[7]==' ')
@@ -181,31 +210,46 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     char* nick=&buf[8];
     space=strchr(nick, ' ');
     if(!space){return 1;}
-//    if(space[-1]==':') // TODO: Handle messages specifically, e.g. handle /msg
-    // Remove escape codes and pick up the text color while we're at it
-    char* color=0;
-    char* esc;
-    char* escend;
-    while((esc=strchr(buf, '\x1b'))&&(escend=strchr(esc, 'm')))
+    if(space[-1]==':')
     {
-      escend[0]=0;
-      if(!color && strcmp(&esc[1], "[0")){color=strdup(&esc[1]);}
-      memmove(esc, &escend[1], strlen(&escend[1])+1);
+// TODO: handle /msg (PMs)
+      if(config_get_bool("soundradio_cmd") && !fork())
+      {
+        execlp("sh", "sh", "-c", config_get_str("soundcmd"), (char*)0);
+        _exit(0);
+      }
+      if(!strncmp(space, " /mbs youTube ", 14) && config_get_bool("youtuberadio_cmd") && !fork())
+      {
+// TODO: store the PID and make sure it's dead before starting a new video?
+// TODO: only play videos from mods?
+        char* id=&space[14];
+        char* offset=strchr(id, ' ');
+        if(!offset){_exit(1);}
+        offset[0]=0;
+        offset=&offset[1];
+        // Handle format string
+        const char* fmt=config_get_str("youtubecmd");
+        int len=strlen(fmt)+1;
+        len+=strcount(fmt, "%i")*(strlen(id)-2);
+        len+=strcount(fmt, "%t")*(strlen(id)-2);
+        char cmd[len];
+        cmd[0]=0;
+        while(fmt[0])
+        {
+          if(!strncmp(fmt, "%i", 2)){strcat(cmd, id); fmt=&fmt[2]; continue;}
+          if(!strncmp(fmt, "%t", 2)){strcat(cmd, offset); fmt=&fmt[2]; continue;}
+          for(len=0; fmt[len] && strncmp(&fmt[len], "%i", 2) && strncmp(&fmt[len], "%t", 2); ++len);
+          strncat(cmd, fmt, len);
+          fmt=&fmt[len];
+        }
+        execlp("sh", "sh", "-c", cmd, (char*)0);
+        _exit(0);
+      }
     }
-    char bottom=autoscroll_before(data->scroll);
+// TODO: handle logging PMs
+    if(config_get_bool("enable_logging")){logger_write(buf, channel, 0);}
     // Insert new content
-    GtkTextIter end;
-    gtk_text_buffer_get_end_iter(data->buffer, &end);
-    gtk_text_buffer_insert(data->buffer, &end, "\n", -1);
-    int startnum=gtk_text_iter_get_offset(&end);
-    gtk_text_buffer_insert(data->buffer, &end, buf, -1);
-    if(color) // Set color if there was one
-    {
-      GtkTextIter start;
-      gtk_text_buffer_get_iter_at_offset(data->buffer, &start, startnum+8); // 8 to skip timestamps
-      gtk_text_buffer_apply_tag_by_name(data->buffer, color, &start, &end);
-    }
-    if(bottom){autoscroll_after(data->scroll);}
+    printchat_color(data, buf, color, 8);
     if(space[-1]!=':') // Not a message
     {
       if(!strcmp(space, " entered the channel"))
@@ -232,8 +276,21 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
         }
       }
     }
+    free(color);
+    return 1;
+  }
+  if(!strcmp(buf, "Changed color") || !strncmp(buf, "Current color: ", 15))
+  {
+    printchat_color(data, buf, color, 0);
+    free((void*)mycolor);
+    mycolor=color;
     return 1;
   }
+  if(!strncmp(buf, "Color ", 6))
+  {
+    printchat_color(data, buf, color, 0);
+  }
+  free(color);
   if(space && !strcmp(space, " is a moderator."))
   {
     space[0]=0;
@@ -595,16 +652,105 @@ gboolean inputkeys(GtkWidget* widget, GdkEventKey* event, void* data)
 void sendmessage(GtkEntry* entry, struct viddata* data)
 {
   const char* msg=gtk_entry_get_text(entry);
+  dprintf(tc_client_in[1], "%s\n", msg);
+  // Don't print commands
+  if(!strcmp(msg, "/help") ||
+     !strncmp(msg, "/color ", 7) ||
+     !strcmp(msg, "/color") ||
+     !strcmp(msg, "/colors") ||
+     !strncmp(msg, "/nick ", 6) ||
+//     !strncmp(msg, "/msg ", 5) || // except PM commands
+     !strncmp(msg, "/opencam ", 9) ||
+     !strncmp(msg, "/close ", 7) ||
+     !strncmp(msg, "/ban ", 5) ||
+     !strcmp(msg, "/banlist") ||
+     !strncmp(msg, "/forgive ", 9) ||
+     !strcmp(msg, "/names") ||
+     !strcmp(msg, "/mute") ||
+     !strcmp(msg, "/push2talk") ||
+     !strcmp(msg, "/camup") ||
+     !strcmp(msg, "/camdown") ||
+     !strncmp(msg, "/video ", 7) ||
+     !strncmp(msg, "/topic ", 7))
+  {
+    gtk_entry_set_text(entry, "");
+    return;
+  }
   char text[strlen("[00:00] ")+strlen("You: ")+strlen(msg)+1];
   time_t timestamp=time(0);
   struct tm* t=localtime(&timestamp);
   sprintf(text, "[%02i:%02i] ", t->tm_hour, t->tm_min);
   sprintf(&text[8], "You: %s", msg);
-  printchat(data, text);
-  dprintf(tc_client_in[1], "%s\n", msg);
+  if(config_get_bool("enable_logging")){logger_write(text, channel, 0);}
+  printchat_color(data, text, mycolor, 8);
   gtk_entry_set_text(entry, "");
 }
 
+void startsession(GtkButton* button, struct viddata* data)
+{
+  gtk_widget_hide(GTK_WIDGET(gtk_builder_get_object(data->gui, "startwindow")));
+  gtk_widget_hide(GTK_WIDGET(gtk_builder_get_object(data->gui, "channelpasswordwindow")));
+  gtk_widget_show_all(GTK_WIDGET(gtk_builder_get_object(data->gui, "main")));
+  const char* nick=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "start_nick")));
+  channel=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "start_channel")));
+  const char* chanpass=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "channelpassword")));
+  const char* acc_user=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "acc_username")));
+  const char* acc_pass=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "acc_password")));
+  pipe(tc_client);
+  pipe(tc_client_in);
+  if(!fork())
+  {
+    prctl(PR_SET_PDEATHSIG, SIGHUP);
+    close(tc_client[0]);
+    close(tc_client_in[1]);
+    dup2(tc_client[1], 1);
+    dup2(tc_client_in[0], 0);
+    if(acc_user[0])
+    {
+      execl("./tc_client", "./tc_client", "-u", acc_user, channel, nick, chanpass, (char*)0);
+    }else{
+      execl("./tc_client", "./tc_client", channel, nick, chanpass, (char*)0);
+    }
+  }
+  if(acc_user[0]){dprintf(tc_client_in[1], "%s\n", acc_pass);}
+  write(tc_client_in[1], "/color\n", 7);
+  GIOChannel* tcchannel=g_io_channel_unix_new(tc_client[0]);
+  g_io_channel_set_encoding(tcchannel, 0, 0);
+  g_io_add_watch(tcchannel, G_IO_IN, handledata, data);
+  // Remember, if asked to
+  char save=0;
+  if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(data->gui, "start_rememberchan"))))
+  {
+    config_set("remember_nick", gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "start_nick"))));
+    config_set("remember_chan", gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "start_channel"))));
+    config_set("remember_chan_nick", "True");
+    save=1;
+  }
+  else if(config_get_bool("remember_chan_nick")) // Remove previously remembered info
+  {
+    config_set("remember_nick", "");
+    config_set("remember_chan", "");
+    config_set("remember_chan_nick", "False");
+    save=1;
+  }
+  // Same for account
+  if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(data->gui, "start_rememberacc"))))
+  {
+    config_set("remember_username", gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "acc_username"))));
+    config_set("remember_password", gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "acc_password"))));
+    config_set("remember_acc", "True");
+    save=1;
+  }
+  else if(config_get_bool("remember_acc")) // Remove previously remembered info
+  {
+    config_set("remember_username", "");
+    config_set("remember_password", "");
+    config_set("remember_acc", "False");
+    save=1;
+  }
+  if(save){config_save();}
+}
+
 int main(int argc, char** argv)
 {
   struct viddata data={0,0,0,0,0};
@@ -643,6 +789,7 @@ int main(int argc, char** argv)
   gtk_init(&argc, &argv);
   GtkBuilder* gui=gtk_builder_new_from_file("gtkgui.glade");
   gtk_builder_connect_signals(gui, 0);
+  data.gui=gui;
 
 #ifdef HAVE_V4L2
   GtkWidget* item=GTK_WIDGET(gtk_builder_get_object(gui, "menuitem_broadcast_camera"));
@@ -652,6 +799,9 @@ int main(int argc, char** argv)
   GtkWidget* item=GTK_WIDGET(gtk_builder_get_object(gui, "menuitem_broadcast"));
   gtk_widget_destroy(item);
 #endif
+
+  item=GTK_WIDGET(gtk_builder_get_object(gui, "menuitem_options_settings"));
+  g_signal_connect(item, "activate", G_CALLBACK(showsettings), gui);
   
   data.box=GTK_WIDGET(gtk_builder_get_object(gui, "cambox"));
   userlistwidget=GTK_WIDGET(gtk_builder_get_object(gui, "userlistbox"));
@@ -678,37 +828,54 @@ int main(int argc, char** argv)
   //colormap("[35;1", "#b9807f");
 
   GtkWidget* panes=GTK_WIDGET(gtk_builder_get_object(gui, "vpaned"));
-  g_signal_connect(panes, "notify::position", (GCallback)handleresizepane, &data);
+  g_signal_connect(panes, "notify::position", G_CALLBACK(handleresizepane), &data);
 
   GtkWidget* inputfield=GTK_WIDGET(gtk_builder_get_object(gui, "inputfield"));
-  g_signal_connect(inputfield, "activate", (GCallback)sendmessage, &data);
-  g_signal_connect(inputfield, "key-press-event", (GCallback)inputkeys, &data);
+  g_signal_connect(inputfield, "activate", G_CALLBACK(sendmessage), &data);
+  g_signal_connect(inputfield, "key-press-event", G_CALLBACK(inputkeys), &data);
+
+  config_load();
+  // Sound
+  GtkWidget* option=GTK_WIDGET(gtk_builder_get_object(gui, "soundradio_cmd"));
+  g_signal_connect(option, "toggled", G_CALLBACK(toggle_soundcmd), gui);
+  // Logging
+  option=GTK_WIDGET(gtk_builder_get_object(gui, "enable_logging"));
+  g_signal_connect(option, "toggled", G_CALLBACK(toggle_logging), gui);
+  option=GTK_WIDGET(gtk_builder_get_object(gui, "save_settings"));
+  g_signal_connect(option, "clicked", G_CALLBACK(savesettings), gui);
+  // Youtube
+  option=GTK_WIDGET(gtk_builder_get_object(gui, "youtuberadio_cmd"));
+  g_signal_connect(option, "toggled", G_CALLBACK(toggle_youtubecmd), gui);
 
   GtkWidget* window=GTK_WIDGET(gtk_builder_get_object(gui, "main"));
-  g_signal_connect(window, "configure-event", (GCallback)handleresize, &data);
-  gtk_widget_show_all(window);
-
-  pipe(tc_client);
-  pipe(tc_client_in);
-  if(!fork())
+  g_signal_connect(window, "configure-event", G_CALLBACK(handleresize), &data);
+
+  // Start window and channel password window signals
+  GtkWidget* button=GTK_WIDGET(gtk_builder_get_object(gui, "connectbutton"));
+  g_signal_connect(button, "clicked", G_CALLBACK(startsession), &data);
+  button=GTK_WIDGET(gtk_builder_get_object(gui, "channelpasswordbutton"));
+  g_signal_connect(button, "clicked", G_CALLBACK(startsession), &data);
+  button=GTK_WIDGET(gtk_builder_get_object(gui, "channelpassword"));
+  g_signal_connect(button, "activate", G_CALLBACK(startsession), &data);
+  GtkWidget* startwindow=GTK_WIDGET(gtk_builder_get_object(gui, "startwindow"));
+  // Set channel and nick from last session
+  if(config_get_bool("remember_chan_nick"))
   {
-    prctl(PR_SET_PDEATHSIG, SIGHUP);
-    close(tc_client[0]);
-    close(tc_client_in[1]);
-    dup2(tc_client[1], 1);
-    dup2(tc_client_in[0], 0);
-    argv[0]="./tc_client";
-    execv("./tc_client", argv);
+    gtk_entry_set_text(GTK_ENTRY(gtk_builder_get_object(gui, "start_nick")), config_get_str("remember_nick"));
+    gtk_entry_set_text(GTK_ENTRY(gtk_builder_get_object(gui, "start_channel")), config_get_str("remember_chan"));
+    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(gui, "start_rememberchan")), 1);
   }
-  close(tc_client_in[0]);
-  GIOChannel* tcchannel=g_io_channel_unix_new(tc_client[0]);
-  g_io_channel_set_encoding(tcchannel, 0, 0);
-  unsigned int channel_id=g_io_add_watch(tcchannel, G_IO_IN, handledata, &data);
+  // Set username and password from last session
+  if(config_get_bool("remember_acc"))
+  {
+    gtk_entry_set_text(GTK_ENTRY(gtk_builder_get_object(gui, "acc_username")), config_get_str("remember_username"));
+    gtk_entry_set_text(GTK_ENTRY(gtk_builder_get_object(gui, "acc_password")), config_get_str("remember_password"));
+    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(gui, "start_rememberacc")), 1);
+  }
+  gtk_widget_show_all(startwindow);
 
   gtk_main();
  
-  g_source_remove(channel_id);
-  g_io_channel_shutdown(tcchannel, 0, 0);
   camera_cleanup();
 #ifdef HAVE_SOUND
   #if HAVE_SOUND==avresample
diff --git a/utilities/gtk/compat.c b/utilities/gtk/compat.c
index 4b45d2a..3f2efd4 100644
--- a/utilities/gtk/compat.c
+++ b/utilities/gtk/compat.c
@@ -61,11 +61,12 @@
     close(f);
     buf[st.st_size]=0;
     char* pos;
-    if((pos=strstr(buf, "<requires ")))
+    if((pos=strstr(buf, "<requires "))) // Don't require anything
     {
       char* end=newline(pos);
       memmove(pos, end, strlen(end)+1);
     }
+    // Convert orientation properties into GtkV/GtkH object types
     char* orientation;
     while((orientation=strstr(buf, "<property name=\"orientation\">")))
     {
@@ -80,6 +81,7 @@
         pos[10]=dir;
       }
     }
+    // Convert remaining GtkBoxes and GtkPaneds with the default orientation
     while((pos=strstr(buf, "class=\"GtkBox\"")) || (pos=strstr(buf, "class=\"GtkPaned\"")))
     {
       memmove(&pos[11], &pos[10], strlen(&pos[10])+1);
diff --git a/utilities/gtk/config.c b/utilities/gtk/config.c
new file mode 100644
index 0000000..7d5a813
--- /dev/null
+++ b/utilities/gtk/config.c
@@ -0,0 +1,127 @@
+/*
+    tc_client-gtk, a graphical user interface for tc_client
+    Copyright (C) 2015  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, version 3 of the License.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include "config.h"
+
+struct configitem
+{
+  const char* name;
+  char* value;
+};
+
+struct configitem* configitems=0;
+unsigned int configitemcount=0;
+
+void config_load(void)
+{
+  const char* home=getenv("HOME");
+  char filename[strlen(home)+strlen("/.config/tc_client-gtk0")];
+  sprintf(filename, "%s/.config/tc_client-gtk", home);
+  FILE* f=fopen(filename, "r");
+  if(!f){return;}
+  char buf[2048];
+  while(fgets(buf, 2048, f))
+  {
+    char* sep=strchr(buf, ':');
+    if(!sep){continue;}
+    sep[0]=0;
+    char* value=&sep[1];
+    while(value[0]==' '){value=&value[1];}
+    while((sep=strchr(value, '\r'))||(sep=strchr(value, '\n'))){sep[0]=0;}
+    ++configitemcount;
+    configitems=realloc(configitems, sizeof(struct configitem)*configitemcount);
+    configitems[configitemcount-1].name=strdup(buf);
+    configitems[configitemcount-1].value=strdup(value);
+  }
+  fclose(f);
+}
+
+void config_save(void)
+{
+  const char* home=getenv("HOME");
+  char filename[strlen(home)+strlen("/.config/tc_client-gtk0")];
+  sprintf(filename, "%s/.config", home);
+  mkdir(filename, 0700);
+  strcat(filename, "/tc_client-gtk");
+  FILE* f=fopen(filename, "w");
+  if(!f){perror("fopen(~/.config/tc_client-gtk)"); return;}
+  unsigned int i;
+  for(i=0; i<configitemcount; ++i)
+  {
+    fprintf(f, "%s: %s\n", configitems[i].name, configitems[i].value);
+  }
+  fclose(f);
+}
+
+char config_get_bool(const char* name)
+{
+  unsigned int i;
+  for(i=0; i<configitemcount; ++i)
+  {
+    if(!strcmp(configitems[i].name, name)){return !strcasecmp(configitems[i].value, "True");}
+  }
+  return 0;
+}
+
+const char* config_get_str(const char* name)
+{
+  unsigned int i;
+  for(i=0; i<configitemcount; ++i)
+  {
+    if(!strcmp(configitems[i].name, name)){return configitems[i].value;}
+  }
+  return "";
+}
+
+int config_get_int(const char* name)
+{
+  unsigned int i;
+  for(i=0; i<configitemcount; ++i)
+  {
+    if(!strcmp(configitems[i].name, name)){return atoi(configitems[i].value);}
+  }
+  return 0;
+}
+
+void config_set(const char* name, const char* value)
+{
+  unsigned int i;
+  for(i=0; i<configitemcount; ++i)
+  {
+    if(!strcmp(configitems[i].name, name))
+    {
+      free(configitems[i].value);
+      configitems[i].value=strdup(value);
+      return;
+    }
+  }
+  ++configitemcount;
+  configitems=realloc(configitems, sizeof(struct configitem)*configitemcount);
+  configitems[configitemcount-1].name=strdup(name);
+  configitems[configitemcount-1].value=strdup(value);
+}
+
+void config_set_int(const char* name, int value)
+{
+  int size=snprintf(0,0, "%i", value);
+  char buf[size+1];
+  sprintf(buf, "%i", value);
+  config_set(name, buf);
+}
diff --git a/utilities/gtk/config.h b/utilities/gtk/config.h
new file mode 100644
index 0000000..a12032c
--- /dev/null
+++ b/utilities/gtk/config.h
@@ -0,0 +1,25 @@
+/*
+    tc_client-gtk, a graphical user interface for tc_client
+    Copyright (C) 2015  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, version 3 of the License.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+extern void config_load(void);
+extern void config_save(void);
+
+extern char config_get_bool(const char* name);
+extern const char* config_get_str(const char* name);
+extern int config_get_int(const char* name);
+
+extern void config_set(const char* name, const char* value);
+extern void config_set_int(const char* name, int value);
diff --git a/utilities/gtk/gui.c b/utilities/gtk/gui.c
new file mode 100644
index 0000000..c59a207
--- /dev/null
+++ b/utilities/gtk/gui.c
@@ -0,0 +1,111 @@
+/*
+    tc_client-gtk, a graphical user interface for tc_client
+    Copyright (C) 2015  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, version 3 of the License.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <gtk/gtk.h>
+#include "gui.h"
+#include "config.h"
+#include "logging.h"
+
+char autoscroll_before(GtkAdjustment* scroll)
+{
+  // Figure out if we're at the bottom and should autoscroll with new content
+  int upper=gtk_adjustment_get_upper(scroll);
+  int size=gtk_adjustment_get_page_size(scroll);
+  int value=gtk_adjustment_get_value(scroll);
+  return (value+size==upper);
+}
+
+void autoscroll_after(GtkAdjustment* scroll)
+{
+  while(gtk_events_pending()){gtk_main_iteration();} // Make sure the textview's new size affects scroll's "upper" value first
+  int upper=gtk_adjustment_get_upper(scroll);
+  int size=gtk_adjustment_get_page_size(scroll);
+  gtk_adjustment_set_value(scroll, upper-size);
+}
+
+void settings_reset(GtkBuilder* gui)
+{
+  // Sound
+  GtkWidget* option=GTK_WIDGET(gtk_builder_get_object(gui, "soundradio_cmd"));
+  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(option), config_get_bool("soundradio_cmd"));
+  option=GTK_WIDGET(gtk_builder_get_object(gui, "soundcmd"));
+  gtk_entry_set_text(GTK_ENTRY(option), config_get_str("soundcmd"));
+  // Logging
+  option=GTK_WIDGET(gtk_builder_get_object(gui, "enable_logging"));
+  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(option), config_get_bool("enable_logging"));
+  GtkWidget* logpath=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_channel"));
+  gtk_entry_set_text(GTK_ENTRY(logpath), config_get_str("logpath_channel"));
+  logpath=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_pm"));
+  gtk_entry_set_text(GTK_ENTRY(logpath), config_get_str("logpath_pm"));
+  // Youtube
+  option=GTK_WIDGET(gtk_builder_get_object(gui, "youtuberadio_cmd"));
+  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(option), config_get_bool("youtuberadio_cmd"));
+  option=GTK_WIDGET(gtk_builder_get_object(gui, "youtubecmd"));
+  gtk_entry_set_text(GTK_ENTRY(option), config_get_str("youtubecmd"));
+}
+
+void showsettings(GtkMenuItem* item, GtkBuilder* gui)
+{
+  settings_reset(gui);
+  GtkWidget* w=GTK_WIDGET(gtk_builder_get_object(gui, "settings"));
+  gtk_widget_show_all(w);
+}
+
+void savesettings(GtkButton* button, GtkBuilder* gui)
+{
+  // Sound
+  GtkWidget* soundcmd=GTK_WIDGET(gtk_builder_get_object(gui, "soundcmd"));
+  config_set("soundcmd", gtk_entry_get_text(GTK_ENTRY(soundcmd)));
+  GtkWidget* soundradio_cmd=GTK_WIDGET(gtk_builder_get_object(gui, "soundradio_cmd"));
+  config_set("soundradio_cmd", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(soundradio_cmd))?"True":"False");
+  // Logging
+  GtkWidget* logging=GTK_WIDGET(gtk_builder_get_object(gui, "enable_logging"));
+  config_set("enable_logging", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(logging))?"True":"False");
+  if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(logging))){logger_close_all();}
+  GtkWidget* logpath=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_channel"));
+  config_set("logpath_channel", gtk_entry_get_text(GTK_ENTRY(logpath)));
+  logpath=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_pm"));
+  config_set("logpath_pm", gtk_entry_get_text(GTK_ENTRY(logpath)));
+  // Youtube
+  GtkWidget* youtubecmd=GTK_WIDGET(gtk_builder_get_object(gui, "youtubecmd"));
+  config_set("youtubecmd", gtk_entry_get_text(GTK_ENTRY(youtubecmd)));
+  GtkWidget* youtuberadio_cmd=GTK_WIDGET(gtk_builder_get_object(gui, "youtuberadio_cmd"));
+  config_set("youtuberadio_cmd", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(youtuberadio_cmd))?"True":"False");
+
+  config_save();
+  GtkWidget* settings=GTK_WIDGET(gtk_builder_get_object(gui, "settings"));
+  gtk_widget_hide(settings);
+}
+
+void toggle_soundcmd(GtkToggleButton* button, GtkBuilder* gui)
+{
+  GtkWidget* field=GTK_WIDGET(gtk_builder_get_object(gui, "soundcmd"));
+  gtk_widget_set_sensitive(field, gtk_toggle_button_get_active(button));
+}
+
+void toggle_logging(GtkToggleButton* button, GtkBuilder* gui)
+{
+  GtkWidget* field1=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_channel"));
+  GtkWidget* field2=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_pm"));
+  gtk_widget_set_sensitive(field1, gtk_toggle_button_get_active(button));
+  gtk_widget_set_sensitive(field2, gtk_toggle_button_get_active(button));
+}
+
+void toggle_youtubecmd(GtkToggleButton* button, GtkBuilder* gui)
+{
+  GtkWidget* field=GTK_WIDGET(gtk_builder_get_object(gui, "youtubecmd"));
+  gtk_widget_set_sensitive(field, gtk_toggle_button_get_active(button));
+}
diff --git a/utilities/gtk/gui.h b/utilities/gtk/gui.h
new file mode 100644
index 0000000..e4cfa68
--- /dev/null
+++ b/utilities/gtk/gui.h
@@ -0,0 +1,26 @@
+/*
+    tc_client-gtk, a graphical user interface for tc_client
+    Copyright (C) 2015  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, version 3 of the License.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <gtk/gtk.h>
+
+extern char autoscroll_before(GtkAdjustment* scroll);
+extern void autoscroll_after(GtkAdjustment* scroll);
+extern void settings_reset(GtkBuilder* gui);
+extern void showsettings(GtkMenuItem* item, GtkBuilder* gui);
+extern void savesettings(GtkButton* button, GtkBuilder* gui);
+extern void toggle_soundcmd(GtkToggleButton* button, GtkBuilder* gui);
+extern void toggle_logging(GtkToggleButton* button, GtkBuilder* gui);
+extern void toggle_youtubecmd(GtkToggleButton* button, GtkBuilder* gui);
diff --git a/utilities/gtk/logging.c b/utilities/gtk/logging.c
new file mode 100644
index 0000000..6b1a107
--- /dev/null
+++ b/utilities/gtk/logging.c
@@ -0,0 +1,97 @@
+/*
+    tc_client-gtk, a graphical user interface for tc_client
+    Copyright (C) 2015  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, version 3 of the License.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+#include <sys/stat.h>
+#include "config.h"
+#include "../stringutils.h"
+
+struct logfile
+{
+  FILE* f;
+  const char* path;
+};
+struct logfile* logfiles=0;
+unsigned int logfilecount=0;
+
+void logger_write(const char* line, const char* channel, const char* nick)
+{
+  const char* home=getenv("HOME");
+  const char* path=config_get_str(nick?"logpath_pm":"logpath_channel");
+  int namelen=strlen(path)+1;
+  namelen+=strcount(path, "%h")*(strlen(home)-2);
+  namelen+=strcount(path, "%c")*(strlen(channel)-2);
+  if(nick){namelen+=strcount(path, "%n")*(strlen(nick)-2);}
+  char filename[namelen];
+  filename[0]=0;
+  int len;
+  while(path[0])
+  {
+    if(!strncmp(path, "%h", 2)){strcat(filename, home); path=&path[2]; continue;}
+    if(!strncmp(path, "%c", 2)){strcat(filename, channel); path=&path[2]; continue;}
+    if(nick && !strncmp(path, "%n", 2)){strcat(filename, nick); path=&path[2]; continue;}
+    for(len=0; path[len] && strncmp(&path[len], "%h", 2) && strncmp(&path[len], "%c", 2) && (!nick||strncmp(&path[len], "%n", 2)); ++len);
+    strncat(filename, path, len);
+    path=&path[len];
+  }
+  unsigned int i;
+  for(i=0; i<logfilecount; ++i)
+  {
+    if(!strcmp(filename, logfiles[i].path)){break;}
+  }
+  if(i==logfilecount)
+  {
+    ++logfilecount;
+    logfiles=realloc(logfiles, logfilecount*sizeof(struct logfile));
+    logfiles[i].path=strdup(filename);
+    // Make sure the whole path exists
+    char* sep=filename;
+    while((sep=strchr(sep, '/')))
+    {
+      sep[0]=0;
+printf("Creating '%s' if it doesn't exist yet\n", filename);
+      mkdir(filename, 0700);
+      sep[0]='/';
+      sep=&sep[1];
+    }
+    logfiles[i].f=fopen(logfiles[i].path, "a");
+    if(!logfiles[i].f)
+    {
+      perror("fopen(logfile)");
+      free((void*)logfiles[i].path);
+      --logfilecount;
+      return;
+    }
+    fprintf(logfiles[i].f, "Opening logfile on %ti (TODO: format)\n", time(0));
+  }
+  fprintf(logfiles[i].f, "%s\n", line);
+}
+
+void logger_close_all(void)
+{
+  unsigned int i;
+  for(i=0; i<logfilecount; ++i)
+  {
+    fclose(logfiles[i].f);
+    free((void*)logfiles[i].path);
+  }
+  free(logfiles);
+  logfiles=0;
+  logfilecount=0;
+}
diff --git a/utilities/gtk/logging.h b/utilities/gtk/logging.h
new file mode 100644
index 0000000..76c1831
--- /dev/null
+++ b/utilities/gtk/logging.h
@@ -0,0 +1,19 @@
+/*
+    tc_client-gtk, a graphical user interface for tc_client
+    Copyright (C) 2015  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, version 3 of the License.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+extern void logger_write(const char* line, const char* channel, const char* nick);
+extern void logger_close_all(void);
diff --git a/utilities/list.c b/utilities/list.c
index bae53df..707a182 100644
--- a/utilities/list.c
+++ b/utilities/list.c
@@ -1,5 +1,5 @@
 /*
-    modbot, a bot for tc_client that queues and plays videos
+    A simple list implementation
     Copyright (C) 2015  alicia@ion.nu
 
     This program is free software: you can redistribute it and/or modify
diff --git a/utilities/list.h b/utilities/list.h
index 1462584..5e8e7d5 100644
--- a/utilities/list.h
+++ b/utilities/list.h
@@ -1,5 +1,5 @@
 /*
-    modbot, a bot for tc_client that queues and plays videos
+    A simple list implementation
     Copyright (C) 2015  alicia@ion.nu
 
     This program is free software: you can redistribute it and/or modify
diff --git a/utilities/stringutils.c b/utilities/stringutils.c
new file mode 100644
index 0000000..e7d33b4
--- /dev/null
+++ b/utilities/stringutils.c
@@ -0,0 +1,29 @@
+/*
+    A few simple string utilities
+    Copyright (C) 2015  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, version 3 of the License.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <string.h>
+
+int strcount(const char* haystack, const char* needle)
+{
+  int c=0;
+  haystack=strstr(haystack, needle);
+  while(haystack)
+  {
+    ++c;
+    haystack=strstr(&haystack[1], needle);
+  }
+  return c;
+}
diff --git a/utilities/stringutils.h b/utilities/stringutils.h
new file mode 100644
index 0000000..c942bfe
--- /dev/null
+++ b/utilities/stringutils.h
@@ -0,0 +1,17 @@
+/*
+    A few simple string utilities
+    Copyright (C) 2015  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, version 3 of the License.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+extern int strcount(const char* haystack, const char* needle);