diff --git a/common/utils/T/tracer/Makefile.remote b/common/utils/T/tracer/Makefile.remote
index 79b1d6ea1eaa40d9cd51fb800159cd216c1685c3..9dc082e01e1d1f3b02ab0e81d5030f6124cb7785 100644
--- a/common/utils/T/tracer/Makefile.remote
+++ b/common/utils/T/tracer/Makefile.remote
@@ -1,5 +1,5 @@
 CC=gcc
-CFLAGS=-Wall -g -pthread -DT_TRACER
+CFLAGS=-Wall -g -pthread -DT_TRACER -I.
 
 #CFLAGS += -O3 -ffast-math -fomit-frame-pointer
 
@@ -11,10 +11,14 @@ OBJS=remote_old.o plot.o database.o gui.o
 $(PROG): gui/gui.a $(OBJS)
 	$(CC) $(CFLAGS) -o $(PROG) $(OBJS) gui/gui.a $(LIBS)
 
-textlog: remote.o database.o event.o handler.o textlog.o
-	$(CC) $(CFLAGS) -o textlog $^
+textlog: utils.o remote.o database.o event.o handler.o textlog.o \
+         view/view.a gui/gui.a
+	$(CC) $(CFLAGS) -o textlog $^ $(LIBS)
 
-.PHONY: gui/gui.a
+.PHONY: gui/gui.a view/view.a
+
+view/view.a:
+	cd view && make
 
 gui/gui.a:
 	cd gui && make
@@ -25,3 +29,4 @@ gui/gui.a:
 clean:
 	rm -f *.o $(PROG) core textlog
 	cd gui && make clean
+	cd view && make clean
diff --git a/common/utils/T/tracer/remote.c b/common/utils/T/tracer/remote.c
index c22cc1bf55231bb5b45d8577d0c8a33e11273d5b..020aba3cdb80bf0ca81bb7baf99c2956f4c2ae36 100644
--- a/common/utils/T/tracer/remote.c
+++ b/common/utils/T/tracer/remote.c
@@ -8,6 +8,9 @@
 #include "event.h"
 #include "handler.h"
 #include "textlog.h"
+#include "view/view.h"
+#include "gui/gui.h"
+#include "utils.h"
 #include "../T_defs.h"
 
 #define DEFAULT_REMOTE_PORT 2021
@@ -55,6 +58,7 @@ void usage(void)
 "                                    they will be processed in order\n"
 "                                    by default, all is off\n"
 "    -p <port>                 use given port (default %d)\n"
+"    -x                        GUI output\n",
   DEFAULT_REMOTE_PORT
   );
   exit(1);
@@ -88,6 +92,13 @@ event get_event(int s, char *v, void *d)
   return new_event(type, length, v, d);
 }
 
+static void *gui_thread(void *_g)
+{
+  gui *g = _g;
+  gui_loop(g);
+  return NULL;
+}
+
 int main(int n, char **v)
 {
   char *database_filename = NULL;
@@ -104,6 +115,7 @@ int main(int n, char **v)
   int l;
   event_handler *h;
   textlog *textlog;
+  int gui_mode = 0;
 
   on_off_name = malloc(n * sizeof(char *)); if (on_off_name == NULL) abort();
   on_off_action = malloc(n * sizeof(int)); if (on_off_action == NULL) abort();
@@ -122,6 +134,7 @@ int main(int n, char **v)
       { on_off_name[on_off_n]=NULL; on_off_action[on_off_n++]=1; continue; }
     if (!strcmp(v[i], "-OFF"))
       { on_off_name[on_off_n]=NULL; on_off_action[on_off_n++]=0; continue; }
+    if (!strcmp(v[i], "-x")) { gui_mode = 1; continue; }
     usage();
   }
 
@@ -142,6 +155,23 @@ int main(int n, char **v)
       "ENB_UL_CHANNEL_ESTIMATE",
       "ev: {} eNB_id [eNB_ID] frame [frame] subframe [subframe]");
 
