diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py
index 7330f21a..276dbd6d 100644
--- a/coretk/coretk/app.py
+++ b/coretk/coretk/app.py
@@ -1,7 +1,7 @@
 import logging
 import tkinter as tk
 
-from coretk import appdirs
+from coretk import appconfig
 from coretk.coreclient import CoreClient
 from coretk.graph import CanvasGraph
 from coretk.images import ImageEnum, Images
@@ -26,7 +26,7 @@ class Application(tk.Frame):
         self.radiovar = tk.IntVar(value=1)
         self.show_grid_var = tk.IntVar(value=1)
         self.adjust_to_dim_var = tk.IntVar(value=0)
-        self.config = appdirs.read_config()
+        self.config = appconfig.read()
         self.core = CoreClient(self)
         self.setup_app()
         self.draw()
@@ -70,10 +70,13 @@ class Application(tk.Frame):
         menu_action = MenuAction(self, self.master)
         menu_action.on_quit()
 
+    def save_config(self):
+        appconfig.save(self.config)
+
 
 if __name__ == "__main__":
     log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s"
     logging.basicConfig(level=logging.DEBUG, format=log_format)
-    appdirs.check_directory()
+    appconfig.check_directory()
     app = Application()
     app.mainloop()
diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appconfig.py
similarity index 70%
rename from coretk/coretk/appdirs.py
rename to coretk/coretk/appconfig.py
index 553b0949..67f97181 100644
--- a/coretk/coretk/appdirs.py
+++ b/coretk/coretk/appconfig.py
@@ -1,4 +1,5 @@
 import logging
+import os
 import shutil
 from pathlib import Path
 
@@ -18,6 +19,20 @@ CONFIG_PATH = HOME_PATH.joinpath("gui.yaml")
 LOCAL_ICONS_PATH = Path(__file__).parent.joinpath("icons").absolute()
 LOCAL_BACKGROUND_PATH = Path(__file__).parent.joinpath("backgrounds").absolute()
 
+# configuration data
+TERMINALS = [
+    "$TERM",
+    "gnome-terminal --window --",
+    "lxterminal -e",
+    "konsole -e",
+    "xterm -e",
+    "aterm -e",
+    "eterm -e",
+    "rxvt -e",
+    "xfce4-terminal -x",
+]
+EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"]
+
 
 class IndentDumper(yaml.Dumper):
     def increase_indent(self, flow=False, indentless=False):
@@ -42,18 +57,33 @@ def check_directory():
     for background in LOCAL_BACKGROUND_PATH.glob("*"):
         new_background = BACKGROUNDS_PATH.joinpath(background.name)
         shutil.copy(background, new_background)
+
+    if "TERM" in os.environ:
+        terminal = TERMINALS[0]
+    else:
+        terminal = TERMINALS[1]
+    if "EDITOR" in os.environ:
+        editor = EDITORS[0]
+    else:
+        editor = EDITORS[1]
     config = {
+        "preferences": {
+            "editor": editor,
+            "terminal": terminal,
+            "gui3d": "/usr/local/bin/std3d.sh",
+        },
         "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}],
         "nodes": [],
+        "observers": [{"name": "hello", "cmd": "echo hello"}],
     }
-    save_config(config)
+    save(config)
 
 
-def read_config():
+def read():
     with CONFIG_PATH.open("r") as f:
         return yaml.load(f, Loader=yaml.SafeLoader)
 
 
-def save_config(config):
+def save(config):
     with CONFIG_PATH.open("w") as f:
         yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False)
diff --git a/coretk/coretk/canvastooltip.py b/coretk/coretk/canvastooltip.py
index 42270809..8ace41d0 100644
--- a/coretk/coretk/canvastooltip.py
+++ b/coretk/coretk/canvastooltip.py
@@ -18,25 +18,14 @@ class CanvasTooltip:
     """
 
     def __init__(
-        self,
-        canvas,
-        tag_or_id,
-        *,
-        bg="#FFFFEA",
-        pad=(5, 3, 5, 3),
-        text="canvas info",
-        waittime=400,
-        wraplength=250
+        self, canvas, *, bg="#FFFFEA", pad=(5, 3, 5, 3), waittime=400, wraplength=600
     ):
         # in miliseconds, originally 500
         self.waittime = waittime
         # in pixels, originally 180
         self.wraplength = wraplength
         self.canvas = canvas
-        self.text = text
-        self.canvas.tag_bind(tag_or_id, "<Enter>", self.on_enter)
-        self.canvas.tag_bind(tag_or_id, "<Leave>", self.on_leave)
-        self.canvas.tag_bind(tag_or_id, "<ButtonPress>", self.on_leave)
+        self.text = tk.StringVar()
         self.bg = bg
         self.pad = pad
         self.id = None
@@ -61,18 +50,13 @@ class CanvasTooltip:
 
     def show(self, event=None):
         def tip_pos_calculator(canvas, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)):
-
             c = canvas
-
             s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight()
-
             width, height = (
                 pad[0] + label.winfo_reqwidth() + pad[2],
                 pad[1] + label.winfo_reqheight() + pad[3],
             )
-
             mouse_x, mouse_y = c.winfo_pointerxy()
-
             x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1]
             x2, y2 = x1 + width, y1 + height
 
@@ -84,20 +68,14 @@ class CanvasTooltip:
                 y_delta = 0
 
             offscreen = (x_delta, y_delta) != (0, 0)
-
             if offscreen:
-
                 if x_delta:
                     x1 = mouse_x - tip_delta[0] - width
-
                 if y_delta:
                     y1 = mouse_y - tip_delta[1] - height
-
             offscreen_again = y1 < 0  # out on the top
-
             if offscreen_again:
                 y1 = 0
-
             return x1, y1
 
         bg = self.bg
@@ -111,21 +89,18 @@ class CanvasTooltip:
         self.tw.wm_overrideredirect(True)
 
         win = tk.Frame(self.tw, background=bg, borderwidth=0)
+        win.grid()
         label = ttk.Label(
             win,
-            text=self.text,
+            textvariable=self.text,
             justify=tk.LEFT,
             background=bg,
             relief=tk.SOLID,
             borderwidth=0,
             wraplength=self.wraplength,
         )
-
         label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW)
-        win.grid()
-
         x, y = tip_pos_calculator(canvas, label)
-
         self.tw.wm_geometry("+%d+%d" % (x, y))
 
     def hide(self):
diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py
index c2ab3a2f..d924e305 100644
--- a/coretk/coretk/coreclient.py
+++ b/coretk/coretk/coreclient.py
@@ -16,6 +16,17 @@ from coretk.wlannodeconfig import WlanNodeConfig
 
 NETWORK_NODES = {"switch", "hub", "wlan", "rj45", "tunnel", "emane"}
 DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"}
+OBSERVERS = {
+    "processes": "ps",
+    "ifconfig": "ifconfig",
+    "IPV4 Routes": "ip -4 ro",
+    "IPV6 Routes": "ip -6 ro",
+    "Listening sockets": "netstat -tuwnl",
+    "IPv4 MFC entries": "ip -4 mroute show",
+    "IPv6 MFC entries": "ip -6 mroute show",
+    "firewall rules": "iptables -L",
+    "IPSec policies": "setkey -DP",
+}
 
 
 class Node:
@@ -74,6 +85,12 @@ class CustomNode:
         self.services = services
 
 
+class Observer:
+    def __init__(self, name, cmd):
+        self.name = name
+        self.cmd = cmd
+
+
 class CoreClient:
     def __init__(self, app):
         """
@@ -86,13 +103,16 @@ class CoreClient:
         self.master = app.master
         self.interface_helper = None
         self.services = {}
+        self.observer = None
 
         # loaded configuration data
         self.servers = {}
         self.custom_nodes = {}
+        self.custom_observers = {}
         self.read_config()
 
         # data for managing the current session
+        self.state = None
         self.nodes = {}
         self.edges = {}
         self.hooks = {}
@@ -107,27 +127,36 @@ class CoreClient:
         self.emane_config = None
         self.serviceconfig_manager = ServiceNodeConfig(app)
 
+    def set_observer(self, value):
+        self.observer = value
+
     def read_config(self):
         # read distributed server
-        for server_config in self.app.config["servers"]:
-            server = CoreServer(
-                server_config["name"], server_config["address"], server_config["port"]
-            )
+        for config in self.app.config.get("servers", []):
+            server = CoreServer(config["name"], config["address"], config["port"])
             self.servers[server.name] = server
 
         # read custom nodes
-        for node in self.app.config["nodes"]:
-            image_file = node["image"]
+        for config in self.app.config.get("nodes", []):
+            image_file = config["image"]
             image = Images.get_custom(image_file)
             custom_node = CustomNode(
-                node["name"], image, image_file, set(node["services"])
+                config["name"], image, image_file, set(config["services"])
             )
             self.custom_nodes[custom_node.name] = custom_node
 
+        # read observers
+        for config in self.app.config.get("observers", []):
+            observer = Observer(config["name"], config["cmd"])
+            self.custom_observers[observer.name] = observer
+
     def handle_events(self, event):
         logging.info("event: %s", event)
-        if event.link_event is not None:
+        if event.HasField("link_event"):
             self.app.canvas.wireless_draw.hangle_link_event(event.link_event)
+        elif event.HasField("session_event"):
+            if event.session_event.event <= core_pb2.SessionState.SHUTDOWN:
+                self.state = event.session_event.event
 
     def handle_throughputs(self, event):
         interface_throughputs = event.interface_throughputs
