diff --git a/ledpatterns.py b/ledpatterns.py
index 0fab6de2ea06311dcff5a0370c4eab4e9ac14932..4874c799fd9f05f636f38424c12416e54f2b9331 100644
--- a/ledpatterns.py
+++ b/ledpatterns.py
@@ -1,5 +1,5 @@
 from absled import ILedString
-from num_utils import color_hex_to_tuple
+from utils.num_utils import color_hex_to_tuple
 
 
 def wheel(pos):
diff --git a/main.py b/main.py
index a48ad245a4674aa1290d36ae708291e071be7746..d95f3c8e4522dcf70db9c5a8a1f55f3c65fc1c20 100644
--- a/main.py
+++ b/main.py
@@ -221,7 +221,12 @@ def load_pattern(pattern_name: str, leds: ILedString, config_override: dict=None
 
     return getattr(ledpatterns, pattern_name)(pattern_props, leds)
 
-def run_screen():
+
+##################
+# Loop Functions #
+##################
+
+def run_screen(start_widget: str):
     # Initialise Pygame and create a window
     pygame.init()
 
@@ -239,7 +244,7 @@ def run_screen():
     clock = pygame.time.Clock()
 
     # Init widgets
-    widget_to_load = PROP_AVAILABLE_WIDGETS[0]  # TODO change to load multiple widgets
+    widget_to_load = start_widget
 
     current_widget = load_widget(widget_to_load)
 
@@ -295,7 +300,7 @@ def run_screen():
     # Quit Pygame
     pygame.quit()
 
-def run_leds():
+def run_leds(start_pattern: str):
     # Load LEDs
     if PROP_LEDS_DRIVER == "dummy":
         from absled.dummy import DummyLedString
@@ -306,8 +311,10 @@ def run_leds():
     else:
         raise ValueError("Invalid LED driver")
 
-    current_pattern = load_pattern(PROP_AVAILABLE_PATTERNS[0], leds)
-    pattern_timer = module_config.get(PROP_AVAILABLE_PATTERNS[0], {}).get("tick_rate", -1)
+    pattern_name = start_pattern
+
+    current_pattern = load_pattern(pattern_name, leds)
+    pattern_timer = module_config.get(pattern_name, {}).get("tick_rate", -1)
     current_pattern.tick()
 
     while True:
@@ -339,19 +346,30 @@ def run_leds():
 if __name__ == "__main__":
     # Load configuration
     load_module_config()
+    load_sequences()
+
+    initial_widget = widget_sequence[0].get("type") if len(widget_sequence) > 0 else PROP_AVAILABLE_WIDGETS[0]
+    initial_pattern = pattern_sequence[0].get("type") if len(pattern_sequence) > 0 else PROP_AVAILABLE_PATTERNS[0]
 
     # Fork off pygame
-    pygame_thread = threading.Thread(target=run_screen, daemon=True)
+    pygame_thread = threading.Thread(
+        target=lambda: run_screen(initial_widget),
+        daemon=True
+    )
     pygame_thread.start()
 
     # Fork off flask server
     flask_thread = threading.Thread(
         target=lambda: app.run(debug=PROP_FLASK_DEBUG, host=PROP_FLASK_HOST, port=PROP_FLASK_PORT, use_reloader=False),
-        daemon=True)
+        daemon=True
+    )
     flask_thread.start()
 
     # Fork off LED thread
-    led_thread = threading.Thread(target=run_leds, daemon=True)
+    led_thread = threading.Thread(
+        target=lambda: run_leds(initial_pattern),
+        daemon=True
+    )
     led_thread.start()
 
     # Wait for the threads to finish