+  if (gui_mode) {
+    view *tout;
+    gui *g;
+    widget *w, *win;
+    g = gui_init();
+    w = new_text_list(g, 600, 20, 0);
+    win = new_toplevel_window(g, 600, 20*12, "textlog");
+    widget_add_child(g, win, w, -1);
+    //tout = new_textlist(1000, 10, g, w);
+    tout = new_textlist(7, 4, g, w);
+    new_thread(gui_thread, g);
+    textlog_add_view(textlog, tout);
+  } else {
+    view *sout = new_stdout();
+    textlog_add_view(textlog, sout);
+  }
+
   for (i = 0; i < on_off_n; i++)
     on_off(database, on_off_name[i], is_on, on_off_action[i]);
 
diff --git a/common/utils/T/tracer/textlog.c b/common/utils/T/tracer/textlog.c
index e6728f9674badcc66270064db3878b2dbd039f80..4bc66953d52647ec73ee6dd88e2037bd6d69efda 100644
--- a/common/utils/T/tracer/textlog.c
+++ b/common/utils/T/tracer/textlog.c
@@ -1,8 +1,10 @@
 #include "textlog.h"
 #include "handler.h"
 #include "database.h"
+#include "view/view.h"
 #include <stdlib.h>
 #include <string.h>
+#include <stdio.h>
 
 enum format_item_type {
   INSTRING,
@@ -23,25 +25,62 @@ struct textlog {
   char *format;
   void *database;
   unsigned long handler_id;
+  /* parsed format string */
   struct format_item *f;
   int fsize;
+  /* list of views */
+  view **v;
+  int vsize;
+  /* local output buffer */
+  int osize;
+  int omaxsize;
+  char *obuf;
 };
 
-#include <stdio.h>
+static void PUTC(struct textlog *l, char c)
+{
+  if (l->osize == l->omaxsize) {
+    l->omaxsize += 512;
+    l->obuf = realloc(l->obuf, l->omaxsize);
+    if (l->obuf == NULL) abort();
+  }
+  l->obuf[l->osize] = c;
+  l->osize++;
+}
+
+static void PUTS(struct textlog *l, char *s)
+{
+  while (*s) PUTC(l, *s++);
+}
+
+static void PUTI(struct textlog *l, int i)
+{
+  char s[64];
+  sprintf(s, "%d", i);
+  PUTS(l, s);
+}
+
 static void _event(void *p, event e)
 {
   struct textlog *l = p;
   int i;
-//printf("%s %s\n", l->event_name, l->format);
+
+  l->osize = 0;
 
   for (i = 0; i < l->fsize; i++)
   switch(l->f[i].type) {
-  case INSTRING: printf("%s", l->f[i].s); break;
-  case INT:      printf("%d", e.e[l->f[i].event_arg].i); break;
-  case STRING:   printf("%s", e.e[l->f[i].event_arg].s); break;
-  case BUFFER:   printf("{buffer size:%d}",e.e[l->f[i].event_arg].bsize);break;
+  case INSTRING: PUTS(l, l->f[i].s); break;
+  case INT:      PUTI(l, e.e[l->f[i].event_arg].i); break;
+  case STRING:   PUTS(l, e.e[l->f[i].event_arg].s); break;
+  case BUFFER:
+    PUTS(l, "{buffer size:");
+    PUTI(l, e.e[l->f[i].event_arg].bsize);
+    PUTS(l, "}");
+    break;
   }
-  printf("\n");
+  PUTC(l, 0);
+
+  for (i = 0; i < l->vsize; i++) l->v[i]->append(l->v[i], l->obuf);
 }
 
 enum chunk_type { C_ERROR, C_STRING, C_ARG_NAME, C_EVENT_NAME };
@@ -166,3 +205,11 @@ error:
   printf("%s:%d: bad format '%s'\n", __FILE__, __LINE__, format);
   abort();
 }
+
+void textlog_add_view(textlog *_l, view *v)
+{
+  struct textlog *l = _l;
+  l->vsize++;
+  l->v = realloc(l->v, l->vsize * sizeof(view *)); if (l->v == NULL) abort();
+  l->v[l->vsize-1] = v;
+}
diff --git a/common/utils/T/tracer/textlog.h b/common/utils/T/tracer/textlog.h
index 5c23f3a2db8af2a6854d4ac02d9dfbc7178a09c2..b9e9a3e8e3d9bd13fb3879c35f3537790b53f2b4 100644
--- a/common/utils/T/tracer/textlog.h
+++ b/common/utils/T/tracer/textlog.h
@@ -6,4 +6,8 @@ typedef void textlog;
 textlog *new_textlog(void *event_handler, void *database,
     char *event_name, char *format);
 