@@ -161,7 +190,7 @@ class CoreClient:
         response = self.client.get_session(self.session_id)
         logging.info("joining session(%s): %s", self.session_id, response)
         session = response.session
-        session_state = session.state
+        self.state = session.state
         self.client.events(self.session_id, self.handle_events)
 
         # get hooks
@@ -209,11 +238,14 @@ class CoreClient:
         self.app.canvas.canvas_reset_and_redraw(session)
 
         # draw tool bar appropritate with session state
-        if session_state == core_pb2.SessionState.RUNTIME:
+        if self.is_runtime():
             self.app.toolbar.runtime_frame.tkraise()
         else:
             self.app.toolbar.design_frame.tkraise()
 
+    def is_runtime(self):
+        return self.state == core_pb2.SessionState.RUNTIME
+
     def create_new_session(self):
         """
         Create a new session
@@ -769,3 +801,7 @@ class CoreClient:
             )
             configs.append(config_proto)
         return configs
+
+    def run(self, node_id):
+        logging.info("running node(%s) cmd: %s", node_id, self.observer)
+        return self.client.node_command(self.session_id, node_id, self.observer).output
diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py
index fe9b5f6a..ed7e44f6 100644
--- a/coretk/coretk/dialogs/canvasbackground.py
+++ b/coretk/coretk/dialogs/canvasbackground.py
@@ -4,11 +4,11 @@ set wallpaper
 import enum
 import logging
 import tkinter as tk
-from tkinter import filedialog
+from tkinter import filedialog, ttk
 
 from PIL import Image, ImageTk
 
-from coretk.appdirs import BACKGROUNDS_PATH
+from coretk.appconfig import BACKGROUNDS_PATH
 from coretk.dialogs.dialog import Dialog
 
 
@@ -39,7 +39,6 @@ class CanvasBackgroundDialog(Dialog):
 
     def draw(self):
         self.columnconfigure(0, weight=1)
-        self.rowconfigure(0, weight=1)
         self.draw_image()
         self.draw_image_label()
         self.draw_image_selection()
@@ -48,65 +47,67 @@ class CanvasBackgroundDialog(Dialog):
         self.draw_buttons()
 
     def draw_image(self):
-        self.image_label = tk.Label(
-            self, text="(image preview)", height=8, width=32, bg="white"
+        self.image_label = ttk.Label(
+            self, text="(image preview)", width=32, anchor=tk.CENTER
         )
-        self.image_label.grid(row=0, column=0, pady=5, sticky="nsew")
+        self.image_label.grid(row=0, column=0, pady=5)
 
     def draw_image_label(self):
-        label = tk.Label(self, text="Image filename: ")
+        label = ttk.Label(self, text="Image filename: ")
         label.grid(row=1, column=0, sticky="ew")
 
     def draw_image_selection(self):
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.columnconfigure(0, weight=2)
         frame.columnconfigure(1, weight=1)
         frame.columnconfigure(2, weight=1)
         frame.grid(row=2, column=0, sticky="ew")
 
-        entry = tk.Entry(frame, textvariable=self.file_name)
+        entry = ttk.Entry(frame, textvariable=self.file_name)
         entry.focus()
         entry.grid(row=0, column=0, sticky="ew")
 
-        button = tk.Button(frame, text="...", command=self.click_open_image)
+        button = ttk.Button(frame, text="...", command=self.click_open_image)
         button.grid(row=0, column=1, sticky="ew")
 
-        button = tk.Button(frame, text="Clear", command=self.click_clear)
+        button = ttk.Button(frame, text="Clear", command=self.click_clear)
         button.grid(row=0, column=2, sticky="ew")
 
     def draw_options(self):
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.columnconfigure(0, weight=1)
         frame.columnconfigure(1, weight=1)
         frame.columnconfigure(2, weight=1)
         frame.columnconfigure(3, weight=1)
         frame.grid(row=3, column=0, sticky="ew")
 
-        button = tk.Radiobutton(
+        button = ttk.Radiobutton(
             frame, text="upper-left", value=1, variable=self.radiovar
         )
         button.grid(row=0, column=0, sticky="ew")
         self.options.append(button)
 
-        button = tk.Radiobutton(frame, text="centered", value=2, variable=self.radiovar)
+        button = ttk.Radiobutton(
+            frame, text="centered", value=2, variable=self.radiovar
+        )
         button.grid(row=0, column=1, sticky="ew")
         self.options.append(button)
 
-        button = tk.Radiobutton(frame, text="scaled", value=3, variable=self.radiovar)
+        button = ttk.Radiobutton(frame, text="scaled", value=3, variable=self.radiovar)
         button.grid(row=0, column=2, sticky="ew")
         self.options.append(button)
 
-        button = tk.Radiobutton(frame, text="titled", value=4, variable=self.radiovar)
+        button = ttk.Radiobutton(frame, text="titled", value=4, variable=self.radiovar)
         button.grid(row=0, column=3, sticky="ew")
         self.options.append(button)
 
     def draw_additional_options(self):
-        checkbutton = tk.Checkbutton(
+        checkbutton = ttk.Checkbutton(
             self, text="Show grid", variable=self.show_grid_var
         )
         checkbutton.grid(row=4, column=0, sticky="ew", padx=5)
 
-        checkbutton = tk.Checkbutton(
+        checkbutton = ttk.Checkbutton(
             self,
             text="Adjust canvas size to image dimensions",
             variable=self.adjust_to_dim_var,
@@ -118,15 +119,15 @@ class CanvasBackgroundDialog(Dialog):
         self.adjust_to_dim_var.set(0)
 
     def draw_buttons(self):
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(row=6, column=0, pady=5, sticky="ew")
         frame.columnconfigure(0, weight=1)
         frame.columnconfigure(1, weight=1)
 
-        button = tk.Button(frame, text="Apply", command=self.click_apply)
+        button = ttk.Button(frame, text="Apply", command=self.click_apply)
         button.grid(row=0, column=0, sticky="ew")
 
-        button = tk.Button(frame, text="Cancel", command=self.destroy)
+        button = ttk.Button(frame, text="Cancel", command=self.destroy)
         button.grid(row=0, column=1, sticky="ew")
 
     def click_open_image(self):
@@ -144,7 +145,7 @@ class CanvasBackgroundDialog(Dialog):
             img = Image.open(filename)
             img = img.resize((width, height), Image.ANTIALIAS)
             tk_img = ImageTk.PhotoImage(img)
-            self.image_label.config(image=tk_img, width=width, height=height)
+            self.image_label.config(image=tk_img, width=width)
             self.image_label.image = tk_img
 
     def click_clear(self):
@@ -156,7 +157,7 @@ class CanvasBackgroundDialog(Dialog):
         # delete entry
         self.file_name.set("")
         # delete display image
-        self.image_label.config(image="", width=32, height=8)
+        self.image_label.config(image="", width=32)
 
     def click_adjust_canvas(self):
         # deselect all radio buttons and grey them out
diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py
index 20b464d0..1d880639 100644
--- a/coretk/coretk/dialogs/canvassizeandscale.py
+++ b/coretk/coretk/dialogs/canvassizeandscale.py
@@ -2,7 +2,7 @@
 size and scale
 """
 import tkinter as tk
-from tkinter import font
+from tkinter import font, ttk
 
 from coretk.dialogs.canvasbackground import ScaleOption
 from coretk.dialogs.dialog import Dialog
@@ -49,120 +49,120 @@ class SizeAndScaleDialog(Dialog):
         self.draw_buttons()
 
     def draw_size(self):
-        label = tk.Label(self, text="Size", font=self.section_font)
+        label = ttk.Label(self, text="Size", font=self.section_font)
         label.grid(sticky="w")
 
         # draw size row 1
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(sticky="ew", pady=3)
         frame.columnconfigure(1, weight=1)
         frame.columnconfigure(3, weight=1)
-        label = tk.Label(frame, text="Width")
+        label = ttk.Label(frame, text="Width")
         label.grid(row=0, column=0, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.pixel_width)
+        entry = ttk.Entry(frame, textvariable=self.pixel_width)
         entry.grid(row=0, column=1, sticky="ew")
-        label = tk.Label(frame, text="x Height")
+        label = ttk.Label(frame, text="x Height")
         label.grid(row=0, column=2, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.pixel_height)
+        entry = ttk.Entry(frame, textvariable=self.pixel_height)
         entry.grid(row=0, column=3, sticky="ew")
-        label = tk.Label(frame, text="Pixels")
+        label = ttk.Label(frame, text="Pixels")
         label.grid(row=0, column=4, sticky="w")
 
         # draw size row 2
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(sticky="ew", pady=3)
         frame.columnconfigure(1, weight=1)
         frame.columnconfigure(3, weight=1)
-        label = tk.Label(frame, text="Width")
+        label = ttk.Label(frame, text="Width")
         label.grid(row=0, column=0, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.meters_width)
+        entry = ttk.Entry(frame, textvariable=self.meters_width)
         entry.grid(row=0, column=1, sticky="ew")
-        label = tk.Label(frame, text="x Height")
+        label = ttk.Label(frame, text="x Height")
         label.grid(row=0, column=2, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.meters_height)
+        entry = ttk.Entry(frame, textvariable=self.meters_height)
         entry.grid(row=0, column=3, sticky="ew")
-        label = tk.Label(frame, text="Meters")
+        label = ttk.Label(frame, text="Meters")
         label.grid(row=0, column=4, sticky="w")
 
     def draw_scale(self):