diff --git a/num_utils.py b/num_utils.py
deleted file mode 100644
index 229d995af414033f0ac26dd215ce1dffa0d32753..0000000000000000000000000000000000000000
--- a/num_utils.py
+++ /dev/null
@@ -1,47 +0,0 @@
-import os
-import re
-
-from config import *
-import socket
-
-def get_screen_centre():
-    return (PROP_SCREEN_WIDTH * PROP_WINDOW_SCALE) / 2, (PROP_SCREEN_HEIGHT * PROP_WINDOW_SCALE) / 2
-
-
-# noinspection PyTypeChecker
-def color_hex_to_tuple(hx: str) -> tuple[int, int, int]:
-    """
-    Convert a 6-digit hex colour string to a tuple of RGB values.
-    If a hash symbol is present, it is removed.
-    :param hx: the hex colour string
-    :return: the RGB tuple
-    """
-    hx = hx.removeprefix("#")
-    return tuple(int(hx[i:i + 2], 16) for i in (0, 2, 4))
-
-
-def get_ip():
-    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-    s.settimeout(0)
-    try:
-        # doesn't even have to be reachable
-        s.connect(('10.254.254.254', 1))
-        ip_addr = s.getsockname()[0]
-    except Exception:
-        ip_addr = '127.0.0.1'
-    finally:
-        s.close()
-    return ip_addr
-
-def get_font_path_or_default(pygame, font_name: str|None, bold=False, italic=False) -> str:
-    if font_name is not None:
-        if re.match(r".+\.ttf", font_name):
-            if os.path.exists(font_name):
-                font_path = font_name
-            else:
-                font_path = pygame.font.get_default_font()
-        else:
-            font_path = pygame.font.match_font(font_name, bold, italic)
-    else:
-        font_path = pygame.font.get_default_font()
-    return font_path
diff --git a/screenwidgets.py b/screenwidgets.py
index 1f74d6d20ccc86b2771b5ae990f7a38d240ff626..12884e02e916bfbdf56cc6c4905d17e16931f3c9 100644
--- a/screenwidgets.py
+++ b/screenwidgets.py
@@ -1,10 +1,10 @@
 import math
-import os.path
-import re
 from datetime import datetime
 
 from config import PROP_WINDOW_SCALE, PROP_SCREEN_WIDTH, PROP_SCREEN_HEIGHT
-from num_utils import get_screen_centre, get_ip, get_font_path_or_default
+from utils.num import get_screen_centre
+from utils.net import get_ip
+from utils.fonts import get_font_path_or_default
 
 
 class IWidgetMeta(type):
diff --git a/utils/fonts.py b/utils/fonts.py
new file mode 100644
index 0000000000000000000000000000000000000000..006d206366ed07c83ad9e5e180c6801d1608403e
--- /dev/null
+++ b/utils/fonts.py
@@ -0,0 +1,16 @@
+import os
+import re
+
+
+def get_font_path_or_default(pygame, font_name: str|None, bold=False, italic=False) -> str:
+    if font_name is not None:
+        if re.match(r".+\.ttf", font_name):
+            if os.path.exists(font_name):
+                font_path = font_name
+            else:
+                font_path = pygame.font.get_default_font()
+        else:
+            font_path = pygame.font.match_font(font_name, bold, italic)
+    else:
+        font_path = pygame.font.get_default_font()
+    return font_path
diff --git a/utils/net.py b/utils/net.py
new file mode 100644
index 0000000000000000000000000000000000000000..ffc3f0e986255800d9c029771bf8cda9708c9676
--- /dev/null
+++ b/utils/net.py
@@ -0,0 +1,15 @@
+import socket
+
+
+def get_ip():
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.settimeout(0)
+    try:
+        # doesn't even have to be reachable
+        s.connect(('10.254.254.254', 1))
+        ip_addr = s.getsockname()[0]
+    except Exception:
+        ip_addr = '127.0.0.1'
+    finally:
+        s.close()
+    return ip_addr
\ No newline at end of file
diff --git a/utils/num.py b/utils/num.py
new file mode 100644
index 0000000000000000000000000000000000000000..8eeacf48328601b48dc86b5ece9e68464356f32c
--- /dev/null
+++ b/utils/num.py
@@ -0,0 +1,17 @@
+from config import PROP_SCREEN_WIDTH, PROP_SCREEN_HEIGHT, PROP_WINDOW_SCALE
+
+
+def get_screen_centre():
+    return (PROP_SCREEN_WIDTH * PROP_WINDOW_SCALE) / 2, (PROP_SCREEN_HEIGHT * PROP_WINDOW_SCALE) / 2
+
+
+# noinspection PyTypeChecker
+def color_hex_to_tuple(hx: str) -> tuple[int, int, int]:
+    """
+    Convert a 6-digit hex colour string to a tuple of RGB values.
+    If a hash symbol is present, it is removed.
+    :param hx: the hex colour string
+    :return: the RGB tuple
+    """
+    hx = hx.removeprefix("#")
+    return tuple(int(hx[i:i + 2], 16) for i in (0, 2, 4))