+#include "view/view.h"
+
+void textlog_add_view(textlog *l, view *v);
+
 #endif /* _TEXTLOG_H_ */
diff --git a/common/utils/T/tracer/utils.c b/common/utils/T/tracer/utils.c
new file mode 100644
index 0000000000000000000000000000000000000000..2e18ccb60e3f35cf0f5dd38544aaccebe53dfd02
--- /dev/null
+++ b/common/utils/T/tracer/utils.c
@@ -0,0 +1,61 @@
+#include "utils.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <time.h>
+
+void new_thread(void *(*f)(void *), void *data)
+{
+  pthread_t t;
+  pthread_attr_t att;
+
+  if (pthread_attr_init(&att))
+    { fprintf(stderr, "pthread_attr_init err\n"); exit(1); }
+  if (pthread_attr_setdetachstate(&att, PTHREAD_CREATE_DETACHED))
+    { fprintf(stderr, "pthread_attr_setdetachstate err\n"); exit(1); }
+  if (pthread_attr_setstacksize(&att, 10000000))
+    { fprintf(stderr, "pthread_attr_setstacksize err\n"); exit(1); }
+  if (pthread_create(&t, &att, f, data))
+    { fprintf(stderr, "pthread_create err\n"); exit(1); }
+  if (pthread_attr_destroy(&att))
+    { fprintf(stderr, "pthread_attr_destroy err\n"); exit(1); }
+}
+
+void sleepms(int ms)
+{
+  struct timespec t;
+
+  t.tv_sec = ms / 1000;
+  t.tv_nsec = (ms % 1000) * 1000000L;
+
+  /* TODO: deal with EINTR */
+  if (nanosleep(&t, NULL)) abort();
+}
+
+/****************************************************************************/
+/* list                                                                     */
+/****************************************************************************/
+
+list *list_remove_head(list *l)
+{
+  list *ret;
+  if (l == NULL) return NULL;
+  ret = l->next;
+  if (ret != NULL) ret->last = l->last;
+  free(l);
+  return ret;
+}
+
+list *list_append(list *l, void *data)
+{
+  list *new = calloc(1, sizeof(list));
+  if (new == NULL) abort();
+  new->data = data;
+  if (l == NULL) {
+    new->last = new;
+    return new;
+  }
+  l->last->next = new;
+  l->last = new;
+  return l;
+}
diff --git a/common/utils/T/tracer/utils.h b/common/utils/T/tracer/utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..5a37aa34c3485b40f260e39685091ca4fe094a38
--- /dev/null
+++ b/common/utils/T/tracer/utils.h
@@ -0,0 +1,19 @@
+#ifndef _UTILS_H_
+#define _UTILS_H_
+
+void new_thread(void *(*f)(void *), void *data);
+void sleepms(int ms);
+
+/****************************************************************************/
+/* list                                                                     */
+/****************************************************************************/
+
+typedef struct list {
+  struct list *last, *next;
+  void *data;
+} list;
+
+list *list_remove_head(list *l);
+list *list_append(list *l, void *data);
+
+#endif /* _UTILS_H_ */
diff --git a/common/utils/T/tracer/view/Makefile b/common/utils/T/tracer/view/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..bcf49d15cb55b6c511914298ed38351fed0bc18b
--- /dev/null
+++ b/common/utils/T/tracer/view/Makefile
@@ -0,0 +1,13 @@
+CC=gcc
+CFLAGS=-Wall -g -pthread -I..
+
+OBJS=stdout.o textlist.o
+
+view.a: $(OBJS)
+	ar cr view.a $(OBJS)
+
+%.o: %.c
+	$(CC) $(CFLAGS) -o $@ -c $<
+
+clean:
+	rm -f *.a *.o
diff --git a/common/utils/T/tracer/view/stdout.c b/common/utils/T/tracer/view/stdout.c
new file mode 100644
index 0000000000000000000000000000000000000000..695704e4221d4a4667f9dae9e036c49c01824d01
--- /dev/null
+++ b/common/utils/T/tracer/view/stdout.c
@@ -0,0 +1,35 @@
+#include "view.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <pthread.h>
+
+struct stdout {
+  view common;
+  pthread_mutex_t lock;
+};
+
+static void clear(view *this)
+{
+  /* do nothing */
+}
+
+static void append(view *_this, char *s)
+{
+  struct stdout *this = (struct stdout *)_this;
+  if (pthread_mutex_lock(&this->lock)) abort();
+  printf("%s\n", s);
+  if (pthread_mutex_unlock(&this->lock)) abort();
+}
+
+view *new_stdout(void)
+{
+  struct stdout *ret = calloc(1, sizeof(struct stdout));
+  if (ret == NULL) abort();
+
+  ret->common.clear = clear;
+  ret->common.append = (void (*)(view *, ...))append;
+
+  if (pthread_mutex_init(&ret->lock, NULL)) abort();
+
+  return (view *)ret;
+}
diff --git a/common/utils/T/tracer/view/textlist.c b/common/utils/T/tracer/view/textlist.c
new file mode 100644
index 0000000000000000000000000000000000000000..7d1948e044b082b45777e9c911b266e39b735e09
--- /dev/null
+++ b/common/utils/T/tracer/view/textlist.c
@@ -0,0 +1,85 @@
+#include "view.h"
+#include "../utils.h"
+#include "gui/gui.h"
+#include <stdlib.h>
+#include <pthread.h>
+#include <string.h>
+
+struct textlist {
+  view common;
+  gui *g;
+  widget *w;
+  int maxsize;
+  int cursize;
+  float refresh_rate;
+  int autoscroll;
+  pthread_mutex_t lock;
+  list * volatile to_append;
+};
+
+static void _append(struct textlist *this, char *s)
+{
+  if (this->cursize == this->maxsize) {
+    text_list_del(this->g, this->w, 0);
+    this->cursize--;
+  }
+  text_list_add(this->g, this->w, s, -1);
+  this->cursize++;
+}
+
+static void *textlist_thread(void *_this)
+{
+  struct textlist *this = _this;
+
+  while (1) {
+    if (pthread_mutex_lock(&this->lock)) abort();
+    while (this->to_append != NULL) {
+      char *s = this->to_append->data;
+      this->to_append = list_remove_head(this->to_append);
+      _append(this, s);
+      free(s);
+    }
+    if (pthread_mutex_unlock(&this->lock)) abort();
+    sleepms(1000/this->refresh_rate);
+  }
+
+  return 0;
+}
+
+static void clear(view *this)
+{
+  /* TODO */
+}
+
+static void append(view *_this, char *s)
+{
+  struct textlist *this = (struct textlist *)_this;
+  char *dup;
+
+  if (pthread_mutex_lock(&this->lock)) abort();
+  dup = strdup(s); if (dup == NULL) abort();
+  this->to_append = list_append(this->to_append, dup);
+  if (pthread_mutex_unlock(&this->lock)) abort();
+}
+
+view *new_textlist(int maxsize, float refresh_rate, gui *g, widget *w)
+{
+  struct textlist *ret = calloc(1, sizeof(struct textlist));
+  if (ret == NULL) abort();
+
+  ret->common.clear = clear;
+  ret->common.append = (void (*)(view *, ...))append;
+
+  ret->cursize = 0;
+  ret->maxsize = maxsize;
+  ret->refresh_rate = refresh_rate;
+  ret->g = g;
+  ret->w = w;
+  ret->autoscroll = 1;
+
+  if (pthread_mutex_init(&ret->lock, NULL)) abort();
+
+  new_thread(textlist_thread, ret);
+
+  return (view *)ret;
+}
diff --git a/common/utils/T/tracer/view/view.h b/common/utils/T/tracer/view/view.h
new file mode 100644
index 0000000000000000000000000000000000000000..9929e6e5d4324d562553bb791ebf209d7e94e7db
--- /dev/null
+++ b/common/utils/T/tracer/view/view.h
@@ -0,0 +1,16 @@
+#ifndef _VIEW_H_
+#define _VIEW_H_
+
+#include "gui/gui.h"
+
+/* defines the public API of views */
+
+typedef struct view {
+  void (*clear)(struct view *this);
+  void (*append)(struct view *this, ...);
+} view;
+
+view *new_stdout(void);
+view *new_textlist(int maxsize, float refresh_rate, gui *g, widget *w);
+
+#endif /* _VIEW_H_ */