-        label = tk.Label(self, text="Scale", font=self.section_font)
+        label = ttk.Label(self, text="Scale", font=self.section_font)
         label.grid(sticky="w")
 
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(sticky="ew")
         frame.columnconfigure(1, weight=1)
-        label = tk.Label(frame, text="100 Pixels =")
+        label = ttk.Label(frame, text="100 Pixels =")
         label.grid(row=0, column=0, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.scale)
+        entry = ttk.Entry(frame, textvariable=self.scale)
         entry.grid(row=0, column=1, sticky="ew")
-        label = tk.Label(frame, text="Meters")
+        label = ttk.Label(frame, text="Meters")
         label.grid(row=0, column=2, sticky="w")
 
     def draw_reference_point(self):
-        label = tk.Label(self, text="Reference point", font=self.section_font)
+        label = ttk.Label(self, text="Reference point", font=self.section_font)
         label.grid(sticky="w")
-        label = tk.Label(
+        label = ttk.Label(
             self, text="Default is (0, 0), the upper left corner of the canvas"
         )
         label.grid(sticky="w")
 
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(sticky="ew", pady=3)
         frame.columnconfigure(1, weight=1)
         frame.columnconfigure(3, weight=1)
 
-        label = tk.Label(frame, text="X")
+        label = ttk.Label(frame, text="X")
         label.grid(row=0, column=0, sticky="w")
         x_var = tk.StringVar(value=0)
-        entry = tk.Entry(frame, textvariable=x_var)
+        entry = ttk.Entry(frame, textvariable=x_var)
         entry.grid(row=0, column=1, sticky="ew")
 
-        label = tk.Label(frame, text="Y")
+        label = ttk.Label(frame, text="Y")
         label.grid(row=0, column=2, sticky="w")
         y_var = tk.StringVar(value=0)
-        entry = tk.Entry(frame, textvariable=y_var)
+        entry = ttk.Entry(frame, textvariable=y_var)
         entry.grid(row=0, column=3, sticky="ew")
 
-        label = tk.Label(self, text="Translates To")
+        label = ttk.Label(self, text="Translates To")
         label.grid(sticky="w")
 
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(sticky="ew", pady=3)
         frame.columnconfigure(1, weight=1)
         frame.columnconfigure(3, weight=1)
         frame.columnconfigure(5, weight=1)
 
-        label = tk.Label(frame, text="Lat")
+        label = ttk.Label(frame, text="Lat")
         label.grid(row=0, column=0, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.lat)
+        entry = ttk.Entry(frame, textvariable=self.lat)
         entry.grid(row=0, column=1, sticky="ew")
 
-        label = tk.Label(frame, text="Lon")
+        label = ttk.Label(frame, text="Lon")
         label.grid(row=0, column=2, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.lon)
+        entry = ttk.Entry(frame, textvariable=self.lon)
         entry.grid(row=0, column=3, sticky="ew")
 
-        label = tk.Label(frame, text="Alt")
+        label = ttk.Label(frame, text="Alt")
         label.grid(row=0, column=4, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.alt)
+        entry = ttk.Entry(frame, textvariable=self.alt)
         entry.grid(row=0, column=5, sticky="ew")
 
     def draw_save_as_default(self):
-        button = tk.Checkbutton(
+        button = ttk.Checkbutton(
             self, text="Save as default?", variable=self.save_default
         )
         button.grid(sticky="w", pady=3)
 
     def draw_buttons(self):
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.columnconfigure(0, weight=1)
         frame.columnconfigure(1, weight=1)
         frame.grid(sticky="ew")
 
-        button = tk.Button(frame, text="Apply", command=self.click_apply)
+        button = ttk.Button(frame, text="Apply", command=self.click_apply)
         button.grid(row=0, column=0, pady=5, sticky="ew")
 
-        button = tk.Button(frame, text="Cancel", command=self.destroy)
+        button = ttk.Button(frame, text="Cancel", command=self.destroy)
         button.grid(row=0, column=1, pady=5, sticky="ew")
 
     def redraw_grid(self):
diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py
index 20c5ae95..f5c1ab78 100644
--- a/coretk/coretk/dialogs/customnodes.py
+++ b/coretk/coretk/dialogs/customnodes.py
@@ -1,8 +1,8 @@
 import logging
 import tkinter as tk
 from pathlib import Path
+from tkinter import ttk
 
-from coretk import appdirs
 from coretk.coreclient import CustomNode
 from coretk.dialogs.dialog import Dialog
 from coretk.dialogs.icondialog import IconDialog
@@ -22,7 +22,7 @@ class ServicesSelectDialog(Dialog):
         self.columnconfigure(0, weight=1)
         self.rowconfigure(0, weight=1)
 
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(stick="nsew")
         frame.rowconfigure(0, weight=1)
         for i in range(3):
@@ -44,13 +44,13 @@ class ServicesSelectDialog(Dialog):
         for service in sorted(self.current_services):
             self.current.listbox.insert(tk.END, service)
 
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(stick="ew")
         for i in range(2):
             frame.columnconfigure(i, weight=1)
-        button = tk.Button(frame, text="Save", command=self.destroy)
+        button = ttk.Button(frame, text="Save", command=self.destroy)
         button.grid(row=0, column=0, sticky="ew")
-        button = tk.Button(frame, text="Cancel", command=self.click_cancel)
+        button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
         button.grid(row=0, column=1, sticky="ew")
 
         # trigger group change
@@ -103,7 +103,7 @@ class CustomNodesDialog(Dialog):
         self.draw_buttons()
 
     def draw_node_config(self):
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(sticky="nsew")
         frame.columnconfigure(0, weight=1)
         frame.rowconfigure(0, weight=1)
@@ -114,45 +114,45 @@ class CustomNodesDialog(Dialog):
         for name in sorted(self.app.core.custom_nodes):
             self.nodes_list.listbox.insert(tk.END, name)
 
-        frame = tk.Frame(frame)
+        frame = ttk.Frame(frame)
         frame.grid(row=0, column=2, sticky="nsew")
         frame.columnconfigure(0, weight=1)
-        entry = tk.Entry(frame, textvariable=self.name)
+        entry = ttk.Entry(frame, textvariable=self.name)
         entry.grid(sticky="ew")
-        self.image_button = tk.Button(frame, text="Icon", command=self.click_icon)
+        self.image_button = ttk.Button(frame, text="Icon", command=self.click_icon)
         self.image_button.grid(sticky="ew")
-        button = tk.Button(frame, text="Services", command=self.click_services)
+        button = ttk.Button(frame, text="Services", command=self.click_services)
         button.grid(sticky="ew")
 
     def draw_node_buttons(self):
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(pady=2, sticky="ew")
         for i in range(3):
             frame.columnconfigure(i, weight=1)
 
-        button = tk.Button(frame, text="Create", command=self.click_create)
+        button = ttk.Button(frame, text="Create", command=self.click_create)
         button.grid(row=0, column=0, sticky="ew")
 
-        self.edit_button = tk.Button(
+        self.edit_button = ttk.Button(
             frame, text="Edit", state=tk.DISABLED, command=self.click_edit
         )
         self.edit_button.grid(row=0, column=1, sticky="ew")
 
-        self.delete_button = tk.Button(
+        self.delete_button = ttk.Button(
             frame, text="Delete", state=tk.DISABLED, command=self.click_delete
         )
         self.delete_button.grid(row=0, column=2, sticky="ew")
 
     def draw_buttons(self):
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(sticky="ew")
         for i in range(2):
             frame.columnconfigure(i, weight=1)
 
-        button = tk.Button(frame, text="Save", command=self.click_save)
+        button = ttk.Button(frame, text="Save", command=self.click_save)
         button.grid(row=0, column=0, sticky="ew")
 
-        button = tk.Button(frame, text="Cancel", command=self.destroy)
+        button = ttk.Button(frame, text="Cancel", command=self.destroy)
         button.grid(row=0, column=1, sticky="ew")
 
     def reset_values(self):
@@ -189,7 +189,8 @@ class CustomNodesDialog(Dialog):
                 }
             )
         logging.info("saving custom nodes: %s", self.app.config["nodes"])
-        appdirs.save_config(self.app.config)
+        self.app.save_config()
+        self.destroy()
 
     def click_create(self):
         name = self.name.get()
diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py
index 6950dc10..7fd577e2 100644
--- a/coretk/coretk/dialogs/emaneconfig.py
+++ b/coretk/coretk/dialogs/emaneconfig.py
@@ -58,18 +58,18 @@ class EmaneConfiguration(Dialog):
         print("not implemented")
 
     def node_name_and_image(self):
-        f = tk.Frame(self, bg="#d9d9d9")
+        f = ttk.Frame(self)
 
-        lbl = tk.Label(f, text="Node name:", bg="#d9d9d9")
+        lbl = ttk.Label(f, text="Node name:")
         lbl.grid(row=0, column=0, padx=2, pady=2)
-        e = tk.Entry(f, textvariable=self.create_text_variable(""), bg="white")
+        e = ttk.Entry(f, textvariable=self.create_text_variable(""))
         e.grid(row=0, column=1, padx=2, pady=2)
 
         cbb = ttk.Combobox(f, values=["(none)", "core1", "core2"], state="readonly")
         cbb.current(0)
         cbb.grid(row=0, column=2, padx=2, pady=2)
 
-        b = tk.Button(f, image=self.canvas_node.image)
+        b = ttk.Button(f, image=self.canvas_node.image)
         b.grid(row=0, column=3, padx=2, pady=2)
 
         f.grid(row=0, column=0, sticky="nsew")
@@ -96,13 +96,13 @@ class EmaneConfiguration(Dialog):
         self.emane_config_frame.draw_config()
         self.emane_config_frame.grid(sticky="nsew")
 
-        frame = tk.Frame(self.emane_dialog)
+        frame = ttk.Frame(self.emane_dialog)
         frame.grid(sticky="ew")
         for i in range(2):
             frame.columnconfigure(i, weight=1)
-        b1 = tk.Button(frame, text="Appy", command=self.save_emane_option)
+        b1 = ttk.Button(frame, text="Appy", command=self.save_emane_option)
         b1.grid(row=0, column=0, sticky="ew")
-        b2 = tk.Button(frame, text="Cancel", command=self.emane_dialog.destroy)
+        b2 = ttk.Button(frame, text="Cancel", command=self.emane_dialog.destroy)
         b2.grid(row=0, column=1, sticky="ew")
         self.emane_dialog.show()
 
@@ -170,35 +170,33 @@ class EmaneConfiguration(Dialog):
         self.model_config_frame.grid(sticky="nsew")
         self.model_config_frame.draw_config()
 
-        frame = tk.Frame(self.emane_model_dialog)
+        frame = ttk.Frame(self.emane_model_dialog)
         frame.grid(sticky="ew")
         for i in range(2):
             frame.columnconfigure(i, weight=1)
-        b1 = tk.Button(frame, text="Apply", command=self.save_emane_model_options)
+        b1 = ttk.Button(frame, text="Apply", command=self.save_emane_model_options)
         b1.grid(row=0, column=0, sticky="ew")
-        b2 = tk.Button(frame, text="Cancel", command=self.emane_model_dialog.destroy)
+        b2 = ttk.Button(frame, text="Cancel", command=self.emane_model_dialog.destroy)
         b2.grid(row=0, column=1, sticky="ew")
         self.emane_model_dialog.show()
 
     def draw_option_buttons(self, parent):
-        f = tk.Frame(parent, bg="#d9d9d9")
+        f = ttk.Frame(parent)
         f.columnconfigure(0, weight=1)
         f.columnconfigure(1, weight=1)
-        b = tk.Button(
+        b = ttk.Button(
             f,
             text=self.emane_models[0] + " options",
             image=Images.get(ImageEnum.EDITNODE),
             compound=tk.RIGHT,
-            bg="#d9d9d9",
             command=self.draw_model_options,
         )
         b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew")
-        b = tk.Button(
+        b = ttk.Button(
             f,
             text="EMANE options",
             image=Images.get(ImageEnum.EDITNODE),
             compound=tk.RIGHT,
-            bg="#d9d9d9",
             command=self.draw_emane_options,
         )
         b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew")
@@ -233,21 +231,14 @@ class EmaneConfiguration(Dialog):
         self.emane_models = [x.split("_")[1] for x in response.models]
 
         # create combo box and its binding
-        f = tk.Frame(
-            parent,
-            bg="#d9d9d9",
-            highlightbackground="#b3b3b3",
-            highlightcolor="#b3b3b3",
-            highlightthickness=0.5,
-            bd=0,
-        )
+        f = ttk.Frame(parent)
         self.emane_model_combobox = ttk.Combobox(
             f, values=self.emane_models, state="readonly"
         )
         self.emane_model_combobox.grid()
         self.emane_model_combobox.current(0)
         self.emane_model_combobox.bind("<<ComboboxSelected>>", self.combobox_select)
-        f.grid(row=3, column=0, sticky=tk.W + tk.E)
+        f.grid(row=3, column=0, sticky="ew")
 
     def draw_text_label_and_entry(self, parent, label_text, entry_text):
         """
@@ -257,10 +248,10 @@ class EmaneConfiguration(Dialog):
         """
         var = tk.StringVar()
         var.set(entry_text)
-        f = tk.Frame(parent)
-        lbl = tk.Label(f, text=label_text)
+        f = ttk.Frame(parent)
+        lbl = ttk.Label(f, text=label_text)
         lbl.grid(row=0, column=0)
-        e = tk.Entry(f, textvariable=var, bg="white")
+        e = ttk.Entry(f, textvariable=var)
         e.grid(row=0, column=1)
         f.grid(stick=tk.W, padx=2, pady=2)
 
@@ -271,44 +262,33 @@ class EmaneConfiguration(Dialog):
         :return: nothing
         """
         # draw label
-        lbl = tk.Label(self, text="Emane")
+        lbl = ttk.Label(self, text="Emane")
         lbl.grid(row=1, column=0)
 
         # main frame that has emane wiki, a short description, emane models and the configure buttons
-        f = tk.Frame(
-            self,
-            bg="#d9d9d9",
-            highlightbackground="#b3b3b3",
-            highlightcolor="#b3b3b3",
-            highlightthickness=0.5,
-            bd=0,
-            relief=tk.RAISED,
-        )
+        f = ttk.Frame(self)
         f.columnconfigure(0, weight=1)
 
-        b = tk.Button(
+        b = ttk.Button(
             f,
             image=Images.get(ImageEnum.EDITNODE),
             text="EMANE Wiki",
             compound=tk.RIGHT,
-            relief=tk.RAISED,
-            bg="#d9d9d9",
             command=lambda: webbrowser.open_new(
                 "https://github.com/adjacentlink/emane/wiki"
             ),
         )
-        b.grid(row=0, column=0, sticky=tk.W)
+        b.grid(row=0, column=0, sticky="w")
 
-        lbl = tk.Label(
+        lbl = ttk.Label(
             f,
             text="The EMANE emulation system provides more complex wireless radio emulation "
             "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details",
-            bg="#d9d9d9",
         )
         lbl.grid(row=1, column=0, sticky="nsew")
 
-        lbl = tk.Label(f, text="EMANE Models", bg="#d9d9d9")
-        lbl.grid(row=2, column=0, sticky=tk.W)
+        lbl = ttk.Label(f, text="EMANE Models")
+        lbl.grid(row=2, column=0, sticky="w")
 
         self.draw_emane_models(f)
         self.draw_option_buttons(f)
@@ -325,12 +305,12 @@ class EmaneConfiguration(Dialog):
 
         :return:
         """
-        f = tk.Frame(self, bg="#d9d9d9")
+        f = ttk.Frame(self)
         f.columnconfigure(0, weight=1)
         f.columnconfigure(1, weight=1)
-        b = tk.Button(f, text="Link to all routers", bg="#d9d9d9")
+        b = ttk.Button(f, text="Link to all routers")
         b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew")
-        b = tk.Button(f, text="Choose WLAN members", bg="#d9d9d9")
+        b = ttk.Button(f, text="Choose WLAN members")
         b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew")
         f.grid(row=5, column=0, sticky="nsew")
 
@@ -340,12 +320,12 @@ class EmaneConfiguration(Dialog):
         self.destroy()
 
     def draw_apply_and_cancel(self):
-        f = tk.Frame(self, bg="#d9d9d9")
+        f = ttk.Frame(self)
         f.columnconfigure(0, weight=1)
         f.columnconfigure(1, weight=1)
-        b = tk.Button(f, text="Apply", bg="#d9d9d9", command=self.apply)
+        b = ttk.Button(f, text="Apply", command=self.apply)
         b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew")
-        b = tk.Button(f, text="Cancel", bg="#d9d9d9", command=self.destroy)
+        b = ttk.Button(f, text="Cancel", command=self.destroy)
         b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew")
 
         f.grid(sticky="nsew")
diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py
index 99647635..5ae9da8a 100644
--- a/coretk/coretk/dialogs/hooks.py
+++ b/coretk/coretk/dialogs/hooks.py
@@ -19,14 +19,14 @@ class HookDialog(Dialog):
         self.rowconfigure(1, weight=1)
 
         # name and states
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(row=0, sticky="ew", pady=2)
         frame.columnconfigure(0, weight=2)
         frame.columnconfigure(1, weight=7)
         frame.columnconfigure(2, weight=1)
-        label = tk.Label(frame, text="Name")
+        label = ttk.Label(frame, text="Name")
         label.grid(row=0, column=0, sticky="ew")
-        entry = tk.Entry(frame, textvariable=self.name)
+        entry = ttk.Entry(frame, textvariable=self.name)
         entry.grid(row=0, column=1, sticky="ew")
         values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE")
         initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME)
@@ -39,7 +39,7 @@ class HookDialog(Dialog):
         combobox.bind("<<ComboboxSelected>>", self.state_change)
 
         # data
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.columnconfigure(0, weight=1)
         frame.rowconfigure(0, weight=1)
         frame.grid(row=1, sticky="nsew", pady=2)
@@ -53,19 +53,19 @@ class HookDialog(Dialog):
             ),
         )
         self.data.grid(row=0, column=0, sticky="nsew")
-        scrollbar = tk.Scrollbar(frame)
+        scrollbar = ttk.Scrollbar(frame)
         scrollbar.grid(row=0, column=1, sticky="ns")
         self.data.config(yscrollcommand=scrollbar.set)
         scrollbar.config(command=self.data.yview)
 
         # button row
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(row=2, sticky="ew", pady=2)
         for i in range(2):
             frame.columnconfigure(i, weight=1)
-        button = tk.Button(frame, text="Save", command=lambda: self.save())
+        button = ttk.Button(frame, text="Save", command=lambda: self.save())
         button.grid(row=0, column=0, sticky="ew")
-        button = tk.Button(frame, text="Cancel", command=lambda: self.destroy())
+        button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
         button.grid(row=0, column=1, sticky="ew")
 
     def state_change(self, event):
@@ -106,21 +106,21 @@ class HooksDialog(Dialog):
         self.listbox.bind("<<ListboxSelect>>", self.select)
         for hook_file in self.app.core.hooks:
             self.listbox.insert(tk.END, hook_file)
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(row=1, sticky="ew")
         for i in range(4):
             frame.columnconfigure(i, weight=1)
-        button = tk.Button(frame, text="Create", command=self.click_create)
+        button = ttk.Button(frame, text="Create", command=self.click_create)
         button.grid(row=0, column=0, sticky="ew")
-        self.edit_button = tk.Button(
+        self.edit_button = ttk.Button(
             frame, text="Edit", state=tk.DISABLED, command=self.click_edit
         )
         self.edit_button.grid(row=0, column=1, sticky="ew")
-        self.delete_button = tk.Button(
+        self.delete_button = ttk.Button(
             frame, text="Delete", state=tk.DISABLED, command=self.click_delete
         )
         self.delete_button.grid(row=0, column=2, sticky="ew")
-        button = tk.Button(frame, text="Cancel", command=lambda: self.destroy())
+        button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy())
         button.grid(row=0, column=3, sticky="ew")
 
     def click_create(self):
diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py
index 4d26e29f..fb6fb6bb 100644
--- a/coretk/coretk/dialogs/icondialog.py
+++ b/coretk/coretk/dialogs/icondialog.py
@@ -1,7 +1,7 @@
 import tkinter as tk
-from tkinter import filedialog
+from tkinter import filedialog, ttk
 
-from coretk.appdirs import ICONS_PATH
+from coretk.appconfig import ICONS_PATH
 from coretk.dialogs.dialog import Dialog
 from coretk.images import Images
 
@@ -18,30 +18,30 @@ class IconDialog(Dialog):
         self.columnconfigure(0, weight=1)
 
         # row one
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(row=0, column=0, pady=2, sticky="ew")
         frame.columnconfigure(0, weight=1)
         frame.columnconfigure(1, weight=3)
-        label = tk.Label(frame, text="Image")
+        label = ttk.Label(frame, text="Image")
         label.grid(row=0, column=0, sticky="ew")
-        entry = tk.Entry(frame, textvariable=self.file_path)
+        entry = ttk.Entry(frame, textvariable=self.file_path)
         entry.grid(row=0, column=1, sticky="ew")
-        button = tk.Button(frame, text="...", command=self.click_file)
+        button = ttk.Button(frame, text="...", command=self.click_file)
         button.grid(row=0, column=2)
 
         # row two
-        self.image_label = tk.Label(self, image=self.image)
+        self.image_label = ttk.Label(self, image=self.image, anchor=tk.CENTER)
         self.image_label.grid(row=1, column=0, pady=2, sticky="ew")
 
         # row three
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(row=2, column=0, sticky="ew")
         frame.columnconfigure(0, weight=1)
         frame.columnconfigure(1, weight=1)
-        button = tk.Button(frame, text="Apply", command=self.destroy)
+        button = ttk.Button(frame, text="Apply", command=self.destroy)
         button.grid(row=0, column=0, sticky="ew")
 
-        button = tk.Button(frame, text="Cancel", command=self.click_cancel)
+        button = ttk.Button(frame, text="Cancel", command=self.click_cancel)
         button.grid(row=0, column=1, sticky="ew")
 
     def click_file(self):
diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py
new file mode 100644
index 00000000..6c57f08e
--- /dev/null
+++ b/coretk/coretk/dialogs/observers.py
@@ -0,0 +1,149 @@
+import tkinter as tk
+from tkinter import ttk
+
+from coretk.coreclient import Observer
+from coretk.dialogs.dialog import Dialog
+
+
+class ObserverDialog(Dialog):
+    def __init__(self, master, app):
+        super().__init__(master, app, "Observer Widgets", modal=True)
+        self.observers = None
+        self.save_button = None
+        self.delete_button = None
+        self.selected = None
+        self.selected_index = None
+        self.name = tk.StringVar()
+        self.cmd = tk.StringVar()
+        self.draw()
+
+    def draw(self):
+        self.columnconfigure(0, weight=1)
+        self.rowconfigure(0, weight=1)
+        self.draw_listbox()
+        self.draw_form_fields()
+        self.draw_config_buttons()
+        self.draw_apply_buttons()
+
+    def draw_listbox(self):
+        frame = ttk.Frame(self)
+        frame.grid(sticky="nsew")
+        frame.columnconfigure(0, weight=1)
+        frame.rowconfigure(0, weight=1)
+
+        scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL)
+        scrollbar.grid(row=0, column=1, sticky="ns")
+
+        self.observers = tk.Listbox(
+            frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set
+        )
+        self.observers.grid(row=0, column=0, sticky="nsew")
+        self.observers.bind("<<ListboxSelect>>", self.handle_observer_change)
+        for name in sorted(self.app.core.custom_observers):
+            self.observers.insert(tk.END, name)
+
+        scrollbar.config(command=self.observers.yview)
+
+    def draw_form_fields(self):
+        frame = ttk.Frame(self)
+        frame.grid(sticky="ew")
+        frame.columnconfigure(1, weight=1)
+
+        label = ttk.Label(frame, text="Name")
+        label.grid(row=0, column=0, sticky="w")
+        entry = ttk.Entry(frame, textvariable=self.name)
+        entry.grid(row=0, column=1, sticky="ew")
+
+        label = ttk.Label(frame, text="Command")
+        label.grid(row=1, column=0, sticky="w")
+        entry = ttk.Entry(frame, textvariable=self.cmd)
+        entry.grid(row=1, column=1, sticky="ew")
+
+    def draw_config_buttons(self):
+        frame = ttk.Frame(self)
+        frame.grid(pady=2, sticky="ew")
+        for i in range(3):
+            frame.columnconfigure(i, weight=1)
+
+        button = ttk.Button(frame, text="Create", command=self.click_create)
+        button.grid(row=0, column=0, sticky="ew")
+
+        self.save_button = ttk.Button(
+            frame, text="Save", state=tk.DISABLED, command=self.click_save
+        )
+        self.save_button.grid(row=0, column=1, sticky="ew")
+
+        self.delete_button = ttk.Button(
+            frame, text="Delete", state=tk.DISABLED, command=self.click_delete
+        )
+        self.delete_button.grid(row=0, column=2, sticky="ew")
+
+    def draw_apply_buttons(self):
+        frame = ttk.Frame(self)
+        frame.grid(sticky="ew")
+        for i in range(2):
+            frame.columnconfigure(i, weight=1)
+
+        button = ttk.Button(frame, text="Save", command=self.click_save_config)
+        button.grid(row=0, column=0, sticky="ew")
+
+        button = ttk.Button(frame, text="Cancel", command=self.destroy)
+        button.grid(row=0, column=1, sticky="ew")
+
+    def click_save_config(self):
+        observers = []
+        for name in sorted(self.app.core.custom_observers):
+            observer = self.app.core.custom_observers[name]
+            observers.append({"name": observer.name, "cmd": observer.cmd})
+        self.app.config["observers"] = observers
+        self.app.save_config()
+        self.destroy()
+
+    def click_create(self):
+        name = self.name.get()
+        if name not in self.app.core.custom_observers:
+            cmd = self.cmd.get()
+            observer = Observer(name, cmd)
+            self.app.core.custom_observers[name] = observer
+            self.observers.insert(tk.END, name)
+
+    def click_save(self):
+        name = self.name.get()
+        if self.selected:
+            previous_name = self.selected
+            self.selected = name
+            observer = self.app.core.custom_observers.pop(previous_name)
+            observer.name = name
+            observer.cmd = self.cmd.get()
+            self.app.core.custom_observers[name] = observer
+            self.observers.delete(self.selected_index)
+            self.observers.insert(self.selected_index, name)
+            self.observers.selection_set(self.selected_index)
+
+    def click_delete(self):
+        if self.selected:
+            self.observers.delete(self.selected_index)
+            del self.app.core.custom_observers[self.selected]
+            self.selected = None
+            self.selected_index = None
+            self.name.set("")
+            self.cmd.set("")
+            self.observers.selection_clear(0, tk.END)
+            self.save_button.config(state=tk.DISABLED)
+            self.delete_button.config(state=tk.DISABLED)
+
+    def handle_observer_change(self, event):
+        selection = self.observers.curselection()
+        if selection:
+            self.selected_index = selection[0]
+            self.selected = self.observers.get(self.selected_index)
+            observer = self.app.core.custom_observers[self.selected]
+            self.name.set(observer.name)
+            self.cmd.set(observer.cmd)
+            self.save_button.config(state=tk.NORMAL)
+            self.delete_button.config(state=tk.NORMAL)
+        else:
+            self.selected_index = None
+            self.selected = None
+            self.save_button.config(state=tk.DISABLED)
+            self.delete_button.config(state=tk.DISABLED)
diff --git a/coretk/coretk/dialogs/observerwidgets.py b/coretk/coretk/dialogs/observerwidgets.py
deleted file mode 100644
index c1c4d170..00000000
--- a/coretk/coretk/dialogs/observerwidgets.py
+++ /dev/null
@@ -1,148 +0,0 @@
-import tkinter as tk
-
-from coretk.dialogs.dialog import Dialog
-
-
-class Widget:
-    def __init__(self, name, command):
-        self.name = name
-        self.command = command
-
-
-class ObserverWidgetsDialog(Dialog):
-    def __init__(self, master, app):
-        super().__init__(master, app, "Observer Widgets", modal=True)
-        self.config_widgets = {}
-        self.widgets = None
-        self.save_button = None
-        self.delete_button = None
-        self.selected = None
-        self.selected_index = None
-        self.name = tk.StringVar()
-        self.command = tk.StringVar()
-        self.draw()
-
-    def draw(self):
-        self.columnconfigure(0, weight=1)
-        self.rowconfigure(0, weight=1)
-        self.draw_widgets()
-        self.draw_widget_fields()
-        self.draw_widget_buttons()
-        self.draw_apply_buttons()
-
-    def draw_widgets(self):
-        frame = tk.Frame(self)
-        frame.grid(sticky="nsew")
-        frame.columnconfigure(0, weight=1)
-        frame.rowconfigure(0, weight=1)
-
-        scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL)
-        scrollbar.grid(row=0, column=1, sticky="ns")
-
-        self.widgets = tk.Listbox(
-            frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set
-        )
-        self.widgets.grid(row=0, column=0, sticky="nsew")
-        self.widgets.bind("<<ListboxSelect>>", self.handle_widget_change)
-
-        scrollbar.config(command=self.widgets.yview)
-
-    def draw_widget_fields(self):
-        frame = tk.Frame(self)
-        frame.grid(sticky="ew")
-        frame.columnconfigure(1, weight=1)
-
-        label = tk.Label(frame, text="Name")
-        label.grid(row=0, column=0, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.name)
-        entry.grid(row=0, column=1, sticky="ew")
-
-        label = tk.Label(frame, text="Command")
-        label.grid(row=1, column=0, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.command)
-        entry.grid(row=1, column=1, sticky="ew")
-
-    def draw_widget_buttons(self):
-        frame = tk.Frame(self)
-        frame.grid(pady=2, sticky="ew")
-        for i in range(3):
-            frame.columnconfigure(i, weight=1)
-
-        button = tk.Button(frame, text="Create", command=self.click_create)
-        button.grid(row=0, column=0, sticky="ew")
-
-        self.save_button = tk.Button(
-            frame, text="Save", state=tk.DISABLED, command=self.click_save
-        )
-        self.save_button.grid(row=0, column=1, sticky="ew")
-
-        self.delete_button = tk.Button(
-            frame, text="Delete", state=tk.DISABLED, command=self.click_delete
-        )
-        self.delete_button.grid(row=0, column=2, sticky="ew")
-
-    def draw_apply_buttons(self):
-        frame = tk.Frame(self)
-        frame.grid(sticky="ew")
-        for i in range(2):
-            frame.columnconfigure(i, weight=1)
-
-        button = tk.Button(
-            frame, text="Save Configuration", command=self.click_save_configuration
-        )
-        button.grid(row=0, column=0, sticky="ew")
-
-        button = tk.Button(frame, text="Cancel", command=self.destroy)
-        button.grid(row=0, column=1, sticky="ew")
-
-    def click_save_configuration(self):
-        pass
-
-    def click_create(self):
-        name = self.name.get()
-        if name not in self.config_widgets:
-            command = self.command.get()
-            widget = Widget(name, command)
-            self.config_widgets[name] = widget
-            self.widgets.insert(tk.END, name)
-
-    def click_save(self):
-        name = self.name.get()
-        if self.selected:
-            previous_name = self.selected
-            self.selected = name
-            widget = self.config_widgets.pop(previous_name)
-            widget.name = name
-            widget.command = self.command.get()
-            self.config_widgets[name] = widget
-            self.widgets.delete(self.selected_index)
-            self.widgets.insert(self.selected_index, name)
-            self.widgets.selection_set(self.selected_index)
-
-    def click_delete(self):
-        if self.selected:
-            self.widgets.delete(self.selected_index)
-            del self.config_widgets[self.selected]
-            self.selected = None
-            self.selected_index = None
-            self.name.set("")
-            self.command.set("")
-            self.widgets.selection_clear(0, tk.END)
-            self.save_button.config(state=tk.DISABLED)
-            self.delete_button.config(state=tk.DISABLED)
-
-    def handle_widget_change(self, event):
-        selection = self.widgets.curselection()
-        if selection:
-            self.selected_index = selection[0]
-            self.selected = self.widgets.get(self.selected_index)
-            widget = self.config_widgets[self.selected]
-            self.name.set(widget.name)
-            self.command.set(widget.command)
-            self.save_button.config(state=tk.NORMAL)
-            self.delete_button.config(state=tk.NORMAL)
-        else:
-            self.selected_index = None
-            self.selected = None
-            self.save_button.config(state=tk.DISABLED)
-            self.delete_button.config(state=tk.DISABLED)
diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py
new file mode 100644
index 00000000..74293a30
--- /dev/null
+++ b/coretk/coretk/dialogs/preferences.py
@@ -0,0 +1,67 @@
+import tkinter as tk
+from tkinter import ttk
+
+from coretk import appconfig
+from coretk.dialogs.dialog import Dialog
+
+
+class PreferencesDialog(Dialog):
+    def __init__(self, master, app):
+        super().__init__(master, app, "Preferences", modal=True)
+        preferences = self.app.config["preferences"]
+        self.editor = tk.StringVar(value=preferences["editor"])
+        self.terminal = tk.StringVar(value=preferences["terminal"])
+        self.gui3d = tk.StringVar(value=preferences["gui3d"])
+        self.draw()
+
+    def draw(self):
+        self.columnconfigure(0, weight=1)
+        self.draw_programs()
+        self.draw_buttons()
+
+    def draw_programs(self):
+        frame = ttk.LabelFrame(self, text="Programs")
+        frame.grid(sticky="ew", pady=2)
+        frame.columnconfigure(1, weight=1)
+
+        label = ttk.Label(frame, text="Editor")
+        label.grid(row=0, column=0, pady=2, padx=2, sticky="w")
+        combobox = ttk.Combobox(
+            frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly"
+        )
+        combobox.grid(row=0, column=1, sticky="ew")
+
+        label = ttk.Label(frame, text="Terminal")
+        label.grid(row=1, column=0, pady=2, padx=2, sticky="w")
+        combobox = ttk.Combobox(
+            frame,
+            textvariable=self.terminal,
+            values=appconfig.TERMINALS,
+            state="readonly",
+        )
+        combobox.grid(row=1, column=1, sticky="ew")
+
+        label = ttk.Label(frame, text="3D GUI")
+        label.grid(row=2, column=0, pady=2, padx=2, sticky="w")
+        entry = ttk.Entry(frame, textvariable=self.gui3d)
+        entry.grid(row=2, column=1, sticky="ew")
+
+    def draw_buttons(self):
+        frame = ttk.Frame(self)
+        frame.grid(sticky="ew")
+        for i in range(2):
+            frame.columnconfigure(i, weight=1)
+
+        button = ttk.Button(frame, text="Save", command=self.click_save)
+        button.grid(row=0, column=0, sticky="ew")
+
+        button = ttk.Button(frame, text="Cancel", command=self.destroy)
+        button.grid(row=0, column=1, sticky="ew")
+
+    def click_save(self):
+        preferences = self.app.config["preferences"]
+        preferences["terminal"] = self.terminal.get()
+        preferences["editor"] = self.editor.get()
+        preferences["gui3d"] = self.gui3d.get()
+        self.app.save_config()
+        self.destroy()
diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py
index 3ff95cde..25f3e826 100644
--- a/coretk/coretk/dialogs/servers.py
+++ b/coretk/coretk/dialogs/servers.py
@@ -1,6 +1,6 @@
 import tkinter as tk
+from tkinter import ttk
 
-from coretk import appdirs
 from coretk.coreclient import CoreServer
 from coretk.dialogs.dialog import Dialog
 
@@ -31,12 +31,12 @@ class ServersDialog(Dialog):
         self.draw_apply_buttons()
 
     def draw_servers(self):
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(pady=2, sticky="nsew")
         frame.columnconfigure(0, weight=1)
         frame.rowconfigure(0, weight=1)
 
-        scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL)
+        scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL)
         scrollbar.grid(row=0, column=1, sticky="ns")
 
         self.servers = tk.Listbox(
@@ -51,61 +51,61 @@ class ServersDialog(Dialog):
         scrollbar.config(command=self.servers.yview)
 
     def draw_server_configuration(self):
-        label = tk.Label(self, text="Server Configuration")
+        label = ttk.Label(self, text="Server Configuration")
         label.grid(pady=2, sticky="ew")
 
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(pady=2, sticky="ew")
         frame.columnconfigure(1, weight=1)
         frame.columnconfigure(3, weight=1)
         frame.columnconfigure(5, weight=1)
 
-        label = tk.Label(frame, text="Name")
+        label = ttk.Label(frame, text="Name")
         label.grid(row=0, column=0, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.name)
+        entry = ttk.Entry(frame, textvariable=self.name)
         entry.grid(row=0, column=1, sticky="ew")
 
-        label = tk.Label(frame, text="Address")
+        label = ttk.Label(frame, text="Address")
         label.grid(row=0, column=2, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.address)
+        entry = ttk.Entry(frame, textvariable=self.address)
         entry.grid(row=0, column=3, sticky="ew")
 
-        label = tk.Label(frame, text="Port")
+        label = ttk.Label(frame, text="Port")
         label.grid(row=0, column=4, sticky="w")
-        entry = tk.Entry(frame, textvariable=self.port)
+        entry = ttk.Entry(frame, textvariable=self.port)
         entry.grid(row=0, column=5, sticky="ew")
 
     def draw_servers_buttons(self):
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(pady=2, sticky="ew")
         for i in range(3):
             frame.columnconfigure(i, weight=1)
 
-        button = tk.Button(frame, text="Create", command=self.click_create)
+        button = ttk.Button(frame, text="Create", command=self.click_create)
         button.grid(row=0, column=0, sticky="ew")
 
-        self.save_button = tk.Button(
+        self.save_button = ttk.Button(
             frame, text="Save", state=tk.DISABLED, command=self.click_save
         )
         self.save_button.grid(row=0, column=1, sticky="ew")
 
-        self.delete_button = tk.Button(
+        self.delete_button = ttk.Button(
             frame, text="Delete", state=tk.DISABLED, command=self.click_delete
         )
         self.delete_button.grid(row=0, column=2, sticky="ew")
 
     def draw_apply_buttons(self):
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(sticky="ew")
         for i in range(2):
             frame.columnconfigure(i, weight=1)
 
-        button = tk.Button(
+        button = ttk.Button(
             frame, text="Save Configuration", command=self.click_save_configuration
         )
         button.grid(row=0, column=0, sticky="ew")
 
-        button = tk.Button(frame, text="Cancel", command=self.destroy)
+        button = ttk.Button(frame, text="Cancel", command=self.destroy)
         button.grid(row=0, column=1, sticky="ew")
 
     def click_save_configuration(self):
@@ -116,7 +116,8 @@ class ServersDialog(Dialog):
                 {"name": server.name, "address": server.address, "port": server.port}
             )
         self.app.config["servers"] = servers
-        appdirs.save_config(self.app.config)
+        self.app.save_config()
+        self.destroy()
 
     def click_create(self):
         name = self.name.get()
diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py
index 8702d581..45f26a58 100644
--- a/coretk/coretk/dialogs/sessionoptions.py
+++ b/coretk/coretk/dialogs/sessionoptions.py
@@ -1,5 +1,5 @@
 import logging
-import tkinter as tk
+from tkinter import ttk
 
 from coretk.dialogs.dialog import Dialog
 from coretk.widgets import ConfigFrame
@@ -26,13 +26,13 @@ class SessionOptionsDialog(Dialog):
         self.config_frame.draw_config()
         self.config_frame.grid(sticky="nsew")
 
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         frame.grid(sticky="ew")
         for i in range(2):
             frame.columnconfigure(i, weight=1)
-        button = tk.Button(frame, text="Save", command=self.save)
+        button = ttk.Button(frame, text="Save", command=self.save)
         button.grid(row=0, column=0, pady=PAD_Y, padx=PAD_X, sticky="ew")
-        button = tk.Button(frame, text="Cancel", command=self.destroy)
+        button = ttk.Button(frame, text="Cancel", command=self.destroy)
         button.grid(row=0, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew")
 
     def save(self):
diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py
index b7c4d128..40d09c44 100644
--- a/coretk/coretk/dialogs/sessions.py
+++ b/coretk/coretk/dialogs/sessions.py
@@ -1,6 +1,6 @@
 import logging
 import tkinter as tk
-from tkinter.ttk import Scrollbar, Treeview
+from tkinter import ttk
 
 from core.api.grpc import core_pb2
 from coretk.dialogs.dialog import Dialog
@@ -9,12 +9,6 @@ from coretk.images import ImageEnum, Images
 
 class SessionsDialog(Dialog):
     def __init__(self, master, app):
-        """
-        create session table instance
-
-        :param coretk.coreclient.CoreClient grpc: coregrpc
-        :param root.master master:
-        """
         super().__init__(master, app, "Sessions", modal=True)
         self.selected = False
         self.selected_id = None
@@ -32,7 +26,7 @@ class SessionsDialog(Dialog):
         write a short description
         :return: nothing
         """
-        label = tk.Label(
+        label = ttk.Label(
             self,
             text="Below is a list of active CORE sessions. Double-click to \n"
             "connect to an existing session. Usually, only sessions in \n"
@@ -42,7 +36,9 @@ class SessionsDialog(Dialog):
         label.grid(row=0, sticky="ew", pady=5)
 
     def draw_tree(self):
-        self.tree = Treeview(self, columns=("id", "state", "nodes"), show="headings")
+        self.tree = ttk.Treeview(
+            self, columns=("id", "state", "nodes"), show="headings"
+        )
         self.tree.grid(row=1, sticky="nsew")
         self.tree.column("id", stretch=tk.YES)
         self.tree.heading("id", text="ID")
@@ -64,20 +60,20 @@ class SessionsDialog(Dialog):
         self.tree.bind("<Double-1>", self.on_selected)
         self.tree.bind("<<TreeviewSelect>>", self.click_select)
 
-        yscrollbar = Scrollbar(self, orient="vertical", command=self.tree.yview)
+        yscrollbar = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
         yscrollbar.grid(row=1, column=1, sticky="ns")
         self.tree.configure(yscrollcommand=yscrollbar.set)
 
-        xscrollbar = Scrollbar(self, orient="horizontal", command=self.tree.xview)
+        xscrollbar = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview)
         xscrollbar.grid(row=2, sticky="ew", pady=5)
         self.tree.configure(xscrollcommand=xscrollbar.set)
 
     def draw_buttons(self):
-        frame = tk.Frame(self)
+        frame = ttk.Frame(self)
         for i in range(4):
             frame.columnconfigure(i, weight=1)
         frame.grid(row=3, sticky="ew")
-        b = tk.Button(
+        b = ttk.Button(
             frame,
             image=Images.get(ImageEnum.DOCUMENTNEW),
             text="New",
@@ -85,7 +81,7 @@ class SessionsDialog(Dialog):
             command=self.click_new,
         )
         b.grid(row=0, padx=2, sticky="ew")
-        b = tk.Button(
+        b = ttk.Button(
             frame,
             image=Images.get(ImageEnum.FILEOPEN),
             text="Connect",
@@ -93,7 +89,7 @@ class SessionsDialog(Dialog):
             command=self.click_connect,
         )
         b.grid(row=0, column=1, padx=2, sticky="ew")
-        b = tk.Button(
+        b = ttk.Button(
             frame,
             image=Images.get(ImageEnum.EDITDELETE),
             text="Shutdown",
@@ -101,7 +97,7 @@ class SessionsDialog(Dialog):
             command=self.click_shutdown,
         )
         b.grid(row=0, column=2, padx=2, sticky="ew")
-        b = tk.Button(frame, text="Cancel", command=self.click_new)
+        b = ttk.Button(frame, text="Cancel", command=self.click_new)
         b.grid(row=0, column=3, padx=2, sticky="ew")
 
     def click_new(self):
diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py
index 80360c44..e813ae48 100644
--- a/coretk/coretk/graph.py
+++ b/coretk/coretk/graph.py
@@ -150,7 +150,7 @@ class CanvasGraph(tk.Canvas):
                     node.type, node.model
                 )
                 n = CanvasNode(
-                    node.position.x, node.position.y, image, name, self, node.id
+                    node.position.x, node.position.y, image, name, self.master, node.id
                 )
                 self.nodes[n.id] = n
                 core_id_to_canvas_id[node.id] = n.id
@@ -419,14 +419,7 @@ class CanvasGraph(tk.Canvas):
         plot_id = self.find_all()[0]
         logging.info("add node event: %s - %s", plot_id, self.selected)
         if self.selected == plot_id:
-            node = CanvasNode(
-                x=x,
-                y=y,
-                image=image,
-                node_type=node_name,
-                canvas=self,
-                core_id=self.core.peek_id(),
-            )
+            node = CanvasNode(x, y, image, node_name, self.master, self.core.peek_id())
             self.nodes[node.id] = node
             self.core.add_graph_node(self.core.session_id, node.id, x, y, node_name)
             return node
@@ -491,10 +484,11 @@ class CanvasEdge:
 
 
 class CanvasNode:
-    def __init__(self, x, y, image, node_type, canvas, core_id):
+    def __init__(self, x, y, image, node_type, app, core_id):
         self.image = image
         self.node_type = node_type
-        self.canvas = canvas
+        self.app = app
+        self.canvas = app.canvas
         self.id = self.canvas.create_image(
             x, y, anchor=tk.CENTER, image=self.image, tags="node"
         )
@@ -506,19 +500,30 @@ class CanvasNode:
             x, y + 20, text=self.name, tags="nodename"
         )
         self.antenna_draw = WlanAntennaManager(self.canvas, self.id)
-
+        self.tooltip = CanvasTooltip(self.canvas)
         self.canvas.tag_bind(self.id, "<ButtonPress-1>", self.click_press)
         self.canvas.tag_bind(self.id, "<ButtonRelease-1>", self.click_release)
         self.canvas.tag_bind(self.id, "<B1-Motion>", self.motion)
         self.canvas.tag_bind(self.id, "<Button-3>", self.context)
         self.canvas.tag_bind(self.id, "<Double-Button-1>", self.double_click)
         self.canvas.tag_bind(self.id, "<Control-1>", self.select_multiple)
-        self.tooltip = CanvasTooltip(self.canvas, self.id, text=self.name)
+        self.canvas.tag_bind(self.id, "<Enter>", self.on_enter)
+        self.canvas.tag_bind(self.id, "<Leave>", self.on_leave)
 
         self.edges = set()
         self.wlans = []
         self.moving = None
 
+    def on_enter(self, event):
+        if self.app.core.is_runtime() and self.app.core.observer:
+            self.tooltip.text.set("waiting...")
+            self.tooltip.on_enter(event)
+            output = self.app.core.run(self.core_id)
+            self.tooltip.text.set(output)
+
+    def on_leave(self, event):
+        self.tooltip.on_leave(event)
+
     def click(self, event):
         print("click")
 
diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py
index 03e7fc01..726da5cc 100644
--- a/coretk/coretk/images.py
+++ b/coretk/coretk/images.py
@@ -4,7 +4,7 @@ from enum import Enum
 from PIL import Image, ImageTk
 
 from core.api.grpc import core_pb2
-from coretk.appdirs import LOCAL_ICONS_PATH
+from coretk.appconfig import LOCAL_ICONS_PATH
 
 
 class Images:
diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py
index 3b33377c..98b3d254 100644
--- a/coretk/coretk/menuaction.py
+++ b/coretk/coretk/menuaction.py
@@ -7,11 +7,12 @@ import webbrowser
 from tkinter import filedialog, messagebox
 
 from core.api.grpc import core_pb2
-from coretk.appdirs import XML_PATH
+from coretk.appconfig import XML_PATH
 from coretk.dialogs.canvasbackground import CanvasBackgroundDialog
 from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog
 from coretk.dialogs.hooks import HooksDialog
-from coretk.dialogs.observerwidgets import ObserverWidgetsDialog
+from coretk.dialogs.observers import ObserverDialog
+from coretk.dialogs.preferences import PreferencesDialog
 from coretk.dialogs.servers import ServersDialog
 from coretk.dialogs.sessionoptions import SessionOptionsDialog
 from coretk.dialogs.sessions import SessionsDialog
@@ -83,6 +84,10 @@ class MenuAction:
             self.prompt_save_running_session()
             self.app.core.open_xml(file_path)
 
+    def gui_preferences(self):
+        dialog = PreferencesDialog(self.app, self.app)
+        dialog.show()
+
     def canvas_size_and_scale(self):
         dialog = SizeAndScaleDialog(self.app, self.app)
         dialog.show()
@@ -118,5 +123,5 @@ class MenuAction:
         dialog.show()
 
     def edit_observer_widgets(self):
-        dialog = ObserverWidgetsDialog(self.app, self.app)
+        dialog = ObserverDialog(self.app, self.app)
         dialog.show()
diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py
index 3ef38d73..621510e2 100644
--- a/coretk/coretk/menubar.py
+++ b/coretk/coretk/menubar.py
@@ -1,6 +1,8 @@
 import tkinter as tk
+from functools import partial
 
 import coretk.menuaction as action
+from coretk.coreclient import OBSERVERS
 
 
 class Menubar(tk.Menu):
@@ -92,7 +94,9 @@ class Menubar(tk.Menu):
         menu.add_separator()
         menu.add_command(label="Find...", accelerator="Ctrl+F", state=tk.DISABLED)
         menu.add_command(label="Clear marker", state=tk.DISABLED)
-        menu.add_command(label="Preferences...", state=tk.DISABLED)
+        menu.add_command(
+            label="Preferences...", command=self.menuaction.gui_preferences
+        )
         self.add_cascade(label="Edit", menu=menu)
 
     def draw_canvas_menu(self):
@@ -364,23 +368,35 @@ class Menubar(tk.Menu):
         :param tkinter.Menu widget_menu: widget_menu
         :return: nothing
         """
+        var = tk.StringVar(value="none")
         menu = tk.Menu(widget_menu)
-        menu.add_command(label="None", state=tk.DISABLED)
-        menu.add_command(label="processes", state=tk.DISABLED)
-        menu.add_command(label="ifconfig", state=tk.DISABLED)
-        menu.add_command(label="IPv4 routes", state=tk.DISABLED)
-        menu.add_command(label="IPv6 routes", state=tk.DISABLED)
-        menu.add_command(label="OSPFv2 neighbors", state=tk.DISABLED)
-        menu.add_command(label="OSPFv3 neighbors", state=tk.DISABLED)
-        menu.add_command(label="Listening sockets", state=tk.DISABLED)
-        menu.add_command(label="IPv4 MFC entries", state=tk.DISABLED)
-        menu.add_command(label="IPv6 MFC entries", state=tk.DISABLED)
-        menu.add_command(label="firewall rules", state=tk.DISABLED)
-        menu.add_command(label="IPsec policies", state=tk.DISABLED)
-        menu.add_command(label="docker logs", state=tk.DISABLED)
-        menu.add_command(label="OSPFv3 MDR level", state=tk.DISABLED)
-        menu.add_command(label="PIM neighbors", state=tk.DISABLED)
-        menu.add_command(label="Edit...", command=self.menuaction.edit_observer_widgets)
+        menu.var = var
+        menu.add_command(
+            label="Edit Observers", command=self.menuaction.edit_observer_widgets
+        )
+        menu.add_separator()
+        menu.add_radiobutton(
+            label="None",
+            variable=var,
+            value="none",
+            command=lambda: self.app.core.set_observer(None),
+        )
+        for name in sorted(OBSERVERS):
+            cmd = OBSERVERS[name]
+            menu.add_radiobutton(
+                label=name,
+                variable=var,
+                value=name,
+                command=partial(self.app.core.set_observer, cmd),
+            )
+        for name in sorted(self.app.core.custom_observers):
+            observer = self.app.core.custom_observers[name]
+            menu.add_radiobutton(
+                label=name,
+                variable=var,
+                value=name,
+                command=partial(self.app.core.set_observer, observer.cmd),
+            )
         widget_menu.add_cascade(label="Observer Widgets", menu=menu)
 
     def create_adjacency_menu(self, widget_menu):
diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py
index 236dbc24..9ae9e851 100644
--- a/coretk/coretk/widgets.py
+++ b/coretk/coretk/widgets.py
@@ -26,7 +26,7 @@ class FrameScroll(tk.LabelFrame):
         self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2)
         self.canvas.columnconfigure(0, weight=1)
         self.canvas.rowconfigure(0, weight=1)
-        self.scrollbar = tk.Scrollbar(
+        self.scrollbar = ttk.Scrollbar(
             self, orient="vertical", command=self.canvas.yview
         )
         self.scrollbar.grid(row=0, column=1, sticky="ns")
@@ -70,11 +70,11 @@ class ConfigFrame(FrameScroll):
 
         for group_name in sorted(group_mapping):
             group = group_mapping[group_name]
-            frame = tk.Frame(self.frame)
+            frame = ttk.Frame(self.frame)
             frame.columnconfigure(1, weight=1)
             self.frame.add(frame, text=group_name)
             for index, option in enumerate(sorted(group, key=lambda x: x.name)):
-                label = tk.Label(frame, text=option.label)
+                label = ttk.Label(frame, text=option.label)
                 label.grid(row=index, pady=pady, padx=padx, sticky="w")
                 value = tk.StringVar()
                 if option.type == core_pb2.ConfigOptionType.BOOL:
@@ -96,15 +96,15 @@ class ConfigFrame(FrameScroll):
                     combobox.grid(row=index, column=1, sticky="ew", pady=pady)
                 elif option.type == core_pb2.ConfigOptionType.STRING:
                     value.set(option.value)
-                    entry = tk.Entry(frame, textvariable=value)
+                    entry = ttk.Entry(frame, textvariable=value)
                     entry.grid(row=index, column=1, sticky="ew", pady=pady)
                 elif option.type in INT_TYPES:
                     value.set(option.value)
-                    entry = tk.Entry(frame, textvariable=value)
+                    entry = ttk.Entry(frame, textvariable=value)
                     entry.grid(row=index, column=1, sticky="ew", pady=pady)
                 elif option.type == core_pb2.ConfigOptionType.FLOAT:
                     value.set(option.value)
-                    entry = tk.Entry(frame, textvariable=value)
+                    entry = ttk.Entry(frame, textvariable=value)
                     entry.grid(row=index, column=1, sticky="ew", pady=pady)
                 else:
                     logging.error("unhandled config option type: %s", option.type)
@@ -131,7 +131,7 @@ class ListboxScroll(tk.LabelFrame):
         super().__init__(master, cnf, **kw)
         self.columnconfigure(0, weight=1)
         self.rowconfigure(0, weight=1)
-        self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL)
+        self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
         self.scrollbar.grid(row=0, column=1, sticky="ns")
         self.listbox = tk.Listbox(
             self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set
@@ -149,5 +149,5 @@ class CheckboxList(FrameScroll):
     def add(self, name, checked):
         var = tk.BooleanVar(value=checked)
         func = partial(self.clicked, name, var)
-        checkbox = tk.Checkbutton(self.frame, text=name, variable=var, command=func)
+        checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func)
         checkbox.grid(sticky="w")
diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py
index c00cc3af..8c987371 100644
--- a/daemon/core/api/grpc/server.py
+++ b/daemon/core/api/grpc/server.py
@@ -184,9 +184,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
         logging.debug("stop session: %s", request)
         session = self.get_session(request.session_id, context)
         session.data_collect()
-        session.set_state(EventTypes.DATACOLLECT_STATE)
+        session.set_state(EventTypes.DATACOLLECT_STATE, send_event=True)
         session.clear()
-        session.set_state(EventTypes.SHUTDOWN_STATE)
+        session.set_state(EventTypes.SHUTDOWN_STATE, send_event=True)
         return core_pb2.StopSessionResponse(result=True)
 
     def CreateSession(self, request, context):