Repository: hugsy/cemu (language: python)

Issue:
Fix link to Unicorn



Discussion / hints:

Fix patch:
diff --git a/.gitattributes b/.gitattributes
index f792e6f..a94b92f 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,3 @@
 *       text=auto eol=lf
 *.py    auto=text eol=lf
-*.png   -text
\ No newline at end of file
+*.png   -text
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..ab49e98
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,17 @@
+## Contributing to CEMU ##
+
+### License ###
+
+`CEMU` is placed under [license MIT](https://github.com/hugsy/cemu/LICENSE) which provides Open-Source access to the code and its use.
+
+By contributing to `CEMU` code through the _Pull Requests_ mechanism, you accept to release the code written by you under the said license.
+
+
+### Submitting a Patch ###
+
+1. Fork `CEMU` repository (requires GitHub account). Sending a patch from the `patch` or `git diff --patch` commands is not accepted.
+1. Write the changes in your local repo.
+    * Respect the coding style (same indentation format, explicit names as possible)
+    * Comment your code
+1. Submit a pull request **ALWAYS against `dev`** (never `main` - which is reserved for stable code)
+1. Contributors will review your patch. If it is approved, the change will be merged via the GitHub, and you will be seen as contributors. If it needs additional work, the repo owner will respond with useful comments.
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..2a00b2e
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,4 @@
+# These are supported funding model platforms
+
+github: [hugsy,]
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..1462c50
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,157 @@
+name: "Continuous Delivery"
+
+on:
+  workflow_dispatch:
+  push:
+
+env:
+  NAME: cemu
+  REPO: hugsy/cemu
+  DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+
+jobs:
+  build:
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [windows-latest, ubuntu-latest, macos-latest]
+        python-version: ['3.10', '3.11']
+    name: ${{ matrix.os }} / ${{ matrix.python-version }}
+    runs-on: ${{ matrix.os }}
+    outputs:
+      windows-latest-3-10: ${{ join(steps.*.outputs.windows-latest-3-10,'') }}
+      windows-latest-3-11: ${{ join(steps.*.outputs.windows-latest-3-11,'') }}
+      ubuntu-latest-3-10: ${{ join(steps.*.outputs.ubuntu-latest-3-10,'') }}
+      ubuntu-latest-3-11: ${{ join(steps.*.outputs.ubuntu-latest-3-11,'') }}
+      macos-latest-3-10: ${{ join(steps.*.outputs.macos-latest-3-10,'') }}
+      macos-latest-3-11: ${{ join(steps.*.outputs.macos-latest-3-11,'') }}
+
+    steps:
+    - uses: actions/checkout@v3
+
+    - uses: rlespinasse/github-slug-action@v4
+
+    - uses: actions/setup-python@v4
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: "Install Pre-requisite for Linux build"
+      if: matrix.os == 'ubuntu-latest'
+      shell: bash
+      run: |
+        sudo apt update
+        sudo apt upgrade -y
+        sudo apt install -y build-essential python3-dev python3-pip python3-wheel python3-setuptools
+
+    - name: "Install Pre-requisite for Windows build"
+      if: matrix.os == 'windows-latest'
+      shell: pwsh
+      run: |
+        echo todo
+
+    - name: Build artifact
+      shell: bash
+      run: |
+        mkdir build
+        mkdir build/bin
+        python --version
+        python -m pip --version
+        python -m pip install --upgrade pip setuptools wheel
+        python -m pip install --user --upgrade .
+
+    - name: "Post build actions for Windows"
+      if: matrix.os == 'windows-latest'
+      shell: pwsh
+      run: |
+        Copy-Item $env:APPDATA\Python\Python*\Scripts\cemu.exe build\bin\
+
+    - name: "Post build actions for Linux"
+      if: matrix.os == 'ubuntu-latest'
+      shell: bash
+      run: |
+        cp -v ~/.local/bin/cemu build/bin/
+
+    - name: "Post build actions for MacOS"
+      if: matrix.os == 'macos-latest'
+      shell: bash
+      run: |
+        cp -v ~/.local/bin/cemu build/bin/ || cp -v /Users/runner/Library/Python/${{ matrix.python-version }}/bin/cemu build/bin/
+
+    - name: "Run tests"
+      run: |
+        python -m pip install --user --upgrade -r tests/requirements.txt
+        python -m pytest tests/
+
+    - name: Publish artifact
+      id: publish_artifact
+      uses: actions/upload-artifact@v3
+      with:
+        name: ${{ env.NAME }}_${{ matrix.os }}_py${{ matrix.python-version }}_${{ env.GITHUB_REF_SLUG }}_${{ env.GITHUB_SHA_SHORT }}
+        path: build/
+
+    - name: Populate the successful output (Windows)
+      id: output_success_windows
+      if: ${{ matrix.os == 'windows-latest' &&  success() }}
+      shell: pwsh
+      run: |
+        $pyVersion = "${{ matrix.python-version }}" -replace "\.", "-"
+        echo "${{ matrix.os }}-$pyVersion=✅ ${{ matrix.os }} ${{ matrix.python-version }}" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
+
+    - name: Populate the successful output (Other)
+      id: output_success_other
+      if: ${{matrix.os != 'windows-latest' && success() }}
+      shell: bash
+      run: |
+        pyVersion="$(echo -n ${{ matrix.python-version }} | tr . -)"
+        echo "${{ matrix.os }}-${pyVersion}=✅ ${{ matrix.os }} ${{ matrix.python-version }}" >> $GITHUB_OUTPUT
+
+    - name: Populate the failure output (Windows)
+      id: output_failure_windows
+      if: ${{matrix.os == 'windows-latest' && failure() }}
+      shell: pwsh
+      run: |
+        $pyVersion = "${{ matrix.python-version }}" -replace "\.", "-"
+        echo "${{ matrix.os }}-${pyVersion}=❌ ${{ matrix.os }} ${{ matrix.python-version }}" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
+
+    - name: Populate the failure output (Other)
+      id: output_failure_other
+      if: ${{matrix.os != 'windows-latest' && failure() }}
+      shell: bash
+      run: |
+        pyVersion="$(echo -n ${{ matrix.python-version }} | tr . -)"
+        echo "${{ matrix.os }}-$pyVersion=❌ ${{ matrix.os }} ${{ matrix.python-version }}" >> $GITHUB_OUTPUT
+
+  notify:
+    runs-on: 'ubuntu-latest'
+    needs: build
+    steps:
+    - name: Inject slug/short variables
+      uses: rlespinasse/github-slug-action@v4
+
+    - name: Send Discord notification
+      env:
+        COMMIT_URL: "https://github.com/${{ env.REPO }}/commit/${{ env.GITHUB_SHA_SHORT }}"
+        RUN_URL: "https://github.com/${{ env.REPO }}/actions/runs/${{ github.run_id }}"
+        BRANCH_URL: "https://github.com/${{ env.REPO }}/tree/${{ env.GITHUB_REF_SLUG }}"
+        AUTHOR_URL: "https://github.com/${{ github.actor }}"
+      uses: sarisia/actions-status-discord@v1.10.0
+      with:
+        nodetail: true
+        title: 🚧 🚧 Summary of Build `${{ env.GITHUB_SHA_SHORT }}` for `${{ env.REPO }}` 🚧 🚧
+        description: |
+          [Job #${{ github.run_number }}](${{ env.RUN_URL }}): CI build `${{ env.GITHUB_SHA_SHORT }}` initiated by [${{ github.actor }}](${{ env.AUTHOR_URL }}):
+          ● Commit [${{ env.GITHUB_SHA_SHORT }}](${{ env.COMMIT_URL }})
+          ● Branch [`${{ env.GITHUB_REF_SLUG }}`](${{ env.BRANCH_URL }})
+          ● [Detail Page](${{ env.RUN_URL }})
+
+          ${{ needs.build.outputs.windows-latest-3-10 }}
+          ${{ needs.build.outputs.windows-latest-3-11 }}
+          ${{ needs.build.outputs.ubuntu-latest-3-10 }}
+          ${{ needs.build.outputs.ubuntu-latest-3-11 }}
+          ${{ needs.build.outputs.macos-latest-3-10 }}
+          ${{ needs.build.outputs.macos-latest-3-11 }}
+
+
+        color: 0x0000ff
+        username: ${{ github.actor }} via GithubBot
+        avatar_url: "https://github.com/hugsy/cemu/blob/dev/cemu/img/new_logo.png?raw=true"
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
deleted file mode 100644
index 9adc0e3..0000000
--- a/.github/workflows/cd.yml
+++ /dev/null
@@ -1,101 +0,0 @@
-name: "Continuous Delivery"
-on:
-  push:
-    branches:
-      - main
-      - dev
-
-env:
-  NAME: cemu
-  DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
-
-jobs:
-  build:
-    name: "Building cemu"
-    strategy:
-      fail-fast: true
-      matrix:
-        os: [windows-latest, ubuntu-latest, macos-latest]
-        python-version: [3.9, ]
-    runs-on: ${{ matrix.os }}
-
-    steps:
-    - uses: actions/checkout@v2
-
-    - uses: rlespinasse/github-slug-action@v3.x
-
-    - uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
-
-    - name: "Install Pre-requisite for Linux build"
-      if: startsWith(matrix.os, 'ubuntu')
-      shell: bash
-      run: |
-        sudo apt update
-        sudo apt upgrade -y
-        sudo apt install -y build-essential python3-dev python3-pip python3-wheel python3-setuptools
-
-    - name: "Install Pre-requisite for Windows build"
-      if: startsWith(matrix.os, 'windows')
-      shell: pwsh
-      run: |
-        echo todo
-
-    - name: Build artifact
-      shell: bash
-      run: |
-        mkdir build
-        mkdir build/bin
-        python --version
-        python -m pip --version
-        python -m pip install --upgrade pip setuptools wheel
-        python -m pip install --user --upgrade -r requirements.txt
-        python -m pip install --user --upgrade .
-
-    - name: "Post build actions for Windows"
-      if: startsWith(matrix.os, 'windows')
-      shell: pwsh
-      run: |
-        Copy-Item %APPDATA%\Python\Python*\Scripts\cemu.exe build\bin\
-
-    - name: "Post build actions for Linux/MacOS"
-      if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos')
-      shell: bash
-      run: |
-        cp -v ~/.local/bin/cemu build/bin/
-
-    - name: Publish artifact
-      id: publish_artifact
-      uses: actions/upload-artifact@v2
-      with:
-        name: ${{ env.NAME }}_${{ matrix.os }}_py${{ matrix.python }}_${{ env.GITHUB_REF_SLUG }}_${{ env.GITHUB_SHA_SHORT }}
-        path: build/
-
-    - name: Failure notification
-      if: failure()
-      uses: sarisia/actions-status-discord@v1
-      with:
-        nodetail: true
-        title: Failed to build
-        description: |
-          ```
-          ${{ failure.message }}
-          ```
-        color: 0xff0000
-        username: ${{ github.actor }} via GithubBot
-        avatar_url: ${{ github.actor.avatar_url }}
-
-    - name: Success notification
-      if: success()
-      uses: sarisia/actions-status-discord@v1
-      with:
-        nodetail: true
-        title: Successful Build
-        description: |
-          ```
-          ${{ success.message }}
-          ```
-        color: 0x00ff00
-        username: ${{ github.actor }} via GithubBot
-        avatar_url: ${{ github.actor.avatar_url }}
\ No newline at end of file
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..c1b2de8
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,72 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ "dev" ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ "dev" ]
+  schedule:
+    - cron: '16 1 * * 3'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'python' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v2
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        
+        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+        # queries: security-extended,security-and-quality
+
+        
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v2
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+    #   If the Autobuild fails above, remove it and uncomment the following three lines. 
+    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+    # - run: |
+    #   echo "Run, Build Application using script"
+    #   ./location_of_script_within_repo/buildscript.sh
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml
index eaff951..e764da6 100644
--- a/.github/workflows/notify.yml
+++ b/.github/workflows/notify.yml
@@ -1,5 +1,5 @@
 name: "Discord Notification"
-on: [push, pull_request, issues]
+on: [pull_request, issues]
 env:
   DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
 
@@ -7,7 +7,7 @@ jobs:
   notify:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..024670c
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,38 @@
+name: Publish CEmu to PyPI
+
+on:
+  workflow_dispatch:
+  push:
+
+jobs:
+  publish:
+    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+    runs-on: ubuntu-latest
+    environment:
+      name: hugsy
+      url: https://pypi.org/p/cemu
+    permissions:
+      id-token: write
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Setup
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.10"
+
+      - name: Requirements
+        run: |
+          python -m pip install --upgrade build wheel
+
+      - name: Build
+        run: |
+          python -m build
+
+      - name: Publish
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          password: ${{ secrets.PYPI_API_TOKEN }}
+          print-hash: true
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f0fd01d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+.benchmarks
+.pytest_cache
+.vscode
+.vs
+*.pyc
+*.egg-info
+__pycache__
+.ruff_cache
+build
+dist
diff --git a/README.md b/README.md
index 7a1b2be..fa0d194 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,29 @@
-# CEMU #
+<p align="center">
+  <img alt="Logo" src="cemu/img/new_logo.png" width=20%>
+</p>
 
-[![MIT](https://img.shields.io/packagist/l/doctrine/orm.svg?maxAge=2592000?style=plastic)](https://github.com/hugsy/cemu/blob/master/LICENSE) ![Python-Version](https://img.shields.io/pypi/pyversions/cemu.svg) [![PyPi-Version](https://img.shields.io/pypi/v/cemu.svg)](https://pypi.python.org/pypi/cemu) ![[Discord](https://discord.gg/qBn9MbG9vp)](https://img.shields.io/badge/Discord-CEmu-green)
+<h2 align="center">CEmu : The Cheap (Assembly) Emulator</h2>
 
-![cemu-linux](https://i.imgur.com/iHtWvTL.png)
+<p align="center">
+  <a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
+  <a href="https://github.com/hugsy/cemu/blob/master/LICENSE"><img alt="Licence MIT" src="https://img.shields.io/packagist/l/doctrine/orm.svg?maxAge=2592000?style=plastic"></a>
+  <img alt="Python-Version 3.10" src="https://img.shields.io/pypi/pyversions/cemu.svg">
+  <a href="https://pypi.org/project/cemu/"><img alt="CEmu on PyPI" src="https://img.shields.io/pypi/v/cemu.svg"></a>
+  <a href="https://discord.gg/qBn9MbG9vp"><img alt="CEmu on Discord" src="https://img.shields.io/badge/Discord-CEmu-green"></a>
+  <a href="https://pepy.tech/project/cemu"><img alt="Downloads" src="https://pepy.tech/badge/cemu"></a>
+</p>
 
 
-## Description ##
+## Description
 
+Writing assembly is fun. Assembly is the lowest language (humanly understandable) available to communicate with computers, and is crucial to understand the internal mechanisms of any machine. Unfortunately, setting up an environment to write, compile and run assembly for various architectures (x86, ARM, MIPS, SPARC) has always been painful. **CEmu** is an attempt to fix this by providing GUI, CLI and library that allows to write and test assembly quickly.
 
-Writing assembly is fun. Assembly is the lowest language (humanly understandable) available to communicate with computers, and is crucial to understand the internal mechanisms of any machine. Unfortunately, setting up an environment to write, compile and run assembly for various architectures (x86, ARM, MIPS, SPARC) has always been painful. **CEmu** is an attempt to fix this by providing a bundled GUI application that empowers users to write assembly and test it by compiling it to bytecode and executing it in an QEMU-based emulator.
+**CEmu** is built upon:
+ * [Keystone](https://github.com/keystone-engine/keystone) for compiling assembly to bytecode
+ * [Capstone](https://github.com/aquynh/capstone) for disassembling bytecode
+ * [Unicorn](https://github.com/unicorn-engine/unicorn/) for the emulation
 
-**CEmu** combines all the advantages of a basic assembly IDE, compilation and execution environment, by relying on the great libraries [Keystone](https://github.com/keystone-engine/keystone), [Unicorn](https://github.com/unicorn-engine/unicorn/) and [Capstone](https://github.com/aquynh/capstone) libraries in a Qt6 powered GUI.
+And the GUI is powered by in a Qt6.
 
 It allows to test binary samples, check your shellcodes or even simply learn how to write assembly code, all of this for the following architectures:
 
@@ -52,7 +65,44 @@ For 99% of cases, that's all you need to do. `cemu` will be installed in the ass
  * On Linux by default the executable will be found as `/usr/local/bin/cemu` if installed as root, `~/.local/bin/cemu` for non-root
  * On Windows, `%PYTHON_DIR%\Scripts\cemu.exe` if installed with privileges, `%APPDATA%\Python\Python310\Scripts\cemu.exe` if not
 
+## Usage
+
+### As a GUI
+
+After installing with `pip`, simply run the executable, or invoke via Python
+
+```bash
+python -m cemu
+```
+
+This should produce a GUI similar to this:
+
+![cemu-gui](https://i.imgur.com/iHtWvTL.png)
+
+### In the terminal
+
+```bash
+python -m cemu cli
+```
+
+From where you'll end up in a REPL, allowing you to build and run the emulation environment.
+
+![cemu-cli](https://i.imgur.com/SyRJmI5.png)
+
+### As a Library
+
+```bash
+$ python
+>>> import cemu
+```
+
+Then use the provided API to build and run your environment.
 
 ## Contribution ##
 
 `cemu` was created and maintained by myself, [`@_hugsy_`](https://twitter.com/_hugsy_), but kept fresh thanks to [all the contributors](https://github.com/hugsy/cemu/graphs/contributors).
+
+[ ![contributors-img](https://contrib.rocks/image?repo=hugsy/cemu) ](https://github.com/hugsy/cemu/graphs/contributors)
+
+If you just like the tool, consider dropping on Discord (or Twitter or other) a simple *"thanks"*, it is always very appreciated.
+And if you can, consider [sponsoring me](https://github.com/hugsy/sponsors) - it really helps dedicating time and resources to the projects!
diff --git a/cemu/__main__.py b/cemu/__main__.py
index e00395c..3fff75c 100644
--- a/cemu/__main__.py
+++ b/cemu/__main__.py
@@ -3,10 +3,22 @@
 import pathlib
 import sys
 
+import cemu.const
+import cemu.core
+import cemu.log
+
 
 def main():
-    from cemu.core import Cemu
-    Cemu(sys.argv)
+    if cemu.const.DEBUG:
+        cemu.log.register_sink(print)
+        cemu.log.dbg("Starting in Debug Mode")
+
+    if len(sys.argv) >= 2 and sys.argv[1] == "cli":
+        cemu.core.CemuCli(sys.argv[2:])
+        return
+
+    cemu.core.CemuGui(sys.argv)
+    return
 
 
 if __name__ == "__main__":
diff --git a/cemu/arch/__init__.py b/cemu/arch/__init__.py
index a05b37e..3f92d3c 100644
--- a/cemu/arch/__init__.py
+++ b/cemu/arch/__init__.py
@@ -1,9 +1,16 @@
 import enum
 import importlib
-from typing import Optional
+from typing import Optional, TYPE_CHECKING
+
+import capstone
+import keystone
+import unicorn
 
 from cemu.const import SYSCALLS_PATH
 
+if TYPE_CHECKING:
+    import cemu.core
+
 
 class Endianness(enum.Enum):
     LITTLE_ENDIAN = 1
@@ -35,6 +42,7 @@ def __int__(self) -> int:
 
 class Architecture:
     """Generic metaclass for the architectures."""
+
     name: str
     endianness: Endianness = Endianness.LITTLE_ENDIAN
     syntax: Syntax = Syntax.INTEL
@@ -51,42 +59,101 @@ def __repr__(self):
         return f"{self.name} (Ptrsize={self.ptrsize}, Endian={self.endianness}, Syntax={self.syntax})"
 
     def __str__(self):
-        return f"{self.name}"
+        return self.name
 
+    __context: Optional["cemu.core.GlobalContext"] = None
     __syscalls: Optional[dict[str, int]] = None
     syscall_base = 0
 
     @property
     def syscalls(self):
+        if not self.__context:
+            mod = __import__("cemu.core")
+            self.__context = getattr(mod, "context")
+            assert isinstance(self.__context, cemu.core.GlobalContext)
+
         if not self.__syscalls:
-            fpath = SYSCALLS_PATH / (self.syscall_filename + ".csv")
+            syscall_dir = SYSCALLS_PATH / cemu.core.context.os
+            fpath = syscall_dir / (self.syscall_filename + ".csv")
             self.__syscalls = {}
 
-            with fpath.open('r') as fd:
+            with fpath.open("r") as fd:
                 for row in fd.readlines():
-                    row = [x.strip() for x in row.strip().split(',')]
+                    row = [x.strip() for x in row.strip().split(",")]
                     syscall_number = int(row[0])
                     syscall_name = row[1].lower()
-                    self.__syscalls[syscall_name] = self.syscall_base + \
-                        syscall_number
+                    self.__syscalls[syscall_name] = self.syscall_base + syscall_number
 
         return self.__syscalls
 
     def __eq__(self, x):
         if not isinstance(x, Architecture):
             return False
-        return self.name == x.name and self.endianness == x.endianness and self.syntax == x.syntax
+        return (
+            self.name == x.name
+            and self.endianness == x.endianness
+            and self.syntax == x.syntax
+        )
 
+    def unicorn(self) -> tuple[int, int, int]:
+        """Returns a tuple with the values of unicorn architecture, mode, endianess
 
-class ArchitectureManager(dict[str, list[Architecture]]):
+        Raises:
+            KeyError: _description_
+
+        Returns:
+            tuple[int, int, int]: _description_
+        """
+        raise NotImplementedError
+
+    @property
+    def uc(self) -> "unicorn.Uc":
+        arch, mode, endian = self.unicorn()
+        return unicorn.Uc(arch, mode | endian)
+
+    def uc_register(self, name: str) -> int:
+        raise NotImplementedError
+
+    def capstone(self) -> tuple[int, int, int]:
+        """Returns a tuple with the values of unicorn architecture, mode, endianess
+
+        Raises:
+            KeyError: _description_
+
+        Returns:
+            tuple[int, int, int]: _description_
+        """
+        raise NotImplementedError
 
+    @property
+    def cs(self) -> "capstone.Cs":
+        cs_arch, cs_mode, cs_endian = self.capstone()
+        return capstone.Cs(cs_arch, cs_mode | cs_endian)
+
+    def keystone(self) -> tuple[int, int, int]:
+        """Returns a tuple with the values of unicorn architecture, mode, endianess
+
+        Raises:
+            KeyError: _description_
+
+        Returns:
+            tuple[int, int, int]: _description_
+        """
+        raise NotImplementedError
+
+    @property
+    def ks(self) -> "keystone.Ks":
+        ks_arch, ks_mode, ks_endian = self.keystone()
+        return keystone.Ks(ks_arch, ks_mode | ks_endian)
+
+
+class ArchitectureManager(dict[str, list[Architecture]]):
     def __init__(self):
         super().__init__()
         self.load(True)
 
     def load(self, force_reload: bool = False) -> None:
-        """Instanciate all the architecture objects
-        """
+        """Instanciate all the architecture objects"""
         if force_reload:
             generic = importlib.import_module(".generic", package="cemu.arch")
             x86 = importlib.import_module(".x86", package="cemu.arch")
@@ -94,12 +161,24 @@ def load(self, force_reload: bool = False) -> None:
             mips = importlib.import_module(".mips", package="cemu.arch")
             sparc = importlib.import_module(".sparc", package="cemu.arch")
 
-            self["generic"] = [generic.Generic(), ]
-            self["x86"] = [x86.X86(), x86.X86_32(), x86.X86_64(), x86.X86(
-                syntax=Syntax.ATT), x86.X86_32(syntax=Syntax.ATT), x86.X86_64(syntax=Syntax.ATT)]
+            self["generic"] = [
+                generic.Generic(),
+            ]
+            self["x86"] = [
+                x86.X86(),
+                x86.X86_32(),
+                x86.X86_64(),
+                x86.X86(syntax=Syntax.ATT),
+                x86.X86_32(syntax=Syntax.ATT),
+                x86.X86_64(syntax=Syntax.ATT),
+            ]
             self["arm"] = [arm.ARM(), arm.ARM(thumb=True), arm.AARCH64()]
-            self["mips"] = [mips.MIPS(), mips.MIPS(endian=Endianness.BIG_ENDIAN),
-                            mips.MIPS64(), mips.MIPS64(endian=Endianness.BIG_ENDIAN)]
+            self["mips"] = [
+                mips.MIPS(),
+                mips.MIPS(endian=Endianness.BIG_ENDIAN),
+                mips.MIPS64(),
+                mips.MIPS64(endian=Endianness.BIG_ENDIAN),
+            ]
             self["sparc"] = [sparc.SPARC(), sparc.SPARC64()]
         return
 
@@ -124,17 +203,23 @@ def find(self, name: str) -> Architecture:
 
 Architectures = ArchitectureManager()
 
+from .x86 import X86, X86_32, X86_64  # noqa: E402
+from .arm import ARM, AARCH64  # noqa: E402
+from .mips import MIPS, MIPS64  # noqa: E402
+from .sparc import SPARC, SPARC64  # noqa: E402
+from .ppc import PowerPC  # noqa: E402
+
 
 def is_x86_16(a: Architecture):
-    return a.name == "Intel 8086 16bit"
+    return isinstance(a, X86)
 
 
 def is_x86_32(a: Architecture):
-    return a.name == "Intel i386 32bit"
+    return isinstance(a, X86_32)
 
 
 def is_x86_64(a: Architecture):
-    return a.name == "Intel i386 64bit"
+    return isinstance(a, X86_64)
 
 
 def is_x86(a: Architecture):
@@ -142,32 +227,36 @@ def is_x86(a: Architecture):
 
 
 def is_arm(a: Architecture):
-    return a.name in ("ARM THUMB mode", "ARM Native mode")
+    return isinstance(a, ARM)
+
+
+def is_arm_native(a: Architecture):
+    return isinstance(a, ARM) and a.thumb is False  # type: ignore
 
 
 def is_arm_thumb(a: Architecture):
-    return a.name == "ARM THUMB mode"
+    return isinstance(a, ARM) and a.thumb is True  # type: ignore
 
 
 def is_aarch64(a: Architecture):
-    return a.name == "ARM AARCH64"
+    return isinstance(a, AARCH64)
 
 
 def is_mips(a: Architecture):
-    return a.name == "MIPS 32bits"
+    return isinstance(a, MIPS)
 
 
 def is_mips64(a: Architecture):
-    return a.name == "MIPS 64bits"
+    return isinstance(a, MIPS64)
 
 
 def is_sparc(a: Architecture):
-    return a.name == "SPARC 32bits"
+    return isinstance(a, SPARC)
 
 
 def is_sparc64(a: Architecture):
-    return a.name == "SPARC 64bits"
+    return isinstance(a, SPARC64)
 
 
 def is_ppc(a: Architecture):
-    return a.name == "PowerPC 32bits"
+    return isinstance(a, PowerPC)
diff --git a/cemu/arch/arm.py b/cemu/arch/arm.py
index 9d71809..9fa7a4a 100644
--- a/cemu/arch/arm.py
+++ b/cemu/arch/arm.py
@@ -1,3 +1,7 @@
+import capstone
+import keystone
+import unicorn
+
 from cemu.arch import Architecture, Endianness
 
 
@@ -7,8 +11,20 @@ class ARM(Architecture):
     sp = "SP"
     flag = "CPSR"
     registers = [
-        'R0', 'R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9', 'R10', 'R12',  # GPR
-        "FP", "LR",
+        "R0",
+        "R1",
+        "R2",
+        "R3",
+        "R4",
+        "R5",
+        "R6",
+        "R7",
+        "R8",
+        "R9",
+        "R10",
+        "R12",  # GPR
+        "FP",
+        "LR",
         flag,
         pc,
         sp,
@@ -38,6 +54,48 @@ def name(self):
             return "ARM THUMB mode"
         return "ARM Native mode"
 
+    def keystone(self) -> tuple[int, int, int]:
+        if self.thumb:
+            return (
+                keystone.KS_ARCH_ARM,
+                keystone.KS_MODE_THUMB,
+                keystone.KS_MODE_LITTLE_ENDIAN,
+            )
+        return (
+            keystone.KS_ARCH_ARM,
+            keystone.KS_MODE_ARM,
+            keystone.KS_MODE_LITTLE_ENDIAN,
+        )
+
+    def capstone(self) -> tuple[int, int, int]:
+        if self.thumb:
+            return (
+                capstone.CS_ARCH_ARM,
+                capstone.CS_MODE_THUMB,
+                capstone.CS_MODE_LITTLE_ENDIAN,
+            )
+        return (
+            capstone.CS_ARCH_ARM,
+            capstone.CS_MODE_ARM,
+            capstone.CS_MODE_LITTLE_ENDIAN,
+        )
+
+    def unicorn(self) -> tuple[int, int, int]:
+        if self.thumb:
+            return (
+                unicorn.UC_ARCH_ARM,
+                unicorn.UC_MODE_THUMB,
+                unicorn.UC_MODE_LITTLE_ENDIAN,
+            )
+        return (
+            unicorn.UC_ARCH_ARM,
+            unicorn.UC_MODE_ARM,
+            unicorn.UC_MODE_LITTLE_ENDIAN,
+        )
+
+    def uc_register(self, name: str) -> int:
+        return getattr(unicorn.arm_const, f"UC_ARM_REG_{name.upper()}")
+
 
 class AARCH64(Architecture):
     # http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0801a/BABIBIGB.html
@@ -48,13 +106,61 @@ class AARCH64(Architecture):
     flag = "NZCV"
     endianness = Endianness.LITTLE_ENDIAN
     registers = [
-        'X0', 'X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7',
-        'X8', 'X9', 'X10', 'X11', 'X12', 'X13', 'X14', 'X15',
-        'X16', 'X17', 'X18', 'X19', 'X20', 'X21', 'X22', 'X23',
-        'X24', 'X25', 'X26', 'X27', 'X28', 'X29', 'X30',
+        "X0",
+        "X1",
+        "X2",
+        "X3",
+        "X4",
+        "X5",
+        "X6",
+        "X7",
+        "X8",
+        "X9",
+        "X10",
+        "X11",
+        "X12",
+        "X13",
+        "X14",
+        "X15",
+        "X16",
+        "X17",
+        "X18",
+        "X19",
+        "X20",
+        "X21",
+        "X22",
+        "X23",
+        "X24",
+        "X25",
+        "X26",
+        "X27",
+        "X28",
+        "X29",
+        "X30",
         sp,
         flag,
-        pc
+        pc,
     ]
     syscall_filename = "aarch64"
     ptrsize = 8
+    ptrsize = 8
+
+    def keystone(self) -> tuple[int, int, int]:
+        return (keystone.KS_ARCH_ARM64, 0, keystone.KS_MODE_LITTLE_ENDIAN)
+
+    def capstone(self) -> tuple[int, int, int]:
+        return (
+            capstone.CS_ARCH_ARM64,
+            capstone.CS_MODE_ARM,
+            capstone.CS_MODE_LITTLE_ENDIAN,
+        )
+
+    def unicorn(self) -> tuple[int, int, int]:
+        return (
+            unicorn.UC_ARCH_ARM64,
+            unicorn.UC_MODE_ARM,
+            unicorn.UC_MODE_LITTLE_ENDIAN,
+        )
+
+    def uc_register(self, name: str) -> int:
+        return getattr(unicorn.arm64_const, f"UC_ARM64_REG_{name.upper()}")
diff --git a/cemu/arch/mips.py b/cemu/arch/mips.py
index d95b590..aa4b9ec 100644
--- a/cemu/arch/mips.py
+++ b/cemu/arch/mips.py
@@ -1,3 +1,7 @@
+import capstone
+import keystone
+import unicorn
+
 from cemu.arch import Architecture, Endianness
 
 
@@ -7,11 +11,39 @@ class MIPS(Architecture):
     sp: str = "SP"
     flag = None
     registers: list[str] = [
-        "ZERO", "AT", "V0", "V1",
-        'A0', 'A1', 'A2', 'A3',
-        'S0', 'S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S8',
-        'T0', 'T1', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7', 'T8', 'T9',
-        "K0", "K1", "HI", "LO", "GP", "RA",
+        "ZERO",
+        "AT",
+        "V0",
+        "V1",
+        "A0",
+        "A1",
+        "A2",
+        "A3",
+        "S0",
+        "S1",
+        "S2",
+        "S3",
+        "S4",
+        "S5",
+        "S6",
+        "S7",
+        "S8",
+        "T0",
+        "T1",
+        "T2",
+        "T3",
+        "T4",
+        "T5",
+        "T6",
+        "T7",
+        "T8",
+        "T9",
+        "K0",
+        "K1",
+        "HI",
+        "LO",
+        "GP",
+        "RA",
         sp,
         pc,
     ]
@@ -22,7 +54,65 @@ def __init__(self, *args, **kwargs):
         self.endianness = kwargs.get("endian", Endianness.LITTLE_ENDIAN)
         return
 
+    def keystone(self) -> tuple[int, int, int]:
+        return (
+            keystone.KS_ARCH_MIPS,
+            keystone.KS_MODE_MIPS32,
+            keystone.KS_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else keystone.KS_MODE_BIG_ENDIAN,
+        )
+
+    def capstone(self) -> tuple[int, int, int]:
+        return (
+            capstone.CS_ARCH_MIPS,
+            capstone.CS_MODE_MIPS32,
+            capstone.CS_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else capstone.CS_MODE_BIG_ENDIAN,
+        )
+
+    def unicorn(self) -> tuple[int, int, int]:
+        return (
+            unicorn.UC_ARCH_MIPS,
+            unicorn.UC_MODE_MIPS32,
+            unicorn.UC_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else unicorn.UC_MODE_BIG_ENDIAN,
+        )
+
+    def uc_register(self, name: str) -> int:
+        return getattr(unicorn.mips_const, f"UC_MIPS_REG_{name.upper()}")
+
 
 class MIPS64(MIPS):
     ptrsize = 8
     name = "MIPS 64bits"
+    name = "MIPS 64bits"
+
+    def keystone(self) -> tuple[int, int, int]:
+        return (
+            keystone.KS_ARCH_MIPS,
+            keystone.KS_MODE_MIPS64,
+            keystone.KS_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else keystone.KS_MODE_BIG_ENDIAN,
+        )
+
+    def capstone(self) -> tuple[int, int, int]:
+        return (
+            capstone.CS_ARCH_MIPS,
+            capstone.CS_MODE_MIPS64,
+            capstone.CS_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else capstone.CS_MODE_BIG_ENDIAN,
+        )
+
+    def unicorn(self) -> tuple[int, int, int]:
+        return (
+            unicorn.UC_ARCH_MIPS,
+            unicorn.UC_MODE_MIPS64,
+            unicorn.UC_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else unicorn.UC_MODE_BIG_ENDIAN,
+        )
diff --git a/cemu/arch/ppc.py b/cemu/arch/ppc.py
index a749029..ad11280 100644
--- a/cemu/arch/ppc.py
+++ b/cemu/arch/ppc.py
@@ -1,9 +1,9 @@
 from typing import Optional
 
-from cemu.arch import Architecture
+import capstone
+import keystone
 
-# PPC is currently unsupported by unicorn:
-# see https://github.com/unicorn-engine/unicorn/blob/master/include/unicorn/unicorn.h#L94
+from cemu.arch import Architecture
 
 
 class PowerPC(Architecture):
@@ -12,11 +12,58 @@ class PowerPC(Architecture):
     sp: str = "SP"
     flag: Optional[str] = None
     registers: list[str] = [
-        'R0', 'R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7',
-        'R8', 'R9', 'R10', 'R11', 'R12', 'R13', 'R14', 'R15',
-        'R16', 'R17', 'R18', 'R19', 'R20', 'R21', 'R22', 'R23',
-        'R24', 'R25', 'R26', 'R27', 'R28', 'R29', 'R30', 'R31',
+        "R0",
+        "R1",
+        "R2",
+        "R3",
+        "R4",
+        "R5",
+        "R6",
+        "R7",
+        "R8",
+        "R9",
+        "R10",
+        "R11",
+        "R12",
+        "R13",
+        "R14",
+        "R15",
+        "R16",
+        "R17",
+        "R18",
+        "R19",
+        "R20",
+        "R21",
+        "R22",
+        "R23",
+        "R24",
+        "R25",
+        "R26",
+        "R27",
+        "R28",
+        "R29",
+        "R30",
+        "R31",
         pc,
     ]
     syscall_filename: str = "ppc"
     ptrsize: int = 4
+
+    def keystone(self) -> tuple[int, int, int]:
+        return (
+            keystone.KS_ARCH_PPC,
+            keystone.KS_MODE_PPC32,
+            keystone.KS_MODE_LITTLE_ENDIAN,
+        )
+
+    def capstone(self) -> tuple[int, int, int]:
+        return (
+            capstone.CS_ARCH_PPC,
+            capstone.CS_MODE_32,
+            capstone.CS_MODE_LITTLE_ENDIAN,
+        )
+
+    def unicorn(self) -> tuple[int, int, int]:
+        # PPC is currently unsupported by unicorn:
+        # see https://github.com/unicorn-engine/unicorn/blob/master/include/unicorn/unicorn.h#L94
+        raise NotImplementedError
diff --git a/cemu/arch/sparc.py b/cemu/arch/sparc.py
index 62f2e9a..e9b81e2 100644
--- a/cemu/arch/sparc.py
+++ b/cemu/arch/sparc.py
@@ -1,4 +1,8 @@
-from cemu.arch import Architecture, Endianness, Syntax
+import capstone
+import keystone
+import unicorn
+
+from cemu.arch import Architecture, Endianness
 
 
 class SPARC(Architecture):
@@ -7,18 +11,104 @@ class SPARC(Architecture):
     sp: str = "SP"
     flag: str = "ICC"
     registers: list[str] = [
-        'G0', 'G1', 'G2', 'G3', 'G4', 'G5', 'G6', 'G7',
-        'L0', 'L1', 'L2', 'L3', 'L4', 'L5', 'L6', 'L7',
-        'I0', 'I1', 'I2', 'I3', 'I4', 'I5', 'I6', 'I7',
-        'O0', 'O1', 'O2', 'O3', 'O4', 'O5', 'O6', 'O7',
+        "G0",
+        "G1",
+        "G2",
+        "G3",
+        "G4",
+        "G5",
+        "G6",
+        "G7",
+        "L0",
+        "L1",
+        "L2",
+        "L3",
+        "L4",
+        "L5",
+        "L6",
+        "L7",
+        "I0",
+        "I1",
+        "I2",
+        "I3",
+        "I4",
+        "I5",
+        "I6",
+        "I7",
+        "O0",
+        "O1",
+        "O2",
+        "O3",
+        "O4",
+        "O5",
+        "O6",
+        "O7",
         flag,
         pc,
+        sp,
     ]
     syscall_filename: str = "sparc"
     ptrsize: int = 4
-    endianness = Endianness.LITTLE_ENDIAN
+    endianness = Endianness.BIG_ENDIAN
+
+    def keystone(self) -> tuple[int, int, int]:
+        return (
+            keystone.KS_ARCH_SPARC,
+            keystone.KS_MODE_SPARC32,
+            keystone.KS_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else keystone.KS_MODE_BIG_ENDIAN,
+        )
+
+    def capstone(self) -> tuple[int, int, int]:
+        return (
+            capstone.CS_ARCH_SPARC,
+            0,
+            capstone.CS_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else capstone.CS_MODE_BIG_ENDIAN,
+        )
+
+    def unicorn(self) -> tuple[int, int, int]:
+        return (
+            unicorn.UC_ARCH_SPARC,
+            unicorn.UC_MODE_SPARC32,
+            unicorn.UC_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else unicorn.UC_MODE_BIG_ENDIAN,
+        )
+
+    def uc_register(self, name: str) -> int:
+        return getattr(unicorn.sparc_const, f"UC_SPARC_REG_{name.upper()}")
 
 
 class SPARC64(SPARC):
     ptrsize = 8
     name = "SPARC 64bits"
+
+    def keystone(self) -> tuple[int, int, int]:
+        return (
+            keystone.KS_ARCH_SPARC,
+            keystone.KS_MODE_SPARC64,
+            keystone.KS_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else keystone.KS_MODE_BIG_ENDIAN,
+        )
+
+    def capstone(self) -> tuple[int, int, int]:
+        return (
+            capstone.CS_ARCH_SPARC,
+            capstone.CS_MODE_64,
+            capstone.CS_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else capstone.CS_MODE_BIG_ENDIAN,
+        )
+
+    def unicorn(self) -> tuple[int, int, int]:
+        return (
+            unicorn.UC_ARCH_SPARC,
+            unicorn.UC_MODE_SPARC64,
+            unicorn.UC_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else unicorn.UC_MODE_BIG_ENDIAN,
+        )
diff --git a/cemu/arch/x86.py b/cemu/arch/x86.py
index 6a76cb6..8d1bb73 100644
--- a/cemu/arch/x86.py
+++ b/cemu/arch/x86.py
@@ -1,3 +1,10 @@
+from dataclasses import dataclass
+from enum import IntFlag
+
+import capstone
+import keystone
+import unicorn
+
 from cemu.arch import Architecture, Endianness, Syntax
 
 
@@ -6,13 +13,18 @@ class X86(Architecture):
     pc: str = "IP"
     sp: str = "SP"
     flag: str = "EFLAGS"
-    pgr_registers = ["CS", "DS", "ES", "FS", "GS", "SS", ]
     registers: list[str] = [
-        "AX", "BX", "CX", "DX", "SI", "DI", "BP",                 # GPR
         pc,
         sp,
+        "AX",
+        "BX",
+        "CX",
+        "DX",
+        "SI",
+        "DI",
+        "BP",
         flag,
-    ] + pgr_registers
+    ]
     ptrsize: int = 2
     syscall_filename: str = "x86"
 
@@ -21,30 +33,151 @@ def __init__(self, *args, **kwargs):
         self.syntax = kwargs.get("syntax", Syntax.INTEL)
         return
 
+    def unicorn(self) -> tuple[int, int, int]:
+        return (
+            unicorn.UC_ARCH_X86,
+            unicorn.UC_MODE_16,
+            unicorn.UC_MODE_LITTLE_ENDIAN,
+        )
+
+    def capstone(self) -> tuple[int, int, int]:
+        return (
+            capstone.CS_ARCH_X86,
+            capstone.CS_MODE_16,
+            capstone.CS_MODE_LITTLE_ENDIAN,
+        )
+
+    def keystone(self) -> tuple[int, int, int]:
+        return (
+            keystone.KS_ARCH_X86,
+            keystone.KS_MODE_16,
+            keystone.KS_MODE_LITTLE_ENDIAN,
+        )
+
+    def uc_register(self, name: str) -> int:
+        return getattr(unicorn.x86_const, f"UC_X86_REG_{name.upper()}")
+
 
 class X86_32(X86):
+    # See SDM VOL3a
+    class SegmentType(IntFlag):
+        Read = 0
+        Data = 0
+        Execute = 8
+        Code = 8
+        Accessed = 1
+        ReadWrite = 2
+        ExpandDown = 4
+
+    @dataclass
+    class SegmentDescriptor:
+        base: int
+        type: "X86_32.SegmentType"
+        S: bool
+        DPL: int
+        P: bool
+
+        def __int__(self) -> int:
+            return (
+                (self.base)
+                | (self.type << 8)
+                | (int(self.S) << 12)
+                | (self.DPL << 13)
+                | (int(self.P) << 15)
+            )
+
     name = "Intel i386 32bit"
     pc = "EIP"
     sp = "ESP"
+    selector_registers = [
+        "CS",
+        "DS",
+        "ES",
+        "FS",
+        "GS",
+        "SS",
+    ]
     registers = [
-        "EAX", "EBX", "ECX", "EDX", "ESI", "EDI", "EBP",                 # GPR
         pc,
         sp,
+        "EAX",
+        "EBX",
+        "ECX",
+        "EDX",
+        "ESI",
+        "EDI",
+        "EBP",  # GPR
         X86.flag,
-    ] + X86.pgr_registers
+    ] + selector_registers
     ptrsize = 4
 
+    def unicorn(self) -> tuple[int, int, int]:
+        return (
+            unicorn.UC_ARCH_X86,
+            unicorn.UC_MODE_32,
+            unicorn.UC_MODE_LITTLE_ENDIAN,
+        )
+
+    def capstone(self) -> tuple[int, int, int]:
+        return (
+            capstone.CS_ARCH_X86,
+            capstone.CS_MODE_32,
+            capstone.CS_MODE_LITTLE_ENDIAN,
+        )
+
+    def keystone(self) -> tuple[int, int, int]:
+        return (
+            keystone.KS_ARCH_X86,
+            keystone.KS_MODE_32,
+            keystone.KS_MODE_LITTLE_ENDIAN,
+        )
+
 
 class X86_64(X86_32):
     name = "Intel i386 64bit"
     pc = "RIP"
     sp = "RSP"
     registers = [
-        "RAX", "RBX", "RCX", "RDX", "RSI", "RDI", "RBP",                 # GPR
-        "R8", "R9", "R10", "R11", "R12", "R13", "R14", "R14", "R15",
         pc,
         sp,
+        "RAX",
+        "RBX",
+        "RCX",
+        "RDX",
+        "RSI",
+        "RDI",
+        "RBP",
+        "R8",
+        "R9",
+        "R10",
+        "R11",
+        "R12",
+        "R13",
+        "R14",
+        "R15",
         X86_32.flag,
-    ] + X86_32.pgr_registers
+    ] + X86_32.selector_registers
     ptrsize = 8
     syscall_filename = "x86-64"
+    syscall_filename = "x86-64"
+
+    def unicorn(self) -> tuple[int, int, int]:
+        return (
+            unicorn.UC_ARCH_X86,
+            unicorn.UC_MODE_64,
+            unicorn.UC_MODE_LITTLE_ENDIAN,
+        )
+
+    def capstone(self) -> tuple[int, int, int]:
+        return (
+            capstone.CS_ARCH_X86,
+            capstone.CS_MODE_64,
+            capstone.CS_MODE_LITTLE_ENDIAN,
+        )
+
+    def keystone(self) -> tuple[int, int, int]:
+        return (
+            keystone.KS_ARCH_X86,
+            keystone.KS_MODE_64,
+            keystone.KS_MODE_LITTLE_ENDIAN,
+        )
diff --git a/cemu/cli/__init__.py b/cemu/cli/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/cemu/cli/repl.py b/cemu/cli/repl.py
new file mode 100644
index 0000000..fadf805
--- /dev/null
+++ b/cemu/cli/repl.py
@@ -0,0 +1,331 @@
+import os
+import pathlib
+import subprocess
+import tempfile
+
+import unicorn
+from prompt_toolkit import prompt
+from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
+from prompt_toolkit.completion import NestedCompleter
+from prompt_toolkit.history import FileHistory
+from prompt_toolkit.key_binding import KeyBindings
+
+import cemu
+import cemu.arch
+import cemu.const
+import cemu.core
+import cemu.memory
+from cemu.emulator import EmulatorState
+from cemu.log import dbg, error, info, ok, warn
+from cemu.utils import hexdump
+
+bindings = KeyBindings()
+
+TEXT_EDITOR = os.getenv("EDITOR") or "nano -c"
+
+MEMORY_MAP_DEFAULT_LAYOUT: list[cemu.memory.MemorySection] = [
+    cemu.memory.MemorySection(".text", 0x00004000, 0x1000, "READ|EXEC"),
+    cemu.memory.MemorySection(".data", 0x00005000, 0x1000, "READ|WRITE"),
+    cemu.memory.MemorySection(".stack", 0x00006000, 0x4000, "READ|WRITE"),
+    cemu.memory.MemorySection(".misc", 0x0000A000, 0x1000, "READ|WRITE|EXEC"),
+]
+
+
+@bindings.add("c-c")
+def _(event):
+    if cemu.core.context.emulator.is_running:
+        cemu.core.context.emulator.set(EmulatorState.FINISHED)
+    pass
+
+
+class CEmuRepl:
+    def __init__(self, args):
+        super(CEmuRepl, self).__init__()
+        assert cemu.core.context
+        assert isinstance(cemu.core.context, cemu.core.GlobalContext)
+
+        self.history_filepath = pathlib.Path().home() / ".cemu_history"
+
+        self.keep_running = False
+        self.prompt = "(cemu)> "
+        self.__background_emulator_thread = EmulationRunner()
+        cemu.core.context.emulator.set_threaded_runner(
+            self.__background_emulator_thread
+        )
+
+        # register the callbacks for the emulator
+        emu = cemu.core.context.emulator
+        # emu.add_state_change_cb(
+        #     EmulatorState.NOT_RUNNING, self.update_layout_not_running
+        # )
+        # emu.add_state_change_cb(EmulatorState.RUNNING, self.update_layout_running)
+        # emu.add_state_change_cb(EmulatorState.IDLE, self.update_layout_step_running)
+        # emu.add_state_change_cb(
+        #     EmulatorState.FINISHED, self.update_layout_step_finished
+        # )
+
+        dbg("REPL initialized")
+
+        #
+        # set the emulator to a new context
+        #
+        emu.reset()
+        return
+
+    def run_forever(self):
+        self.keep_running = True
+        emu = cemu.core.context.emulator
+        while self.keep_running:
+            #
+            # Refresh the completer values
+            #
+            menu_dict = {
+                ".quit": None,
+                ".version": None,
+                ".debug": None,
+                ".arch": {
+                    "get": None,
+                    "set": {x: None for x in cemu.arch.Architectures.keys()},
+                },
+                ".regs": {
+                    "set": {x: None for x, _ in emu.registers.items()},
+                    "get": {x: None for x, _ in emu.registers.items()},
+                },
+                ".mem": {
+                    "default": None,
+                    "list": None,
+                    "add": None,
+                    "del": None,
+                    "view": None,
+                    "edit": None,
+                },
+                ".reset": None,
+                ".run": None,
+                ".code": {
+                    "show": None,
+                    "check": None,
+                    "edit": None,
+                },
+                ".step": {
+                    "on": None,
+                    "off": None,
+                },
+                # TODO everything below
+                ".load": {
+                    "asm": None,
+                    "bin": None,
+                    "minidump": None,
+                    "coredump": None,
+                },
+                ".save": {
+                    "asm": None,
+                    "bin": None,
+                    "pe": None,
+                    "elf": None,
+                },
+            }
+            completer = NestedCompleter.from_nested_dict(menu_dict)
+
+            #
+            # Prompt
+            #
+            try:
+                line = prompt(
+                    self.prompt,
+                    history=FileHistory(str(self.history_filepath)),
+                    completer=completer,
+                    auto_suggest=AutoSuggestFromHistory(),
+                    bottom_toolbar=self.bottom_toolbar,
+                ).strip()
+
+                if line.startswith("."):
+                    #
+                    # Entering a command ?
+                    #
+                    line = line[1:]
+                    parts = line.split()
+                    command, args = (
+                        (parts[0].lower(), parts[1:])
+                        if len(parts) >= 2
+                        else (parts[0].lower(), [])
+                    )
+                else:
+                    #
+                    # It's assembly, happen and keep looping
+                    #
+                    emu.codelines += line + os.linesep
+                    continue
+
+            except (KeyboardInterrupt, EOFError):
+                self.keep_running = False
+                continue
+
+            #
+            # Dispatch
+            #
+            match command:
+                case "help":
+                    print(os.linesep.join([f" - {x}" for x in menu_dict.keys()]))
+
+                case "version":
+                    print(f"{cemu.const.PROGNAME} v{cemu.const.VERSION}")
+
+                case "quit":
+                    self.keep_running = False
+
+                case "arch":
+                    match args[0]:
+                        case "get":
+                            print(
+                                f"Current architecture {cemu.core.context.architecture}"
+                            )
+                        case "set":
+                            cemu.core.context.architecture = (
+                                cemu.arch.Architectures.find(args[1])
+                            )
+
+                case "regs":
+                    match args[0]:
+                        case "get":
+                            if len(args) == 1:
+                                for regname, regvalue in emu.registers.items():
+                                    print(f"{regname}={regvalue:#x}")
+                            else:
+                                print(f"{args[1]}={emu.registers[args[1]]:#x}")
+                        case "set":
+                            emu.registers[args[1]] = int(args[2])
+
+                case "mem":
+                    match args[0]:
+                        case "default":
+                            emu.sections = MEMORY_MAP_DEFAULT_LAYOUT[:]
+                        case "list":
+                            for idx, section in enumerate(emu.sections):
+                                print(f"{idx:#04x}\t{section}")
+                        case "add":
+                            section = cemu.memory.MemorySection(
+                                args[1], int(args[2], 0), int(args[3], 0), args[4]
+                            )
+                            emu.sections.append(section)
+                            dbg(f"Section {section} added")
+                        case "del":
+                            section = emu.sections[int(args[1])]
+                            emu.sections.remove(section)
+                            dbg(f"Section {section} deleted")
+                        case "view":
+                            if emu.state not in (
+                                EmulatorState.RUNNING,
+                                EmulatorState.IDLE,
+                                EmulatorState.FINISHED,
+                            ):
+                                warn("Emulator not running")
+                            else:
+                                assert emu.vm
+                                address = int(args[1], 0)
+                                size = int(args[2], 0)
+                                data = emu.vm.mem_read(address, size)
+                                print(hexdump(data, base=address))
+
+                case "code":
+                    match args[0]:
+                        case "show":
+                            info(f"{emu.codelines}")
+
+                        case "check":
+                            if emu.validate_assembly_code():
+                                ok("Assembly code is valid")
+                                print(hexdump(emu.code))
+                            else:
+                                error("Assembly code is invalid")
+
+                        case "edit":
+                            with tempfile.NamedTemporaryFile(
+                                suffix=".asm", mode="w+"
+                            ) as f:
+                                filepath = pathlib.Path(f.name)
+                                f.write(emu.codelines)
+                                f.flush()
+                                if subprocess.call([TEXT_EDITOR, filepath]) == 0:
+                                    f.seek(0)
+                                    emu.codelines = f.read()
+
+                case "reset":
+                    emu.reset()
+
+                case "run":
+                    emu.set(EmulatorState.RUNNING)
+
+                case "step":
+                    match args[0]:
+                        case "on":
+                            emu.use_step_mode = True
+                        case "on":
+                            emu.use_step_mode = False
+
+                case "reset":
+                    emu.set(EmulatorState.FINISHED)
+
+                case "debug":
+                    cemu.const.DEBUG = not cemu.const.DEBUG
+                    ok(f"Debug is now {'on' if cemu.const.DEBUG else 'off'}")
+
+                case _:
+                    error(f"Executing {command=}")
+        return
+
+    def bottom_toolbar(self) -> str:
+        return (
+            f"{str(cemu.core.context.emulator)} [{str(cemu.core.context.architecture)}]"
+        )
+
+
+class EmulationRunner:
+    """Runs an emulation session"""
+
+    def run(self):
+        """
+        Runs the emulation
+        """
+        emu = cemu.core.context.emulator
+        if not emu.vm:
+            error("VM is not ready")
+            return
+
+        if not emu.is_running:
+            error("Emulator is in invalid state")
+            return
+
+        try:
+            start_address = emu.pc() or emu.start_addr
+            start_offset = start_address - emu.start_addr
+
+            #
+            # Determine where to stop
+            #
+            if emu.use_step_mode:
+                insn = emu.next_instruction(emu.code[start_offset:], start_address)
+                end_address = insn.end
+                info(f"Stepping from {start_address:#x} to {end_address:#x}")
+            else:
+                end_address = emu.start_addr + len(emu.code)
+                info(f"Running all from {start_address:#x} to {end_address:#x}")
+
+            with emu.lock:
+                #
+                # Run the emulator, let's go!
+                #
+                emu.vm.emu_start(start_address, end_address)
+
+            #
+            # If the execution is over, mark the state as finished
+            #
+            if emu.pc() == (emu.start_addr + len(emu.code)):
+                emu.set(EmulatorState.FINISHED)
+            else:
+                emu.set(EmulatorState.IDLE)
+
+        except unicorn.unicorn.UcError as e:
+            error(f"An error occured: {str(e)} at pc={emu.pc():#x}, sp={emu.sp():#x}")
+            emu.set(EmulatorState.FINISHED)
+
+        return
diff --git a/cemu/const.py b/cemu/const.py
index 9e3f447..0a7d514 100644
--- a/cemu/const.py
+++ b/cemu/const.py
@@ -10,9 +10,11 @@
 ISSUE_LINK = f"https://github.com/{AUTHOR}/{PROGNAME}/issues"
 RELEASE_LINK = f"{URL}/archive/{VERSION}.tar.gz"
 LICENSE = "MIT"
-DESCRIPTION = """Cemu is a simple assembly/dissembly/emulation IDE that provides an easy """\
-    """Plug-n-Play environment to start playing with many architectures (currently supports """\
-    """"x86-{32,64}, ARM, AARCH64, MIPS, SPARC)."""
+DESCRIPTION = (
+    """Cemu is a simple assembly/dissembly/emulation IDE that provides an easy """
+    """Plug-n-Play environment to start playing with many architectures (currently """
+    """supports x86-{32,64}, ARM, AARCH64, MIPS, MIPS64, SPARC and PPC)."""
+)
 HOME = pathlib.Path("").home()
 PKG_PATH = pathlib.Path(__file__).absolute().parent
 ICON_PATH = pathlib.Path(PKG_PATH) / "img/icon.png"
@@ -30,3 +32,20 @@
 
 LOG_INSERT_TIMESTAMP = False
 LOG_DEFAULT_TIMESTAMP_FORMAT = "%Y/%m/%d - %H:%M:%S"
+
+DEFAULT_REGISTER_VIEW_REGISTER_FONT: str = "Courier"
+DEFAULT_REGISTER_VIEW_REGISTER_FONT_SIZE: int = 11
+DEFAULT_REGISTER_VIEW_CHANGED_REGISTER_COLOR: str = "red"
+
+DEFAULT_CODE_VIEW_FONT: str = "Courier"
+DEFAULT_CODE_VIEW_FONT_SIZE: int = 11
+
+DEFAULT_ASSEMBLY_VIEW_FONT: str = "Courier"
+DEFAULT_ASSEMBLY_VIEW_FONT_SIZE: int = 11
+
+DEFAULT_MEMORY_VIEW_FONT: str = "Courier"
+DEFAULT_MEMORY_VIEW_FONT_SIZE: int = 10
+DEFAULT_MEMORY_VIEW_CHUNK_SIZE: int = 256
+
+DEFAULT_FONT: str = "Courier"
+DEFAULT_FONT_SIZE: int = 10
diff --git a/cemu/core.py b/cemu/core.py
index 6178543..d77b648 100644
--- a/cemu/core.py
+++ b/cemu/core.py
@@ -1,32 +1,34 @@
 from __future__ import annotations
 
+import argparse
+
 import sys
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Union
 
-from PyQt6.QtGui import QIcon
-from PyQt6.QtWidgets import QApplication
+if TYPE_CHECKING:
+    import cemu.ui.main
 
 import cemu.arch
+import cemu.cli.repl
 import cemu.const
 import cemu.emulator
 import cemu.log
-import cemu.plugins
+import cemu.os
 import cemu.settings
-import cemu.ui.main
 
 
-class BackendContext:
+class GlobalContext:
     settings: cemu.settings.Settings
     __emulator: cemu.emulator.Emulator
     __architecture: cemu.arch.Architecture
-    __root: cemu.ui.main.CEmuWindow
+    __os: cemu.os.OperatingSystem
 
     def __init__(self):
         self.settings = cemu.settings.Settings()
         self.__emulator = cemu.emulator.Emulator()
-        default_arch = self.settings.get(
-            "Global", "DefaultArchitecture", "x86_64")
+        default_arch = self.settings.get("Global", "DefaultArchitecture", "x86_64")
         self.__architecture = cemu.arch.Architectures.find(default_arch)
+        self.__os = cemu.os.Linux
         return
 
     @property
@@ -35,8 +37,7 @@ def architecture(self) -> cemu.arch.Architecture:
 
     @architecture.setter
     def architecture(self, new_arch: cemu.arch.Architecture):
-        cemu.log.dbg(
-            f"Changing architecture {self.__architecture} to {new_arch}")
+        cemu.log.dbg(f"Changing architecture {self.__architecture} to {new_arch}")
         self.__architecture = new_arch
         cemu.log.dbg(f"Resetting emulator for {self.__architecture}")
         self.__emulator.reset()
@@ -46,6 +47,21 @@ def architecture(self, new_arch: cemu.arch.Architecture):
     def emulator(self) -> cemu.emulator.Emulator:
         return self.__emulator
 
+    @property
+    def os(self) -> cemu.os.OperatingSystem:
+        return self.__os
+
+    @os.setter
+    def os(self, new_os: cemu.os.OperatingSystem):
+        cemu.log.dbg(f"Changing OS {self.__os} to {new_os}")
+        self.__os = new_os
+        self.__emulator.reset()
+        return
+
+
+class GlobalGuiContext(GlobalContext):
+    __root: cemu.ui.main.CEmuWindow
+
     @property
     def root(self) -> cemu.ui.main.CEmuWindow:
         return self.__root
@@ -55,10 +71,13 @@ def root(self, root: cemu.ui.main.CEmuWindow):
         self.__root = root
 
 
-context = BackendContext()
+#
+# The global application context. This **must** defined for cemu to operate
+#
+context: Union[GlobalContext, GlobalGuiContext]
 
 
-def Cemu(args: list[str]):
+def CemuGui(args: list[str]) -> None:
     """Entry point of the GUI
 
     Args:
@@ -66,12 +85,43 @@ def Cemu(args: list[str]):
     """
     global context
 
-    if cemu.const.DEBUG:
-        cemu.log.register_sink(print)
-        cemu.log.dbg("Starting in Debug Mode")
+    from PyQt6.QtGui import QIcon
+    from PyQt6.QtWidgets import QApplication
+
+    from cemu.ui.main import CEmuWindow
+
+    cemu.log.dbg("Creating GUI context")
+    context = GlobalGuiContext()
 
     app = QApplication(args)
     app.setStyleSheet(cemu.const.DEFAULT_STYLE_PATH.open().read())
     app.setWindowIcon(QIcon(str(cemu.const.ICON_PATH.absolute())))
-    context.root = cemu.ui.main.CEmuWindow(app)
+    context.root = CEmuWindow(app)
     sys.exit(app.exec())
+
+
+def CemuCli(argv: list[str]) -> None:
+    """Entry point of the CLI
+
+    Args:
+        args (list[str]): _description_
+    """
+    global context
+
+    #
+    # Initialize the context
+    #
+    cemu.log.dbg("Creating CLI context")
+    context = GlobalContext()
+
+    #
+    # Run the REPL with the command line arguments
+    #
+    args = argparse.ArgumentParser(
+        prog=cemu.const.PROGNAME, description=cemu.const.DESCRIPTION
+    )
+    args.parse_args(argv)
+
+    instance = cemu.cli.repl.CEmuRepl(args)
+    instance.run_forever()
+    return
diff --git a/cemu/emulator.py b/cemu/emulator.py
index 0e7a3ef..37a6cfd 100644
--- a/cemu/emulator.py
+++ b/cemu/emulator.py
@@ -1,85 +1,115 @@
-import functools
-import os
-from enum import Enum, unique
-from typing import Any, Callable, Dict, List, Optional
+from enum import IntEnum, unique
+from multiprocessing import Lock
+from typing import Any, Callable, Optional
 
+import collections
 import unicorn
 
+import cemu.const
 import cemu.core
 import cemu.utils
-from cemu.log import dbg, error, info, log
+from cemu.log import dbg, error, info, warn
 
-from .arch import (Syntax, is_aarch64, is_arm, is_arm_thumb, is_mips,
-                   is_mips64, is_ppc, is_sparc, is_sparc64, is_x86, is_x86_16,
-                   is_x86_32, is_x86_64)
+from .arch import is_x86, is_x86_32, x86
 from .memory import MemorySection
-from .utils import assemble, get_arch_mode
 
 
 @unique
-class EmulatorState(Enum):
-    NOT_RUNNING = 0
-    IDLE = 1
-    RUNNING = 2
-    STEP_RUNNING = 3
-    FINISHED = 4
+class EmulatorState(IntEnum):
+    # fmt: off
+    STARTING = 0                 # CEmu is starting
+    NOT_RUNNING = 1              # CEmu is started, but no emulation context is initialized
+    IDLE = 2                     # The VM is running but stopped: used for stepping mode
+    RUNNING = 3                  # The VM is running
+    TEARDOWN = 5                 # Emulation is finishing
+    FINISHED = 6                 # The VM has reached the end of the execution
+    # fmt: on
+
+
+class EmulationRegisters(collections.UserDict):
+    data: dict[str, int]
+
+    def __getitem__(self, key: str) -> int:
+        """Thin wrapper around `dict` `__getitem__` for register: try to refresh the value to its latest value from the
+        emulator.
+
+        Args:
+            key (str): register name
+
+        Returns:
+            int: the register value
+        """
+        emu = cemu.core.context.emulator
+        if (
+            emu.state
+            in (EmulatorState.RUNNING, EmulatorState.IDLE, EmulatorState.FINISHED)
+            and key in self.data.keys()
+        ):
+            val = emu.get_register_value(key)
+            if val is not None:
+                super().__setitem__(key, val)
+        return super().__getitem__(key)
 
 
 class Emulator:
-    EMU = 0
-    LOG = 1
-
     def __init__(self):
         self.use_step_mode = False
         self.widget = None
-        self.reset()
+        self.lock = Lock()
+        self.state: EmulatorState = EmulatorState.STARTING
+        self.__state_change_callbacks: dict[EmulatorState, list[Callable]] = {
+            EmulatorState.STARTING: [],
+            EmulatorState.NOT_RUNNING: [],
+            EmulatorState.IDLE: [],
+            EmulatorState.RUNNING: [],
+            EmulatorState.TEARDOWN: [],
+            EmulatorState.FINISHED: [],
+        }
+        self.threaded_runner: Optional[object] = None
+        self.vm: Optional[unicorn.Uc] = None
+        self.code: bytes = b""
+        self.codelines: str = ""
+        self.sections: list[MemorySection] = []
+        self.registers: EmulationRegisters = EmulationRegisters({})
+        self.start_addr: int = 0
+
+        #
+        # A call to `reset` **MUST** be done once the program is fully loaded
+        #
         return
 
     def reset(self):
-        self.vm: Optional[unicorn.Uc] = None
-        self.code = None
-        self.__vm_state = EmulatorState.NOT_RUNNING
-        self.stop_now = False
-        self.num_insns = -1
-        self.areas = {}
-        self.registers = {}
+        self.vm = None
+        self.code = b""
+        self.codelines = ""
+        self.sections = []
+        self.registers = EmulationRegisters(
+            {name: 0 for name in cemu.core.context.architecture.registers}
+        )
+        self.start_addr = 0
+        self.set(EmulatorState.NOT_RUNNING)
         return
 
     def __str__(self) -> str:
-        return f"Emulator instance {'' if self.is_running else 'not '}running"
-
-    def unicorn_register(self, reg):
-        curarch = cemu.core.context.architecture
-        if is_x86(curarch):
-            return getattr(unicorn.x86_const, "UC_X86_REG_%s" % reg.upper())
-
-        if is_arm(curarch) or is_arm_thumb(curarch):
-            return getattr(unicorn.arm_const, "UC_ARM_REG_%s" % reg.upper())
-
-        if is_aarch64(curarch):
-            return getattr(unicorn.arm64_const, "UC_ARM64_REG_%s" % reg.upper())
+        if self.is_running:
+            return f"Emulator is running, IP={self.pc()}, SP={self.sp()}"
+        return "Emulator instance is not running"
 
-        # if is_ppc(curarch):
-        #     return getattr(unicorn.ppc_const, "UC_PPC_REG_%s" % reg.upper())
-
-        if is_mips(curarch) or is_mips64(curarch):
-            return getattr(unicorn.mips_const, "UC_MIPS_REG_%s" % reg.upper())
-
-        if is_sparc(curarch) or is_sparc64(curarch):
-            return getattr(unicorn.sparc_const, "UC_SPARC_REG_%s" % reg.upper())
-
-        raise Exception("Cannot find register '%s' for arch '%s'" %
-                        (reg, curarch))
-
-    def get_register_value(self, regname: str) -> int:
+    def get_register_value(self, regname: str) -> Optional[int]:
         """
         Returns an integer value of the register passed as a string.
         """
+
         if not self.vm:
-            return -1
+            return None
+
+        arch = cemu.core.context.architecture
+        ur = arch.uc_register(regname)
+        val = self.vm.reg_read(ur)
 
-        ur = self.unicorn_register(regname)
-        return self.vm.reg_read(ur)
+        # TODO handle extended regs later
+        assert isinstance(val, int)
+        return val
 
     regs = get_register_value
 
@@ -87,67 +117,79 @@ def pc(self) -> int:
         """
         Returns the current value of $pc
         """
-        return self.get_register_value(cemu.core.context.architecture.pc)
+        # return self.get_register_value(cemu.core.context.architecture.pc)
+        return self.registers[cemu.core.context.architecture.pc]
 
     def sp(self) -> int:
         """
         Returns the current value of $sp
         """
-        return self.get_register_value(cemu.core.context.architecture.sp)
+        # return self.get_register_value(cemu.core.context.architecture.sp)
+        return self.registers[cemu.core.context.architecture.sp]
 
-    def unicorn_permissions(self, perms: str) -> int:
-        """
-        Returns the value as an integer of the permission mask given as input.
-        """
-        p = 0
-        for perm in perms.split("|"):
-            p |= getattr(unicorn, "UC_PROT_{}".format(perm.upper(),))
-        return p
-
-    def create_new_vm(self) -> None:
+    def setup(self) -> None:
         """
         Create a new VM, and sets up the hooks
         """
-        arch, mode, endian = get_arch_mode("unicorn")
-        self.vm = unicorn.Uc(arch, mode | endian)
+        if self.vm:
+            #
+            # Environment already setup, just resume
+            #
+            return
+
+        info("Setting up emulation environment...")
+
+        arch = cemu.core.context.architecture
+        self.vm = arch.uc
         self.vm.hook_add(unicorn.UC_HOOK_BLOCK, self.hook_block)
         self.vm.hook_add(unicorn.UC_HOOK_CODE, self.hook_code)
-        self.vm.hook_add(unicorn.UC_HOOK_INTR, self.hook_interrupt)
+        self.vm.hook_add(unicorn.UC_HOOK_INTR, self.hook_interrupt)  # type: ignore
         self.vm.hook_add(unicorn.UC_HOOK_MEM_WRITE, self.hook_mem_access)
         self.vm.hook_add(unicorn.UC_HOOK_MEM_READ, self.hook_mem_access)
         if is_x86(cemu.core.context.architecture):
-            self.vm.hook_add(unicorn.UC_HOOK_INSN, self.hook_syscall,
-                             None, 1, 0, unicorn.x86_const.UC_X86_INS_SYSCALL)
+            self.vm.hook_add(
+                unicorn.UC_HOOK_INSN,
+                self.hook_syscall,
+                None,
+                1,
+                0,
+                unicorn.x86_const.UC_X86_INS_SYSCALL,
+            )
+
+        if not self.__populate_memory():
+            raise Exception("populate_memory() failed")
+
+        if not self.__populate_vm_registers():
+            raise Exception("populate_registers() failed")
+
+        if not self.__populate_text_section():
+            raise Exception("populate_text_section() failed")
+
         return
 
-    def populate_memory(self, areas: List[MemorySection]) -> bool:
+    def __populate_memory(self) -> bool:
         """
-        Populates the VM memory layout according to the values given as parameter.
+        Uses the information from `sections` to populate the unicorn VM memory layout
         """
         if not self.vm:
             error("VM is not initalized")
             return False
 
-        for area in areas:
-            name, address, size, permission, input_file = area.export()
-            perm = self.unicorn_permissions(permission)
-            self.vm.mem_map(address, size, perm)
-            self.areas[name] = [address, size, permission, ]
+        for section in self.sections:
+            self.vm.mem_map(section.address, section.size, perms=section.permission.unicorn())
+            msg = f"Mapping {str(section)}"
 
-            msg = "Map %s @%x (size=%d,perm=%s)" % (name,
-                                                    address, size, permission)
-            if input_file is not None and os.access(input_file, os.R_OK):
-                code = open(input_file, 'rb').read()
-                self.vm.mem_write(address, bytes(code[:size]))
-                msg += " and content from '%s'" % input_file
+            if section.content:
+                self.vm.mem_write(section.address, section.content)
+                msg += f", imported data '{len(section.content)}'"
 
-            info(f"[vm::setup] {msg}")
+            dbg(f"[vm::setup] {msg}")
 
-        self.start_addr = self.areas[".text"][0]
+        self.start_addr = self.sections[0].address
         self.end_addr = -1
         return True
 
-    def populate_registers(self, registers: Dict[str, int]) -> bool:
+    def __populate_vm_registers(self) -> bool:
         """
         Populates the VM memory layout according to the values given as parameter.
         """
@@ -156,224 +198,422 @@ def populate_registers(self, registers: Dict[str, int]) -> bool:
 
         arch = cemu.core.context.architecture
 
-        for r in registers.keys():
-            if is_x86_32(arch):
-                # temporary hack for x86 segmentation issue
-                if r in ('GS', 'FS', 'SS', 'DS', 'CS', 'ES'):
-                    continue
+        #
+        # Set the initial IP if unspecified
+        #
+        if self.registers[arch.pc] == 0:
+            section = self.find_section(".text")
+            self.registers[arch.pc] = section.address
+            warn(
+                f"No value specified for PC register, setting to {self.registers[arch.pc]:#x}"
+            )
+
+        #
+        # Set the initial SP if unspecified, in the middle of the stack section
+        #
+        if self.registers[arch.sp] == 0:
+            section = self.find_section(".stack")
+            self.registers[arch.sp] = section.address + (section.size // 2)
+            warn(
+                f"No value specified for SP register, setting to {self.registers[arch.sp]:#x}"
+            )
+
+        #
+        # Populate all the registers for unicorn
+        #
+        if is_x86_32(arch):
+            # create fake selectors
+            ## required
+            text = self.find_section(".text")
+            self.registers["CS"] = int(
+                x86.X86_32.SegmentDescriptor(
+                    text.address >> 8,
+                    x86.X86_32.SegmentType.Code | x86.X86_32.SegmentType.Accessed,
+                    False,
+                    3,
+                    True,
+                )
+            )
+
+            data = self.find_section(".data")
+            self.registers["DS"] = int(
+                x86.X86_32.SegmentDescriptor(
+                    data.address >> 8,
+                    x86.X86_32.SegmentType.Data | x86.X86_32.SegmentType.Accessed,
+                    False,
+                    3,
+                    True,
+                )
+            )
 
-            ur = self.unicorn_register(r)
-            self.vm.reg_write(ur, registers[r])
-            dbg(f"[vm::setup] Register '{r}' = {registers[r]:#x}")
+            stack = self.find_section(".stack")
+            self.registers["SS"] = int(
+                x86.X86_32.SegmentDescriptor(
+                    stack.address >> 8,
+                    x86.X86_32.SegmentType.Data
+                    | x86.X86_32.SegmentType.Accessed
+                    | x86.X86_32.SegmentType.ExpandDown,
+                    False,
+                    3,
+                    True,
+                )
+            )
+            ## optional
+            self.registers["GS"] = 0
+            self.registers["FS"] = 0
+            self.registers["ES"] = 0
+
+        for regname, regvalue in self.registers.items():
+            # TODO figure out segmentation on unicorn
+            if regname in x86.X86_32.selector_registers:
+                continue
 
-        ur = self.unicorn_register(arch.pc)
-        self.vm.reg_write(ur, self.areas[".text"][0])
-        ur = self.unicorn_register(arch.sp)
-        self.vm.reg_write(ur, self.areas[".stack"][0])
+            self.vm.reg_write(arch.uc_register(regname), regvalue)
+
+        dbg(f"[vm::setup] Registers {self.registers}")
         return True
 
-    def assemble_code(self, code: str, update_end_addr: bool = True) -> bool:
-        """
-        Compile the assembly code using Keystone. Returns True if all went well,
-        False otherwise.
-        """
-        instructions = code.splitlines()
-        nb_instructions = len(instructions)
+    def __refresh_registers_from_vm(self) -> bool:
+        """Refresh the emulation register hashmap by reading values from the VM
 
-        dbg(f"[vm::setup] Assembling {nb_instructions} instructions for {cemu.core.context.architecture.name}")
-        self.code, self.num_insns = assemble(code)
-        if self.num_insns < 0:
-            error(f"Failed to compile: error at line {-self.num_insns:d}")
+        Returns:
+            bool: True on success, False otherwise
+        """
+        if not self.vm or not self.is_running:
             return False
 
-        if self.num_insns != nb_instructions:
-            error(
-                f"[vm::setup] Unexpected number of compiled instructions (got {self.num_insns}, compiled {nb_instructions})")
+        arch = cemu.core.context.architecture
+        for regname in self.registers.keys():
+            value = self.vm.reg_read(arch.uc_register(regname))
+            assert isinstance(value, int)
+            self.registers[regname] = value
+
+        return True
+
+    def __generate_text_bytecode(self) -> bool:
+        """Compile the assembly code using Keystone.
+
+        Returns:
+            bool True if all went well, False otherwise.
+        """
+        dbg(
+            f"[vm::setup] Generating assembly code for {cemu.core.context.architecture.name}"
+        )
+
+        try:
+            insns = cemu.utils.assemble(self.codelines, base_address=self.start_addr)
+            if len(insns) == 0:
+                raise Exception("no instruction")
+        except Exception as e:
+            error(f"Failed to compile: exception {e.__class__.__name__}: {str(e)}")
             return False
 
-        dbg(f"[vm::setup] {self.num_insns} instruction{'s' if self.num_insns > 1 else ''} compiled: {len(self.code)} bytes")
+        self.code = b"".join([insn.bytes for insn in insns])
+        dbg(f"[vm::setup] {len(insns)} instruction(s) compiled: {len(self.code)} bytes")
 
-        # update end_addr since we know the size of the code to execute
-        if update_end_addr:
-            self.end_addr = self.start_addr + len(self.code)
+        self.end_addr = self.start_addr + len(self.code)
         return True
 
-    def map_code(self) -> bool:
+    def validate_assembly_code(self) -> bool:
+        return self.__generate_text_bytecode()
+
+    def __populate_text_section(self) -> bool:
         if not self.vm:
             return False
 
-        if ".text" not in self.areas.keys():
-            error(
-                "[vm::setup] Missing text area (add a .text section in the Mapping tab)")
+        try:
+            text_section = self.find_section(".text")
+        except KeyError:
+            #
+            # Try to get the 1st executable section. Let the exception propagage if it fails
+            #
+            matches = [
+                section for section in self.sections if section.permission.executable
+            ]
+            text_section = matches[0]
+
+        info(f"Using text section {text_section}")
+
+        if not self.__generate_text_bytecode():
+            error("__generate_text_bytecode() failed")
             return False
 
-        if self.code is None:
-            error("[vm::setup] No code defined yet")
-            return False
+        assert isinstance(self.code, bytes)
 
-        addr = self.areas[".text"][0]
-        info(f"Mapping .text at {addr:#x}")
-        self.vm.mem_write(addr, bytes(self.code))
+        dbg(
+            f"Populated text section {text_section} with {len(self.code)} compiled bytes"
+        )
+        self.vm.mem_write(text_section.address, self.code)
         return True
 
-    def disassemble_one_instruction(self, code: bytearray, addr: int) -> str:
+    def next_instruction(self, code: bytes, addr: int) -> cemu.utils.Instruction:
         """
         Returns a string disassembly of the first instruction from `code`.
         """
-        arch = cemu.core.context.architecture
-        for insn in cemu.utils.disassemble(code, 1, addr).values():
-            return f"{insn[0], insn[1]}"
+        for insn in cemu.utils.disassemble(code, 1, addr):
+            return insn
 
         raise Exception("should never be here")
 
-    def hook_code(self, emu, address, size, user_data):
+    def hook_code(
+        self, emu: unicorn.Uc, address: int, size: int, user_data: Any
+    ) -> bool:
         """
         Unicorn instruction hook
         """
+        if not cemu.const.DEBUG:
+            return False
+
         if not self.vm:
             return False
 
-        arch = cemu.core.context.architecture
         code = self.vm.mem_read(address, size)
-        insn = self.disassemble_one_instruction(code, address)
-
-        if self.stop_now:
-            self.start_addr = self.get_register_value(arch.pc)
-            emu.emu_stop()
-            return
-
-        dbg(f"[vm::runtime] Executing instruction at {address:#x}")
-        log(f"[vm::runtime] {address:#x}: {insn}")
+        insn: cemu.utils.Instruction = self.next_instruction(code, address)
 
         if self.use_step_mode:
-            self.stop_now = True
-        return
+            dbg(f"[vm::runtime] Stepping @ {insn}")
+        else:
+            dbg(f"[vm::runtime] Executing @ {insn}")
+        return True
 
-    def hook_block(self, emu, addr, size, misc):
+    def hook_block(self, emu: unicorn.Uc, addr: int, size: int, misc: Any) -> int:
         """
         Unicorn block change hook
         """
         dbg(f"[vm::runtime] Entering block at {addr:#x}")
-        return
+        return 0
 
-    def hook_interrupt(self, emu, intno, data):
+    def hook_interrupt(self, emu: unicorn.Uc, intno: int, data: Any) -> None:
         """
         Unicorn interrupt hook
         """
         dbg(f"[vm::runtime] Triggering interrupt #{intno:d}")
         return
 
-    def hook_syscall(self, emu, user_data):
+    def hook_syscall(self, emu: unicorn.Uc, data: Any) -> int:
         """
         Unicorn syscall hook
         """
-        dbg(f"[vm::runtime] Syscall")
-        return
-
-    def hook_mem_access(self, emu, access, address, size, value, _):
+        dbg("[vm::runtime] Syscall")
+        return 0
+
+    def hook_mem_access(
+        self,
+        emu: unicorn.Uc,
+        access: int,
+        address: int,
+        size: int,
+        value: int,
+        extra: Any,
+    ) -> None:
         if access == unicorn.UC_MEM_WRITE:
             info(f"Write: *{address:#x} = {value:#x} (size={size})")
         elif access == unicorn.UC_MEM_READ:
             info(f"Read: *{address:#x} (size={size})")
         return
 
-    def run(self) -> None:
+    def teardown(self) -> None:
         """
-        Runs the emulation
+        Stops the unicorn environment
         """
         if not self.vm:
-            error("VM is not ready")
             return
 
-        info("Starting emulation context")
+        info(f"Ending emulation context at {self.pc():#x}")
 
-        try:
-            if self.use_step_mode:
-                self.set_vm_state(EmulatorState.STEP_RUNNING)
-            else:
-                self.set_vm_state(EmulatorState.RUNNING)
+        for section in self.sections:
+            dbg(f"[vm::teardown] Unmapping {section}")
+            self.vm.mem_unmap(section.address, section.size)
 
-            self.vm.emu_start(self.start_addr, self.end_addr)
+        dbg(f"[vm::teardown] Deleting {self.vm}")
+        del self.vm
+        self.vm = None
+        return
 
-            if self.pc() == self.end_addr:
-                self.set_vm_state(EmulatorState.FINISHED)
+    def find_section(self, section_name: str) -> MemorySection:
+        """Lookup a particular section by its name
 
-        except unicorn.unicorn.UcError as e:
-            error(f"An error occured: {str(e)}")
-            log(f"pc={self.pc():  # x} , sp={self.sp():#x}")
-            self.set_vm_state(EmulatorState.FINISHED)
+        Args:
+            section_name (str): the name of the sections to search
 
-        return
+        Raises:
+            KeyError: if `section_name` not found
 
-    def __stop(self) -> None:
+        Returns:
+            MemorySection: _description_
         """
-        Stops the VM, frees the allocations
-        """
-        if not self.vm:
-            return
+        matches = [section for section in self.sections if section.name == section_name]
+        if not matches:
+            raise KeyError(f"Section '{section_name}' not found")
 
-        self.set_vm_state(EmulatorState.NOT_RUNNING)
+        if len(matches) > 1:
+            raise ValueError(f"Too many sections named {section_name}")
 
-        for area in self.areas.keys():
-            addr, size = self.areas[area][0:2]
-            self.vm.mem_unmap(addr, size)
+        return matches[0]
 
-        del self.vm
-        self.vm = None
-        return
+    def add_state_change_cb(self, new_state: EmulatorState, cb: Callable) -> None:
+        """Register a callback triggered when the emulator switches to a new state
 
-    def __finish(self) -> None:
+        Args:
+            new_state (EmulatorState): the new state
+            cb (Callable): the callback to execute when that happens
         """
-        Clean up the emulator execution context.
 
-        This internal function is called when the VM execution has finished, i.e.:
-        1. an exception has occured in unicorn
-        2. the emulator has reached the .text end address
-        """
-        info("Ending emulation context")
-        self.__stop()
+        self.__state_change_callbacks[new_state].append(cb)
         return
 
-    def lookup_map(self, mapname: str):
-        """
+    def set(self, new_state: EmulatorState):
+        """Set the new state of the emulator, and invoke the associated callbacks
+
+        Args:
+            new_state (EmulatorState): the new state
         """
-        for area in self.areas.keys():
-            if area == mapname:
-                return self.areas[area][0]
-        raise KeyError("Section '{}' not found".format(mapname))
 
-    def set_vm_state(self, new_state: EmulatorState) -> None:
+        if self.state == new_state:
+            return
+
+        dbg(f"Emulator state transition: {self.state.name} -> {new_state.name}")
+
+        #
+        # Validate which state we're entering
+        #
+        match new_state:
+            case EmulatorState.RUNNING | EmulatorState.IDLE:
+                #
+                # Make sure there's always an emulation environment ready
+                #
+                self.setup()
+
+                #
+                # If we stopped from execution (i.e RUNNING -> [IDLE,FINISHED]), refresh registers
+                #
+                if self.state == EmulatorState.RUNNING:
+                    self.__refresh_registers_from_vm()
+
+            case EmulatorState.FINISHED:
+                self.__refresh_registers_from_vm()
+
+            case _:
+                pass
+
+        #
+        # Do the state change
+        #
+        info(f"Emulator is now {new_state.name}")
+        self.state = new_state
+
+        dbg(
+            f"Executing {len(self.__state_change_callbacks[new_state])} callbacks for state {new_state.name}"
+        )
+
+        #
+        # Notify the components who've subscribed to the new state change
+        #
+        for new_state_cb in self.__state_change_callbacks[new_state]:
+            function_name = f"{new_state_cb.__module__}.{new_state_cb.__class__.__qualname__}.{new_state_cb.__name__}"
+            res = new_state_cb()
+            dbg(f"{function_name}() return {res}")
+
+        match self.state:
+            case EmulatorState.RUNNING:
+                #
+                # This will effectively trigger the execution in unicorn
+                #
+                assert self.threaded_runner, "No threaded runner defined"
+                assert callable(
+                    getattr(self.threaded_runner, "run")
+                ), "Threaded runner is not runnable"
+                self.threaded_runner.run()  # type: ignore
+
+            case EmulatorState.TEARDOWN:
+                #
+                # When the execution is finished, cleanup and switch back to a "NotRunning" state
+                # This is done to make sure all the callback can still access the VM
+                #
+                self.teardown()
+
+                #
+                # Completely reset the emulation envionment, and set the status to NOT_RUNNING
+                #
+                self.reset()
+
+            case _:
+                pass
+
+        return
+
+    @property
+    def is_running(self) -> bool:
+        return self.state in (
+            EmulatorState.RUNNING,
+            EmulatorState.IDLE,
+        )
+
+    def set_threaded_runner(self, runnable_object: object):
+        self.threaded_runner = runnable_object
+        return
+
+    def read(self, address: int, size: int) -> Optional[bytearray]:
+        """Public wrapper for `vm.mem_read`
+
+        Args:
+            address (int): _description_
+            size (int): _description_
+
+        Returns:
+            bytearray:
+            None:
         """
-        Updates the internal state of the VM, and propagates the notification
-        signals.
+        if not self.vm or not self.is_running:
+            return None
+
+        return self.vm.mem_read(address, size)
+
+    def write(self, address: int, data: bytes) -> Optional[int]:
+        """Public wrapper for `vm.mem_write`
+
+        Args:
+            address (int): _description_
+            data (bytes): _description_
+
+        Returns:
+            bytearray:
+            None:
         """
-        dbg(f"Switching VM state from {self.__vm_state} to {new_state}")
-        self.__vm_state = new_state
+        if not self.vm or not self.is_running:
+            return None
 
-        if new_state == EmulatorState.NOT_RUNNING:
-            cemu.core.context.root.signals["refreshRegisterGrid"].emit()
-            cemu.core.context.root.signals["refreshMemoryEditor"].emit()
-            cemu.core.context.root.signals["setCommandButtonStopState"].emit()
-            return
+        self.vm.mem_write(address, data)
+        return len(data)
 
-        if new_state == EmulatorState.RUNNING:
-            cemu.core.context.root.signals["setCommandButtonsRunState"].emit()
-            return
+    def start(self, start_address: int, end_address: int) -> None:
+        """Public wrapper for `vm.emu_start`
 
-        if new_state == EmulatorState.STEP_RUNNING:
-            cemu.core.context.root.signals["refreshRegisterGrid"].emit()
-            cemu.core.context.root.signals["refreshMemoryEditor"].emit()
-            cemu.core.context.root.signals["setCommandButtonsStepRunState"].emit(
-            )
-            return
+        Args:
 
-        if new_state == EmulatorState.FINISHED:
-            self.__finish()
-            return
+        Returns:
+            bytearray:
+            None:
+        """
+        assert self.vm
+        assert self.is_running
+
+        with self.lock:
+            self.vm.emu_start(start_address, end_address)
 
-        info(f"VM state is now {new_state}")
         return
 
-    @property
-    def is_running(self) -> bool:
-        return self.vm is not None and self.__vm_state in (EmulatorState.RUNNING, EmulatorState.STEP_RUNNING)
+    def stop(self) -> None:
+        """Public wrapper for `vm.emu_stop`
+
+        Args:
+
+        Returns:
+            bytearray:
+            None:
+        """
+        assert self.vm
+        assert self.is_running
+
+        self.vm.emu_stop()
+        return
diff --git a/cemu/errors.py b/cemu/errors.py
new file mode 100644
index 0000000..588a91a
--- /dev/null
+++ b/cemu/errors.py
@@ -0,0 +1,2 @@
+class AssemblyException(Exception):
+    pass
diff --git a/cemu/exports.py b/cemu/exports.py
index a3a4de2..aca8a7c 100644
--- a/cemu/exports.py
+++ b/cemu/exports.py
@@ -1,55 +1,37 @@
-import tempfile
 import os
+import tempfile
+from typing import Any, List
 
-from typing import Callable, Dict, List, Tuple, Any
-
-from lief import (
-    PE,
-    ELF,
-)
-
-from .arch import (
-    Architecture,
-    is_x86_32,
-    is_x86_64,
-    is_arm,
-    is_aarch64,
-    is_mips,
-)
-
-
-from .memory import (
-    MemoryPermission,
-    MemorySection,
-)
-
+from lief import PE
 
-from .utils import (
-    generate_random_string,
-)
+from .arch import Architecture, is_x86_32, is_x86_64
+from .memory import MemoryPermission, MemorySection
+from .utils import generate_random_string
 
 
 def parse_as_lief_pe_permission(perm: MemoryPermission, extra: Any = None) -> int:
     res = 0
     if perm.r:
-        res |= PE.SECTION_CHARACTERISTICS.MEM_READ
+        res |= PE.SECTION_CHARACTERISTICS.MEM_READ.value
     if perm.w:
-        res |= PE.SECTION_CHARACTERISTICS.MEM_WRITE
+        res |= PE.SECTION_CHARACTERISTICS.MEM_WRITE.value
     if perm.x:
-        res |= PE.SECTION_CHARACTERISTICS.MEM_EXECUTE
+        res |= PE.SECTION_CHARACTERISTICS.MEM_EXECUTE.value
 
     if extra:
         if extra.lower() == "code":
-            res |= PE.SECTION_CHARACTERISTICS.CNT_CODE
+            res |= PE.SECTION_CHARACTERISTICS.CNT_CODE.value
         if extra.lower() == "idata":
-            res |= PE.SECTION_CHARACTERISTICS.CNT_INITIALIZED_DATA
+            res |= PE.SECTION_CHARACTERISTICS.CNT_INITIALIZED_DATA.value
         if extra.lower() == "udata":
-            res |= PE.SECTION_CHARACTERISTICS.CNT_UNINITIALIZED_DATA
+            res |= PE.SECTION_CHARACTERISTICS.CNT_UNINITIALIZED_DATA.value
 
     return res
 
 
-def build_pe_executable(text: bytes, memory_layout: List[MemorySection], arch: Architecture) -> str:
+def build_pe_executable(
+    text: bytes, memory_layout: List[MemorySection], arch: Architecture
+) -> str:
     """
     Uses LIEF to build a standalone binary.
 
@@ -73,8 +55,13 @@ def build_pe_executable(text: bytes, memory_layout: List[MemorySection], arch: A
     reladdr = 0x1000
 
     for mem in memory_layout:
-        name, base_address, size, permission = mem.name, mem.address, mem.size, mem.permission
-        if name in (".stack", ):
+        name, _, size, permission = (
+            mem.name,
+            mem.address,
+            mem.size,
+            mem.permission,
+        )
+        if name in (".stack",):
             continue
 
         sect = PE.Section(name)
@@ -82,18 +69,16 @@ def build_pe_executable(text: bytes, memory_layout: List[MemorySection], arch: A
         if name == ".text":
             # .text section: copy our code and set the entrypoint to the
             # beginning VA
-            sect.content = text
+            sect.content = memoryview(text)
             sect.virtual_address = reladdr
-            sect.characteristics = parse_as_lief_pe_permission(
-                permission, "code")
+            sect.characteristics = parse_as_lief_pe_permission(permission, "code")
             sections["text"] = pe.add_section(sect, PE.SECTION_TYPES.TEXT)
 
         elif name == ".data":
             # .data is also sure to exist
-            sect.content = b"\x00"
+            sect.content = memoryview(b"\x00")
             sect.virtual_address = reladdr
-            sect.characteristics = parse_as_lief_pe_permission(
-                permission, "udata")
+            sect.characteristics = parse_as_lief_pe_permission(permission, "udata")
             sections["data"] = pe.add_section(sect, PE.SECTION_TYPES.DATA)
 
         reladdr += size
@@ -102,11 +87,9 @@ def build_pe_executable(text: bytes, memory_layout: List[MemorySection], arch: A
     pe.header.add_characteristic(PE.HEADER_CHARACTERISTICS.EXECUTABLE_IMAGE)
     pe.header.add_characteristic(PE.HEADER_CHARACTERISTICS.DEBUG_STRIPPED)
     if is_x64:
-        pe.header.add_characteristic(
-            PE.HEADER_CHARACTERISTICS.LARGE_ADDRESS_AWARE)
+        pe.header.add_characteristic(PE.HEADER_CHARACTERISTICS.LARGE_ADDRESS_AWARE)
     else:
-        pe.header.add_characteristic(
-            PE.HEADER_CHARACTERISTICS.CHARA_32BIT_MACHINE)
+        pe.header.add_characteristic(PE.HEADER_CHARACTERISTICS.CHARA_32BIT_MACHINE)
 
     # fixing pe optional header
     pe.optional_header.addressof_entrypoint = sections["text"].virtual_address
@@ -115,7 +98,7 @@ def build_pe_executable(text: bytes, memory_layout: List[MemorySection], arch: A
     pe.optional_header.major_subsystem_version = 0x05
     pe.optional_header.minor_subsystem_version = 0x02
     pe.optional_header.major_linker_version = 0x02
-    pe.optional_header.minor_linker_version = 0x1e
+    pe.optional_header.minor_linker_version = 0x1E
     pe.optional_header.remove(PE.DLL_CHARACTERISTICS.NX_COMPAT)
     pe.optional_header.add(PE.DLL_CHARACTERISTICS.NO_SEH)
     # pe.add_library("ntdll.dll")
@@ -129,7 +112,8 @@ def build_pe_executable(text: bytes, memory_layout: List[MemorySection], arch: A
     return outfile
 
 
-def build_elf_executable(asm_code: bytes, memory_layout: List[MemorySection], arch: Architecture) -> str:
-    """
-    """
+def build_elf_executable(
+    asm_code: bytes, memory_layout: List[MemorySection], arch: Architecture
+) -> str:
+    """ """
     raise NotImplementedError("ELF generation will be implemented soon")
diff --git a/cemu/img/new_logo.png b/cemu/img/new_logo.png
new file mode 100644
index 0000000..6b12ff8
Binary files /dev/null and b/cemu/img/new_logo.png differ
diff --git a/cemu/memory.py b/cemu/memory.py
index 0ffd107..04042cf 100644
--- a/cemu/memory.py
+++ b/cemu/memory.py
@@ -1,90 +1,251 @@
-import pathlib
 import enum
+import pathlib
+from typing import Optional
+
+import unicorn
 
-from typing import Optional, Tuple
+MemoryLayoutEntryType = tuple[str, int, int, str, Optional[pathlib.Path]]
 
-import cemu.core
+MEMORY_FIELD_SEPARATOR = "|"
 
-MemoryLayoutEntryType = Tuple[str, int, int, str, Optional[pathlib.Path]]
 
+class MemoryPermission(enum.IntFlag):
+    """Abstract class for memory permission."""
 
-class Permission(enum.Flag):
-    """Representation of Linux permission."""
     NONE = 0
     EXECUTE = 1
     WRITE = 2
     READ = 4
     ALL = EXECUTE | READ | WRITE
 
-    def __str__(self) -> str:
-        perm_str = ""
-        perm_str += "r" if self & Permission.READ else "-"
-        perm_str += "w" if self & Permission.WRITE else "-"
-        perm_str += "x" if self & Permission.EXECUTE else "-"
-        return perm_str
-
+    def to_string(self) -> str:
+        perm = []
+        if self & MemoryPermission.READ:
+            perm.append("read")
+        if self & MemoryPermission.WRITE:
+            perm.append("write")
+        if self & MemoryPermission.EXECUTE:
+            perm.append("exec")
+        return MEMORY_FIELD_SEPARATOR.join(perm)
+
+    __str__ = to_string
+
+    @property
+    def readable(self):
+        return self & MemoryPermission.READ
+
+    r = readable
+
+    @property
+    def writable(self):
+        return self & MemoryPermission.WRITE
+
+    w = writable
+
+    @property
+    def executable(self):
+        return self & MemoryPermission.EXECUTE
+
+    x = executable
+
+    @staticmethod
+    def from_string(perm: str) -> "MemoryPermission":
+        perm_obj = MemoryPermission.NONE
+
+        for block in map(str.lower, perm.split(MEMORY_FIELD_SEPARATOR)):
+            block = block.strip()
+            if block == "read":
+                perm_obj |= MemoryPermission.READ
+            elif block == "write":
+                perm_obj |= MemoryPermission.WRITE
+            elif block == "exec":
+                perm_obj |= MemoryPermission.EXECUTE
+            else:
+                raise ValueError(f"Unsupported value {block}")
+        return perm_obj
+
+    @staticmethod
+    def from_windows(protect: int) -> "MemoryPermission":
+        """Converts region Protection values to a MemoryProtection
+
+        Args:
+            protect (int): _description_
+
+        Raises:
+            ValueError: if there's no match
+            NotImplementedError: for PAGE_GUARD
+
+        Returns:
+            MemoryPermission: _description_
+        """
+        match protect:
+            case 0x01:
+                # PAGE_NOACCESS
+                return MemoryPermission.NONE
+            case 0x02:
+                # PAGE_READONLY
+                return MemoryPermission.READ
+            case 0x04:
+                # PAGE_READWRITE
+                return MemoryPermission.READ | MemoryPermission.WRITE
+            case 0x08:
+                # PAGE_WRITECOPY
+                return MemoryPermission.READ | MemoryPermission.WRITE
+            case 0x10:
+                # PAGE_EXECUTE
+                return MemoryPermission.EXECUTE
+            case 0x20:
+                # PAGE_EXECUTE_READ
+                return MemoryPermission.EXECUTE | MemoryPermission.READ
+            case 0x40:
+                # PAGE_EXECUTE_READWRITE
+                return (
+                    MemoryPermission.READ
+                    | MemoryPermission.WRITE
+                    | MemoryPermission.EXECUTE
+                )
+            case 0x80:
+                # PAGE_EXECUTE_WRITECOPY
+                return (
+                    MemoryPermission.READ
+                    | MemoryPermission.WRITE
+                    | MemoryPermission.EXECUTE
+                )
+            case 0x100:
+                raise NotImplementedError("PAGE_GUARD is not implemented")
+
+        raise ValueError(f"Invalid value {protect}")
+
+    def as_windows_str(self) -> str:
+        """Converts region Protection values to a MemoryProtection
+
+        Args:
+            protect (int): _description_
+
+        Raises:
+            ValueError: if there's no match
+            NotImplementedError: for PAGE_GUARD
+
+        Returns:
+            MemoryPermission: _description_
+        """
+
+        if self == MemoryPermission.NONE:
+            return "PAGE_NOACCESS"
+
+        if self == MemoryPermission.READ:
+            return "PAGE_READONLY"
+
+        if self == MemoryPermission.READ | MemoryPermission.WRITE:
+            return "PAGE_READWRITE"
+
+        if self == MemoryPermission.EXECUTE:
+            return "PAGE_EXECUTE"
+
+        if self == MemoryPermission.EXECUTE | MemoryPermission.READ:
+            return "PAGE_EXECUTE_READ"
+
+        if self == MemoryPermission.ALL:
+            return "PAGE_EXECUTE_READWRITE"
+
+        raise ValueError(f"Cannot convert value {int(self)}")
+
+    def unicorn(self) -> int:
+        """Get the integer value as used by `unicorn`
+
+        Returns:
+            int: the protection, as a value understandable for unicorn
+        """
+        if self == MemoryPermission.NONE:
+            return unicorn.UC_PROT_NONE
+
+        unicorn_permission = 0
+        if self & MemoryPermission.READ:
+            unicorn_permission |= unicorn.UC_PROT_READ
+
+        if self & MemoryPermission.WRITE:
+            unicorn_permission |= unicorn.UC_PROT_WRITE
+
+        if self & MemoryPermission.EXECUTE:
+            unicorn_permission |= unicorn.UC_PROT_EXEC
+
+        return unicorn_permission
 
-class MemoryPermission:
 
-    def __init__(self, perm: str):
-        self.r, self.w, self.x = False, False, False
-        self.parse(perm)
+class MemorySection:
+    """Abstraction class for memory section"""
+
+    def __init__(
+        self,
+        name: str,
+        addr: int,
+        size: int,
+        perm: str,
+        data_file: Optional[pathlib.Path] = None,
+    ):
+        if addr < 0 or addr >= 2**64:
+            raise ValueError("address")
+
+        if len(name.strip()) == 0:
+            raise ValueError("name")
+
+        if size < 0:
+            raise ValueError("size")
+
+        self.name = name.strip().lower()
+        self.address = addr
+        self.size = size
+        self.permission = MemoryPermission.from_string(perm)
+        self.file_source = data_file if data_file and data_file.is_file() else None
         return
 
-    def parse(self, perm: str) -> None:
-        if perm.strip().lower() == "all":
-            self.r = self.w = self.x = True
-            return
-
-        for p in perm.split("|"):
-            p = p.strip().lower()
-            if p == "read":
-                self.r = True
-            elif p == "write":
-                self.w = True
-            elif p == "exec":
-                self.x = True
-        return
+    @property
+    def end(self):
+        return self.address + self.size - 1
 
-    def __str__(self) -> str:
-        m = []
-        if self.r:
-            m.append("READ")
-        if self.w:
-            m.append("WRITE")
-        if self.x:
-            m.append("EXEC")
-        return "|".join(m)
-
-    def short(self) -> str:
-        m = []
-        if self.r:
-            m.append("r")
-        if self.w:
-            m.append("w")
-        if self.x:
-            m.append("x")
-        return "".join(m)
+    @property
+    def content(self) -> Optional[bytes]:
+        """Get the content from source file. This data will be used to populate the memory region
 
+        Raises:
+            AttributeError: if the file content exceeds the region size
 
-class MemorySection:
-    def __init__(self, name: str, addr: int, size: int, perm: str, data_file: Optional[pathlib.Path] = None):
-        self.name = name
-        self.address = addr
-        self.size = size
-        self.permission = MemoryPermission(perm)
-        self.file_source = None
-        self.content = None
-        if data_file and data_file.is_file():
-            self.file_source = data_file
-            self.content = data_file.open("rb").read()
-        return
+        Returns:
+            bytes: the file content
+        """
+        if not self.file_source:
+            return None
 
-    def __str__(self) -> str:
-        return "[0x{:x}-0x{:x}] {:s} ({:s})".format(self.address, self.address+self.size-1, self.name, self.permission.short())
+        data = self.file_source.open("rb").read()
+        if len(data) > self.size:
+            raise AttributeError("Insufficient space")
+        return data
 
-    def export(self) -> MemoryLayoutEntryType:
-        return (self.name, self.address, self.size, str(self.permission), self.file_source)
+    def __str__(self) -> str:
+        return (
+            f"MemorySection([{self.address:#x}-{self.end:#x}], "
+            f"name='{self.name:s}', "
+            f"permission={str(self.permission)})"
+        )
 
     def __contains__(self, addr: int) -> bool:
-        return self.address <= addr < self.address + self.size
+        """`in` operator overload
+
+        Args:
+            addr (int): _description_
+
+        Returns:
+            bool: _description_
+        """
+        return self.address <= addr <= self.end
+
+    def overlaps(self, other: "MemorySection") -> bool:
+        """Indicates whether this memory section overlaps another
+
+        Args:
+            other (MemorySection): _description_
+
+        Returns:
+            bool: _description_
+        """
+        return self.address in other or other.address in self
diff --git a/cemu/os.py b/cemu/os.py
new file mode 100644
index 0000000..c716557
--- /dev/null
+++ b/cemu/os.py
@@ -0,0 +1,26 @@
+from dataclasses import dataclass
+from typing import Union
+
+
+@dataclass
+class OperatingSystem:
+    name: str
+
+    def __str__(self):
+        return self.name
+
+    def __fspath__(self):
+        return str(self).lower()
+
+    def __eq__(self, __value: Union[str, "OperatingSystem"]) -> bool:
+        if isinstance(__value, str):
+            return str(self).lower() == __value.lower()
+
+        if isinstance(__value, OperatingSystem):
+            return self.name == __value.name
+
+        raise ValueError
+
+
+Linux = OperatingSystem("Linux")
+Windows = OperatingSystem("Windows")
diff --git a/cemu/plugins/__init__.py b/cemu/plugins/__init__.py
index d9ef418..03c95fb 100644
--- a/cemu/plugins/__init__.py
+++ b/cemu/plugins/__init__.py
@@ -8,6 +8,7 @@
 from PyQt6.QtWidgets import QDockWidget
 
 import cemu.core
+
 if TYPE_CHECKING:
     from cemu.ui.main import CEmuWindow
 from cemu.const import PLUGINS_PATH
@@ -25,6 +26,7 @@ def __str__(self):
 
     @property
     def rootWindow(self) -> CEmuWindow:
+        assert isinstance(cemu.core.context, cemu.core.GlobalGuiContext)
         return cemu.core.context.root
 
 
diff --git a/cemu/plugins/pyconsole/console.py b/cemu/plugins/pyconsole/console.py
index db89504..dc82072 100644
--- a/cemu/plugins/pyconsole/console.py
+++ b/cemu/plugins/pyconsole/console.py
@@ -8,6 +8,7 @@
 
 import cemu.core
 import cemu.emulator
+from cemu import const
 
 CONSOLE_DEFAULT_PROMPT = ">>> "
 
@@ -40,8 +41,10 @@ def __init__(self, parent, motd=""):
 
         self.setWordWrapMode(QTextOption.WrapMode.WrapAnywhere)
         self.setUndoRedoEnabled(False)
-        self.document().setDefaultFont(QFont("monospace", 10, QFont.Weight.Normal))
-        self.setText(self.startup_message+os.linesep)
+        self.document().setDefaultFont(
+            QFont(const.DEFAULT_FONT, const.DEFAULT_FONT_SIZE, QFont.Weight.Normal)
+        )
+        self.setText(self.startup_message + os.linesep)
         self.newPrompt()
         return
 
@@ -51,7 +54,7 @@ def appendPlainText(self, message):
         return
 
     def newPrompt(self):
-        prompt = '.' * len(self.prompt) if self.construct else self.prompt
+        prompt = "." * len(self.prompt) if self.construct else self.prompt
         self.appendPlainText(prompt)
         self.moveCursor(QTextCursor.MoveOperation.End)
         return
@@ -65,11 +68,13 @@ def setCommand(self, command):
         if self.getCommand() == command:
             return
         self.moveCursor(QTextCursor.MoveOperation.End)
-        self.moveCursor(QTextCursor.MoveOperation.StartOfLine,
-                        QTextCursor.MoveMode.KeepAnchor)
+        self.moveCursor(
+            QTextCursor.MoveOperation.StartOfLine, QTextCursor.MoveMode.KeepAnchor
+        )
         for i in range(len(self.prompt)):
-            self.moveCursor(QTextCursor.MoveOperation.Right,
-                            QTextCursor.MoveMode.KeepAnchor)
+            self.moveCursor(
+                QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor
+            )
         self.textCursor().removeSelectedText()
         self.textCursor().insertText(command)
         self.moveCursor(QTextCursor.MoveOperation.End)
@@ -84,11 +89,11 @@ def getConstruct(self, command):
                 self.construct = []
                 return ret_val
             else:
-                return ''
+                return ""
         else:
-            if command and command[-1] == (':'):
+            if command and command[-1] == (":"):
                 self.construct.append(command)
-                return ''
+                return ""
             else:
                 return command
 
@@ -103,7 +108,7 @@ def getPrevHistoryEntry(self):
         if self.history:
             self.history_index = max(0, self.history_index - 1)
             return self.history[self.history_index]
-        return ''
+        return ""
 
     def getNextHistoryEntry(self):
         if self.history:
@@ -111,7 +116,7 @@ def getNextHistoryEntry(self):
             self.history_index = min(hist_len, self.history_index + 1)
             if self.history_index < hist_len:
                 return self.history[self.history_index]
-        return ''
+        return ""
 
     def get_current_position(self):
         return self.textCursor().columnNumber() - len(self.prompt)
@@ -137,8 +142,7 @@ def run_command(self):
 
                 result = eval(command, self.__locales, self.__globals)
                 if result is not None:
-                    self.appendPlainText(
-                        f"{result}{os.linesep}".format(result))
+                    self.appendPlainText(f"{result}{os.linesep}".format(result))
 
             except SyntaxError:
                 exec(command, self.__locales, self.__globals)
@@ -150,8 +154,7 @@ def run_command(self):
                 traceback_lines = traceback.format_exc().splitlines()
                 for i in (3, 2, 1, -1):
                     traceback_lines.pop(i)
-                self.appendPlainText(os.linesep.join(
-                    traceback_lines)+os.linesep)
+                self.appendPlainText(os.linesep.join(traceback_lines) + os.linesep)
 
             finally:
                 sys.stdout = old_stdout
@@ -197,7 +200,10 @@ def keyPressEvent(self, event):
             self.setCommand(self.getNextHistoryEntry())
             return
 
-        if event.key() == Qt.Key.Key_D and event.modifiers() == Qt.KeyboardModifier.ControlModifier:
+        if (
+            event.key() == Qt.Key.Key_D
+            and event.modifiers() == Qt.KeyboardModifier.ControlModifier
+        ):
             self.close()
 
         super(PythonConsole, self).keyPressEvent(event)
@@ -213,4 +219,4 @@ def vm(self):
 
     @property
     def arch(self):
-        return self.emu.arch
+        return cemu.core.context.architecture
diff --git a/cemu/plugins/scratchboard.py b/cemu/plugins/scratchboard.py
index 4fabc5b..0bd3506 100644
--- a/cemu/plugins/scratchboard.py
+++ b/cemu/plugins/scratchboard.py
@@ -1,19 +1,11 @@
 from typing import Optional
-from cemu.ui.main import CEmuWindow
-from PyQt6.QtWidgets import (
-    QFrame,
-    QLabel,
-    QVBoxLayout,
-    QTextEdit,
-    QWidget,
-    QDockWidget,
-)
 
-from PyQt6.QtGui import(
-    QFont,
-)
+from PyQt6.QtGui import QFont
+from PyQt6.QtWidgets import QDockWidget, QFrame, QTextEdit, QVBoxLayout
 
+from cemu.log import error
 from cemu.ui.highlighter import Highlighter
+from cemu.ui.main import CEmuWindow
 
 
 class ScratchboardWidget(QDockWidget):
@@ -21,9 +13,9 @@ def __init__(self, parent: CEmuWindow, *args, **kwargs):
         super(ScratchboardWidget, self).__init__("Scratchboard", parent)
         self.parent = parent
         self.title = "Scratchboard"
-        layout = QVBoxLayout()
+        QVBoxLayout()
         self.__editor = QTextEdit()
-        self.__editor.setFont(QFont('Courier', 11))
+        self.__editor.setFont(QFont("Courier", 11))
         self.__editor.setFrameStyle(QFrame.Shape.Panel | QFrame.Shape.NoFrame)
         self.__highlighter = Highlighter(self.__editor, "rest")
         self.setWidget(self.__editor)
@@ -34,5 +26,5 @@ def register(parent: CEmuWindow) -> Optional[QDockWidget]:
     try:
         return ScratchboardWidget(parent)
     except Exception as e:
-        log(f"Failed to register 'ScratchboardWidget': {e}")
+        error(f"Failed to register 'ScratchboardWidget': {e}")
         return None
diff --git a/cemu/settings.py b/cemu/settings.py
index 3884a38..931f77d 100644
--- a/cemu/settings.py
+++ b/cemu/settings.py
@@ -7,7 +7,6 @@
 
 
 class Settings:
-
     def __init__(self, *args, **kwargs):
         self.__config: Optional[configparser.ConfigParser] = None
         self.__config_filename = cemu.const.CONFIG_FILEPATH
@@ -23,6 +22,7 @@ def set(self, section: str, key: str, value: Any) -> None:
         """
         Store a setting
         """
+        assert self.__config
         if not self.__config.has_section(section):
             self.__config.add_section(section)
 
@@ -33,24 +33,28 @@ def get(self, section: str, key: str, default=None) -> Any:
         """
         Retrieve a setting
         """
+        assert self.__config
         return self.__config.get(section, key, fallback=default)
 
     def getint(self, section: str, key: str, default=0) -> int:
         """
         Retrieve an integer setting
         """
+        assert self.__config
         return self.__config.getint(section, key, fallback=default)
 
     def getboolean(self, section: str, key: str, default=False) -> bool:
         """
         Retrieve a boolean setting
         """
+        assert self.__config
         return self.__config.getboolean(section, key, fallback=default)
 
     def save(self) -> None:
         """
         Save the settings to disk
         """
+        assert self.__config
         with open(self.__config_filename, "w") as fd:
             self.__config.write(fd)
         dbg(f"Settings saved to '{self.__config_filename}'")
@@ -72,7 +76,7 @@ def __create_default_config_file(self) -> None:
         """
         Deploy a new config file as ~/.cemu.ini
         """
-        with self.__config_filename.open('w') as cfg:
+        with self.__config_filename.open("w") as cfg:
             cfg.write(cemu.const.TEMPLATE_CONFIG.open().read())
         return
 
@@ -80,4 +84,5 @@ def __contains__(self, key: str) -> bool:
         """
         Check if a config key exists
         """
+        assert self.__config
         return key in self.__config
diff --git a/cemu/shortcuts.py b/cemu/shortcuts.py
index 5dcf633..72baea5 100644
--- a/cemu/shortcuts.py
+++ b/cemu/shortcuts.py
@@ -4,42 +4,43 @@
 
 
 class ShortcutManager:
-
     def __init__(self, *args, **kwargs):
-        self._defaults = {
+        self._defaults: dict[str, tuple[str, str]] = {
+            # fmt: off
             # menubar
-            "exit_application":   ["Alt+F4", "Exit the application"],
-            "generate_asm_file":   ["", "Save the content as a compilable Assembly file."],
-            "generate_c_file":     ["", "Save the content as a compilable C file."],
-            "save_as_binary":      ["Ctrl+N", "Save the content of the raw binary pane in a file."],
-            "save_as_asm":         ["Ctrl+S", "Save the content of the assembly pane in a file."],
-            "load_binary":         ["Ctrl+B", "Load a raw binary file."],
-            "load_assembly":       ["Ctrl+O", "Load an assembly file."],
-            "shortcut_popup":      ["Ctrl+P", "Show the Shortcut bindings"],
-            "about_popup":         ["", "Generic information about CEMU"],
-            "generate_pe_exe":     ["", "Build a valid Windows PE executable"],
-            "generate_elf_exe":    ["", "Build a valid Linux ELF executable"],
-
-            "toggle_focus_mode":   ["Ctrl+F", "Toggle focus mode"],
+            "exit_application":    ("Alt+F4", "Exit the application"),
+            "generate_asm_file":   ("", "Save the content as a compilable Assembly file."),
+            "generate_c_file":     ("", "Save the content as a compilable C file."),
+            "save_as_binary":      ("Ctrl+N", "Save the content of the raw binary pane in a file."),
+            "save_as_asm":         ("Ctrl+S", "Save the content of the assembly pane in a file."),
+            "load_binary":         ("Ctrl+B", "Load a raw binary file."),
+            "load_assembly":       ("Ctrl+O", "Load an assembly file."),
+            "shortcut_popup":      ("Ctrl+P", "Show the Shortcut bindings"),
+            "about_popup":         ("", "Generic information about CEMU"),
+            "generate_pe_exe":     ("", "Build a valid Windows PE executable"),
+            "generate_elf_exe":    ("", "Build a valid Linux ELF executable"),
+            "toggle_focus_mode":   ("Ctrl+F", "Toggle focus mode"),
 
             # emulator
-            "emulator_check":      ["Alt+C", "Check the assembly code from the assembly pane"],
-            "emulator_run_all":    ["Alt+R", "Check and run the assembly code"],
-            "emulator_step":       ["Alt+S", "Starts emulation by stepping into it"],
-            "emulator_stop":       ["Alt+X", "Stop emulation"],
+            "emulator_check":      ("Alt+C", "Check the assembly code from the assembly pane"),
+            "emulator_run_all":    ("Alt+R", "Check and run the assembly code"),
+            "emulator_step":       ("Alt+S", "Starts emulation by stepping into it"),
+            "emulator_stop":       ("Alt+X", "Stop emulation"),
+            "emulator_reset":      ("", "Reset emulator"),
+            # fmt: on
         }
 
-        self._config = {}
+        self._config: dict[str, tuple[str, str]] = {}
         self.load()
         return
 
-    def shortcut(self, attr):
+    def shortcut(self, attr: str) -> str:
         return self._config[attr][0]
 
-    def description(self, attr):
+    def description(self, attr) -> str:
         return self._config[attr][1]
 
-    def load(self):
+    def load(self) -> bool:
         """
         Load the shortcuts dict from either the config file if the value exists, or
         the defaults
@@ -47,9 +48,8 @@ def load(self):
         settings = cemu.core.context.settings
         for key in self._defaults:
             default_shortcut, description = self._defaults[key]
-            value = settings.get(
-                SHORTCUT_CONFIG_SECTION_NAME, key, default_shortcut)
+            value = settings.get(SHORTCUT_CONFIG_SECTION_NAME, key, default_shortcut)
             settings.set(SHORTCUT_CONFIG_SECTION_NAME, key, value)
-            self._config[key] = [value, description]
+            self._config[key] = (value, description)
 
         return True
diff --git a/cemu/syscalls/aarch64.csv b/cemu/syscalls/linux/aarch64.csv
similarity index 100%
rename from cemu/syscalls/aarch64.csv
rename to cemu/syscalls/linux/aarch64.csv
diff --git a/cemu/syscalls/arm-thumb.csv b/cemu/syscalls/linux/arm-thumb.csv
similarity index 100%
rename from cemu/syscalls/arm-thumb.csv
rename to cemu/syscalls/linux/arm-thumb.csv
diff --git a/cemu/syscalls/arm.csv b/cemu/syscalls/linux/arm.csv
similarity index 100%
rename from cemu/syscalls/arm.csv
rename to cemu/syscalls/linux/arm.csv
diff --git a/cemu/syscalls/mips.csv b/cemu/syscalls/linux/mips.csv
similarity index 100%
rename from cemu/syscalls/mips.csv
rename to cemu/syscalls/linux/mips.csv
diff --git a/cemu/syscalls/mips64.csv b/cemu/syscalls/linux/mips64.csv
similarity index 100%
rename from cemu/syscalls/mips64.csv
rename to cemu/syscalls/linux/mips64.csv
diff --git a/cemu/syscalls/sparc.csv b/cemu/syscalls/linux/sparc.csv
similarity index 100%
rename from cemu/syscalls/sparc.csv
rename to cemu/syscalls/linux/sparc.csv
diff --git a/cemu/syscalls/x86-64.csv b/cemu/syscalls/linux/x86-64.csv
similarity index 100%
rename from cemu/syscalls/x86-64.csv
rename to cemu/syscalls/linux/x86-64.csv
diff --git a/cemu/syscalls/x86.csv b/cemu/syscalls/linux/x86.csv
similarity index 100%
rename from cemu/syscalls/x86.csv
rename to cemu/syscalls/linux/x86.csv
diff --git a/cemu/templates/template.asm b/cemu/templates/linux/template.asm
similarity index 100%
rename from cemu/templates/template.asm
rename to cemu/templates/linux/template.asm
diff --git a/cemu/templates/template.c b/cemu/templates/linux/template.c
similarity index 100%
rename from cemu/templates/template.c
rename to cemu/templates/linux/template.c
diff --git a/cemu/ui/codeeditor.py b/cemu/ui/codeeditor.py
index b1bda55..92d79b8 100644
--- a/cemu/ui/codeeditor.py
+++ b/cemu/ui/codeeditor.py
@@ -1,27 +1,45 @@
 from __future__ import annotations
 
+import os
 import typing
 
 from PyQt6.QtCore import Qt, QVariant
 from PyQt6.QtGui import QFont, QTextFormat
-from PyQt6.QtWidgets import (QDockWidget, QFrame, QHBoxLayout, QLabel,
-                             QTextEdit, QVBoxLayout, QWidget)
+from PyQt6.QtWidgets import (
+    QDockWidget,
+    QFrame,
+    QHBoxLayout,
+    QLabel,
+    QTextEdit,
+    QVBoxLayout,
+    QWidget,
+)
 
 import cemu.core
-from cemu.const import COMMENT_MARKER
+from cemu.const import (
+    COMMENT_MARKER,
+    DEFAULT_ASSEMBLY_VIEW_FONT,
+    DEFAULT_ASSEMBLY_VIEW_FONT_SIZE,
+    DEFAULT_CODE_VIEW_FONT,
+    DEFAULT_CODE_VIEW_FONT_SIZE,
+)
+from cemu.log import error
 
 if typing.TYPE_CHECKING:
     from cemu.ui.main import CEmuWindow
 
-from ..utils import assemble, get_cursor_position
+from ..utils import assemble
 from .highlighter import Highlighter
+from .utils import get_cursor_position
 
 
 class CodeEdit(QTextEdit):
     def __init__(self, parent):
         super(CodeEdit, self).__init__(parent)
         self.cursorPositionChanged.connect(self.UpdateHighlightedLine)
-        self.setFont(QFont('Courier', 11))
+        self.setFont(
+            QFont(DEFAULT_CODE_VIEW_FONT, pointSize=DEFAULT_CODE_VIEW_FONT_SIZE)
+        )
         self.setFrameStyle(QFrame.Shape.Panel | QFrame.Shape.NoFrame)
         self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
         return
@@ -30,47 +48,79 @@ def UpdateHighlightedLine(self):
         selection = QTextEdit.ExtraSelection()
         selection.format.setBackground(self.palette().alternateBase())
         selection.format.setProperty(
-            QTextFormat.Property.FullWidthSelection, QVariant(True))
+            QTextFormat.Property.FullWidthSelection, QVariant(True)
+        )
         selection.cursor = self.textCursor()
         selection.cursor.clearSelection()
-        self.setExtraSelections([selection, ])
+        self.setExtraSelections(
+            [
+                selection,
+            ]
+        )
         return
 
 
 class AssemblyView(QTextEdit):
-    def __init__(self, parent, editor):
+    def __init__(self, parent, editor: CodeEdit):
         super().__init__(parent)
         self.rootWindow = parent.rootWindow
         self.setReadOnly(True)
-        self.setFont(QFont('Courier', 11))
+        self.setFont(QFont(DEFAULT_ASSEMBLY_VIEW_FONT, DEFAULT_ASSEMBLY_VIEW_FONT_SIZE))
         self.setFixedWidth(140)
         self.setFrameStyle(QFrame.Shape.Panel | QFrame.Shape.NoFrame)
         self.setStyleSheet("background-color: rgb(211, 211, 211);")
-        self.__editor = editor
-        self.__editor.textChanged.connect(self.__update_assembly_code)
+        self.editor = editor
+        self.editor.textChanged.connect(self.update_assembly_code_pane)
+        self.__last_assembly_error_msg = ""
         return
 
-    def __update_assembly_code(self):
-        lines = self.__editor.toPlainText().splitlines()
-        nb_lines = len(lines)
-        bytecode_lines = ["", ]*nb_lines
-        old_code = ""
-
-        for idx in range(nb_lines):
-            curline = lines[idx].strip()
-            if not curline or curline.startswith(COMMENT_MARKER):
-                bytecode_lines[idx] = "\n"
-                continue
-
-            asm = "\n".join(lines[:idx+1])
-            code, _ = assemble(asm)
-            if len(code) > len(old_code):
-                new_code = code[len(old_code):]
-                new_line = " ".join(["%.02x" % x for x in new_code])
-                old_code = code
-                bytecode_lines[idx] = new_line
-
-        self.setText("\n".join(bytecode_lines))
+    def update_assembly_code_pane(self):
+        #
+        # Execute only on line return
+        #
+        text: str = self.editor.toPlainText()
+        cur: int = self.editor.textCursor().position()
+        if cur < 1 or text[cur - 1] != os.linesep:
+            return
+
+        #
+        # TODO add switch "Live Preview"
+        #
+        pane_width = self.width() // 10
+        lines: list[str] = text.splitlines()
+        bytecode_lines: list[str] = [
+            "",
+        ] * pane_width
+        assembly_failed_lines: list[int] = []
+
+        for i in range(len(lines)):
+            try:
+                line = lines[i].strip()
+                if not line:
+                    bytecode_lines[i] = ""
+                    continue
+
+                insns = assemble(line)
+                insn = insns[0]
+                bytecode = " ".join([f"{b:02x}" for b in insn.bytes])
+                if len(bytecode) > (pane_width - 3):
+                    bytecode_lines[i] = bytecode[: pane_width - 3] + "..."
+                else:
+                    bytecode_lines[i] = bytecode
+
+            except Exception:
+                assembly_failed_lines.append(i)
+                bytecode_lines[i] = ""
+
+        if assembly_failed_lines:
+            msg = (
+                f"Failed to assemble lines {', '.join(map(str, assembly_failed_lines))}"
+            )
+            if msg != self.__last_assembly_error_msg:
+                error(msg)
+                self.__last_assembly_error_msg = msg
+
+        self.setText(os.linesep.join(bytecode_lines))
         return
 
 
@@ -113,6 +163,7 @@ def __init__(self, parent, *args, **kwargs):
         self.code_editor_frame = CodeEditorFrame(self)
         self.editor: CodeEdit = self.code_editor_frame.editor
         self.editor.cursorPositionChanged.connect(self.onCursorPositionChanged)
+        self.editor.textChanged.connect(self.onUpdateText)
         layout = QVBoxLayout()
         layout.setSpacing(0)
         self.widget_title_label = QLabel("Code (Line:1 Column:1)")
@@ -123,14 +174,17 @@ def __init__(self, parent, *args, **kwargs):
         self.setWidget(widget)
         return
 
+    def onUpdateText(self):
+        cemu.core.context.emulator.codelines = self.getCleanContent()
+        return
+
     def onCursorPositionChanged(self):
         self.UpdateTitleLabel()
         return
 
     def UpdateTitleLabel(self):
         row_num, col_num = get_cursor_position(self.editor)
-        self.widget_title_label.setText(
-            f"Code (Line:{row_num+1} Column:{col_num+1})")
+        self.widget_title_label.setText(f"Code (Line:{row_num+1} Column:{col_num+1})")
         return
 
     def getCleanContent(self) -> str:
@@ -163,14 +217,25 @@ def parse_syscalls(lines: list[str]) -> list[str]:
                 parsed.append(line)
             return parsed
 
+        def remove_empty_lines(lines: list[str]) -> list[str]:
+            #
+            # Bug in keystone: miscount of number of assembled insn
+            #
+            # Repro:
+            # >>> ks = keystone.Ks(keystone.KS_ARCH_X86, keystone.KS_MODE_64)
+            # >>> ks.asm("inc rax\n\n\ndec rbx", as_bytes=True)
+            # (b'H\xff\xc0H\xff\xcb', 4)
+            #
+            # TODO report
+            #
+            return [x for x in lines if x]
+
         code: str = self.editor.toPlainText()
         if not code:
             return ""
 
         lines: list[str] = code.splitlines()
-
         lines = remove_comments(lines)
-
         lines = parse_syscalls(lines)
-
-        return '\n'.join(lines)
+        lines = remove_empty_lines(lines)
+        return os.linesep.join(lines)
diff --git a/cemu/ui/command.py b/cemu/ui/command.py
index 16a0d6b..1309601 100644
--- a/cemu/ui/command.py
+++ b/cemu/ui/command.py
@@ -1,92 +1,95 @@
 from __future__ import annotations
 
-from typing import TYPE_CHECKING
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Callable
 
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtWidgets import (QDockWidget, QHBoxLayout, QMessageBox,
-                             QPushButton, QWidget)
+from PyQt6.QtWidgets import QDockWidget, QHBoxLayout, QPushButton, QWidget
 
-from cemu.log import dbg, error, info
+import cemu.core
+from cemu import utils
+from cemu.emulator import Emulator, EmulatorState
+from cemu.log import dbg, info
+from cemu.ui.utils import PopupType, popup
 
 if TYPE_CHECKING:
     from cemu.ui.main import CEmuWindow
 
-import cemu.core
 
-from ..emulator import Emulator, EmulatorState
+@dataclass
+class CommandButton:
+    label: str
+    on_click: Callable
+    shortcut: str
 
 
 class CommandWidget(QDockWidget):
-
-    setCommandButtonsForRunningSignal = pyqtSignal()
-    setCommandButtonsForStepRunningSignal = pyqtSignal()
-    setCommandButtonsForStopSignal = pyqtSignal()
-
     def __init__(self, parent: CEmuWindow, *args, **kwargs):
         super().__init__("Control Panel", parent)
-        self.emulator: Emulator = cemu.core.context.emulator
+        self.root = parent
         sc = parent.shortcuts
+
+        #
+        # Layout setup
+        #
         layout = QHBoxLayout()
         layout.addStretch(1)
 
-        self.__runButton = QPushButton("Run all code")
-        self.__runButton.clicked.connect(self.onClickRunAll)
-        self.__runButton.setShortcut(sc.shortcut("emulator_run_all"))
-
-        self.__stepButton = QPushButton("Step to Next")
-        self.__stepButton.clicked.connect(self.onClickStepNext)
-        self.__stepButton.setShortcut(sc.shortcut("emulator_step"))
-
-        self.__stopButton = QPushButton("Stop")
-        self.__stopButton.setShortcut(sc.shortcut("emulator_stop"))
-        self.__stopButton.clicked.connect(self.onClickStop)
-        self.__stopButton.setDisabled(True)
-
-        self.__checkAsmButton = QPushButton("Check assembly")
-        self.__checkAsmButton.setShortcut(sc.shortcut("emulator_check"))
-        self.__checkAsmButton.clicked.connect(self.onClickCheckCode)
-
-        layout.addWidget(self.__runButton)
-        layout.addWidget(self.__stepButton)
-        layout.addWidget(self.__stopButton)
-        layout.addWidget(self.__checkAsmButton)
+        buttons = {
+            "run": CommandButton(" ⏩ ", self.onClickRunAll, "emulator_run_all"),
+            "step": CommandButton(" ⏯️ ", self.onClickStepNext, "emulator_step"),
+            "stop": CommandButton(" ⏹️ ", self.onClickStop, "emulator_stop"),
+            "reset": CommandButton(" 🔄️ ", self.onClickReset, "emulator_reset"),
+            "check": CommandButton(" ✅ ", self.onClickCheckCode, "emulator_check"),
+        }
+
+        self.buttons: dict[str, QPushButton] = {}
+        for name in buttons:
+            button = QPushButton(buttons[name].label)
+            button.clicked.connect(buttons[name].on_click)
+            button.setShortcut(sc.shortcut(buttons[name].shortcut))
+            self.buttons[name] = button
+            layout.addWidget(self.buttons[name])
 
         widget = QWidget(self)
         widget.setLayout(layout)
         self.setWidget(widget)
 
-        parent.signals["setCommandButtonsRunState"] = self.setCommandButtonsForRunningSignal
-        self.setCommandButtonsForRunningSignal.connect(
-            self.onSignalEmulationRun)
-
-        parent.signals["setCommandButtonsStepRunState"] = self.setCommandButtonsForStepRunningSignal
-        self.setCommandButtonsForStepRunningSignal.connect(
-            self.onSignalEmulationStepRun)
+        #
+        # Emulator state callback
+        #
+        self.emulator: Emulator = cemu.core.context.emulator
 
-        parent.signals["setCommandButtonStopState"] = self.setCommandButtonsForStopSignal
-        self.setCommandButtonsForStopSignal.connect(self.onEmulationStop)
+        self.emulator.add_state_change_cb(
+            EmulatorState.NOT_RUNNING, self.onNotRunningUpdateCommandButtons
+        )
+        self.emulator.add_state_change_cb(
+            EmulatorState.RUNNING, self.onRunningUpdateCommandButtons
+        )
+        self.emulator.add_state_change_cb(
+            EmulatorState.IDLE, self.onIdleUpdateCommandButtons
+        )
+        self.emulator.add_state_change_cb(
+            EmulatorState.FINISHED, self.onFinishedUpdateCommandButtons
+        )
         return
 
-    def __run(self) -> None:
+    def onClickRunAll(self) -> None:
         """
-        Internal method that starts the emulation
+        Command to run the emulation from $pc until the end of
+        the code provided
         """
-
-        if not self.emulator.is_running:
-            if not self.load_emulation_context():
-                error("An error occured when loading context")
-                return
-
-        self.emulator.run()
+        self.emulator.use_step_mode = False
+        # self.emulator.stop_now = False
+        self.emulator.set(EmulatorState.RUNNING)
         return
 
     def onClickStepNext(self) -> None:
         """
-        Command to step into the next instruction.
+        Command to step into the next instruction. Enable the stepping mode in the emulator, then run.
         """
         self.emulator.use_step_mode = True
-        self.emulator.stop_now = False
-        self.__run()
+        # self.emulator.stop_now = False
+        self.emulator.set(EmulatorState.RUNNING)
         return
 
     def onClickStop(self):
@@ -94,82 +97,75 @@ def onClickStop(self):
         Callback function for "Stop execution"
         """
         if not self.emulator.is_running:
-            error("Emulator is not running...")
+            popup("Emulator is not running...")
             return
 
-        info("Stopping emulation...")
-        self.emulator.set_vm_state(EmulatorState.FINISHED)
-        ("Emulation context has stopped")
+        dbg("Stopping emulation...")
+        self.emulator.set(EmulatorState.FINISHED)
+        info("Emulation context has stopped")
         return
 
-    def onClickRunAll(self) -> None:
+    def onClickReset(self):
         """
-        Command to run the emulation from $pc until the end of
-        the code provided
+        Callback function for "Reset execution"
         """
-        self.emulator.use_step_mode = False
-        self.emulator.stop_now = False
-        self.__run()
+        if self.emulator.state != EmulatorState.FINISHED:
+            raise Exception("Should never be here")
+
+        dbg("Tearing down VM on user demand...")
+        self.emulator.set(EmulatorState.TEARDOWN)
+        info("Emulation context reset")
         return
 
-    def onClickCheckCode(self) -> bool:
+    def onClickCheckCode(self) -> None:
         """
         Callback function for performing a syntaxic check of the code in the code pane.
         """
-        code = cemu.core.context.root.get_codeview_content()
-        if self.emulator.assemble_code(code, False):
-            msg = "Your code is syntaxically valid."
-            popup = QMessageBox.information
-            is_valid = True
-        else:
-            msg = "Some errors were found in your code, check the logs..."
-            popup = QMessageBox.warning
-            is_valid = False
-
-        popup(self, "Checking assembly code syntax...", msg)
-        return is_valid
-
-    def onSignalEmulationRun(self) -> None:
-        """
-        Signal callback called when notifying the start of emulation
-        """
-        # enable the "Stop" button, disable the other ones
-        self.__stopButton.setDisabled(False)
-        self.__runButton.setDisabled(True)
-        self.__stepButton.setDisabled(True)
+        code = self.root.get_codeview_content()
+        try:
+            if not code:
+                raise ValueError("Empty code")
+
+            insns = utils.assemble(code)
+            title = "Success"
+            msg = f"Your code is syntaxically valid, {len(insns)} instructions compiled"
+            popup_style = PopupType.Information
+        except Exception as e:
+            title = "Some errors were found in your code."
+            msg = f"{e.__class__.__name__}: {str(e)}"
+            popup_style = PopupType.Error
+
+        popup(msg, popup_style, title=title)
         return
 
-    def onSignalEmulationStepRun(self) -> None:
-        """
-        Signal callback called when notifying the step run of emulation
-        """
-        # everything is enabled
-        self.__stopButton.setDisabled(False)
-        self.__runButton.setDisabled(False)
-        self.__stepButton.setDisabled(False)
+    def onRunningUpdateCommandButtons(self) -> None:
+        """On `running` state, disable all buttons"""
+        self.buttons["run"].setDisabled(True)
+        self.buttons["step"].setDisabled(True)
+        self.buttons["stop"].setDisabled(True)
+        self.buttons["reset"].setDisabled(True)
         return
 
-    def onEmulationStop(self) -> None:
-        """
-        Signal callback called when notifying the end of emulation
-        """
-        # enable the "Stop" button, disable the other ones
-        self.__stopButton.setDisabled(True)
-        self.__stepButton.setDisabled(False)
-        self.__runButton.setDisabled(False)
+    def onNotRunningUpdateCommandButtons(self) -> None:
+        """On `not running` state, we can do all but stop"""
+        self.buttons["run"].setDisabled(False)
+        self.buttons["step"].setDisabled(False)
+        self.buttons["stop"].setDisabled(True)
+        self.buttons["reset"].setDisabled(True)
         return
 
-    def load_emulation_context(self) -> bool:
-        """
-        Prepare the emulation context based on the current context from the UI
-        """
-        self.emulator.reset()
-        self.emulator.create_new_vm()
-        code = cemu.core.context.root.get_codeview_content()
-        memory_layout = cemu.core.context.root.get_memory_layout()
-        regs = cemu.core.context.root.get_registers()
-
-        return self.emulator.populate_memory(memory_layout) and \
-            self.emulator.assemble_code(code) and \
-            self.emulator.populate_registers(regs) and \
-            self.emulator.map_code()
+    def onIdleUpdateCommandButtons(self) -> None:
+        """On `idle` state, we can either step more, run all or stop"""
+        self.buttons["stop"].setDisabled(False)
+        self.buttons["step"].setDisabled(False)
+        self.buttons["run"].setDisabled(False)
+        self.buttons["reset"].setDisabled(True)
+        return
+
+    def onFinishedUpdateCommandButtons(self):
+        """In the finished state, we can only completely reset the emulation context"""
+        self.buttons["run"].setDisabled(True)
+        self.buttons["step"].setDisabled(True)
+        self.buttons["stop"].setDisabled(True)
+        self.buttons["reset"].setDisabled(False)
+        return
diff --git a/cemu/ui/highlighter.py b/cemu/ui/highlighter.py
index c0f5147..4191369 100644
--- a/cemu/ui/highlighter.py
+++ b/cemu/ui/highlighter.py
@@ -13,15 +13,15 @@ def __init__(self, *args, **kwargs):
         self.styles: dict[str, QTextCharFormat] = {}
         for token, style in self.style:
             qtf = QTextCharFormat()
-            if style['color']:
-                qtf.setForeground(self.hex2QColor(style['color']))
-            if style['bgcolor']:
-                qtf.setBackground(self.hex2QColor(style['bgcolor']))
-            if style['bold']:
+            if style["color"]:
+                qtf.setForeground(self.hex2QColor(style["color"]))
+            if style["bgcolor"]:
+                qtf.setBackground(self.hex2QColor(style["bgcolor"]))
+            if style["bold"]:
                 qtf.setFontWeight(QFont.Weight.Bold)
-            if style['italic']:
+            if style["italic"]:
                 qtf.setFontItalic(True)
-            if style['underline']:
+            if style["underline"]:
                 qtf.setFontUnderline(True)
             self.styles[str(token)] = qtf
         return
@@ -35,9 +35,14 @@ def hex2QColor(self, c):
     def format(self, tokensource, outfile):
         self.data = []
         for ttype, value in tokensource:
-            l = len(value)
-            t = str(ttype)
-            self.data.extend([self.styles[t], ]*l)
+            value_width = len(value)
+            type_as_str = str(ttype)
+            self.data.extend(
+                [
+                    self.styles[type_as_str],
+                ]
+                * value_width
+            )
         return
 
 
@@ -52,11 +57,11 @@ def __init__(self, parent, mode):
     def highlightBlock(self, text):
         cb = self.currentBlock()
         p = cb.position()
-        text = self.document().toPlainText() + '\n'
+        text = self.document().toPlainText() + "\n"
         highlight(text, self.lexer, self.formatter)
         for i in range(len(text)):
             try:
-                self.setFormat(i, 1, self.formatter.data[p+i])
+                self.setFormat(i, 1, self.formatter.data[p + i])
             except IndexError:
                 pass
         self.tstamp = time.time()
diff --git a/cemu/ui/log.py b/cemu/ui/log.py
index c5db7f1..917d221 100644
--- a/cemu/ui/log.py
+++ b/cemu/ui/log.py
@@ -7,9 +7,8 @@
 class LogWidget(QDockWidget):
     def __init__(self, parent, *args, **kwargs):
         super(LogWidget, self).__init__("Cemu Logs", parent)
-        self.parent = self.parentWidget()
         self.__editor = QTextEdit()
-        self.__editor.setFont(QFont('Courier', 11))
+        self.__editor.setFont(QFont("Courier", 11))
         self.__editor.setFrameStyle(QFrame.Shape.Panel | QFrame.Shape.NoFrame)
         self.__editor.setReadOnly(True)
         self.setWidget(self.__editor)
diff --git a/cemu/ui/main.py b/cemu/ui/main.py
index 84802f6..49a650e 100644
--- a/cemu/ui/main.py
+++ b/cemu/ui/main.py
@@ -1,26 +1,46 @@
 import functools
+import os
 import pathlib
 import tempfile
 from typing import Callable, Optional
 
+import unicorn
 from PyQt6.QtCore import QFileInfo, QSettings, Qt
 from PyQt6.QtGui import QAction, QIcon
-from PyQt6.QtWidgets import (QApplication, QDockWidget, QFileDialog,
-                             QGridLayout, QLabel, QMainWindow, QMenu,
-                             QMessageBox, QWidget)
+from PyQt6.QtWidgets import (
+    QApplication,
+    QDockWidget,
+    QFileDialog,
+    QGridLayout,
+    QLabel,
+    QMainWindow,
+    QMenu,
+    QMessageBox,
+    QWidget,
+)
 
 import cemu.core
+import cemu.exports
 import cemu.plugins
 import cemu.utils
+from cemu.emulator import EmulatorState
 from cemu.log import dbg, error, info, ok, warn
+from cemu.ui.utils import popup
 
 from ..arch import Architecture, Architectures, Endianness
-from ..const import (AUTHOR, CONFIG_FILEPATH, EXAMPLE_PATH, HOME, ISSUE_LINK,
-                     TEMPLATE_PATH, TITLE, URL, VERSION)
-from ..exports import build_elf_executable, build_pe_executable
+from ..const import (
+    AUTHOR,
+    CONFIG_FILEPATH,
+    EXAMPLE_PATH,
+    HOME,
+    ISSUE_LINK,
+    TEMPLATE_PATH,
+    TITLE,
+    URL,
+    VERSION,
+)
 from ..memory import MemorySection
 from ..shortcuts import ShortcutManager
-from ..utils import assemble
 from .codeeditor import CodeWidget
 from .command import CommandWidget
 from .log import LogWidget
@@ -30,9 +50,11 @@
 
 
 class CEmuWindow(QMainWindow):
-
     def __init__(self, app: QApplication, *args, **kwargs):
         super(CEmuWindow, self).__init__()
+        assert cemu.core.context is not None
+        assert isinstance(cemu.core.context, cemu.core.GlobalGuiContext)
+
         self.rootWindow = self
         self.__app = app
         self.recentFileActions: list[QAction] = []
@@ -40,6 +62,10 @@ def __init__(self, app: QApplication, *args, **kwargs):
         self.archActions = {}
         self.signals = {}
         self.current_file: Optional[pathlib.Path] = None
+        self.__background_emulator_thread = EmulationRunner()
+        cemu.core.context.emulator.set_threaded_runner(
+            self.__background_emulator_thread
+        )
 
         self.shortcuts = ShortcutManager()
 
@@ -58,16 +84,11 @@ def __init__(self, app: QApplication, *args, **kwargs):
         self.__dockable_widgets.append(self.__codeWidget)
         self.setCentralWidget(self.__codeWidget)
 
-        self.addDockWidget(
-            Qt.DockWidgetArea.LeftDockWidgetArea, self.__regsWidget)
-        self.addDockWidget(
-            Qt.DockWidgetArea.LeftDockWidgetArea, self.__mapWidget)
-        self.addDockWidget(
-            Qt.DockWidgetArea.RightDockWidgetArea, self.__memWidget)
-        self.addDockWidget(
-            Qt.DockWidgetArea.RightDockWidgetArea, self.__cmdWidget)
-        self.addDockWidget(
-            Qt.DockWidgetArea.BottomDockWidgetArea, self.__logWidget)
+        self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.__regsWidget)
+        self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.__mapWidget)
+        self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.__memWidget)
+        self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.__cmdWidget)
+        self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.__logWidget)
 
         # ... and the extra plugins too
         self.LoadExtraPlugins()
@@ -79,9 +100,25 @@ def __init__(self, app: QApplication, *args, **kwargs):
         # set up on-quit hooks
         self.__app.aboutToQuit.connect(self.onAboutToQuit)
 
+        # register the callbacks for the emulator
+        emu = cemu.core.context.emulator
+        emu.add_state_change_cb(
+            EmulatorState.NOT_RUNNING, self.update_layout_not_running
+        )
+        emu.add_state_change_cb(EmulatorState.RUNNING, self.update_layout_running)
+        emu.add_state_change_cb(EmulatorState.IDLE, self.update_layout_step_running)
+        emu.add_state_change_cb(
+            EmulatorState.FINISHED, self.update_layout_step_finished
+        )
+
         # show everything
         self.show()
         dbg("Main window initialized")
+
+        #
+        # set the emulator to a new context
+        #
+        emu.reset()
         return
 
     def __del__(self):
@@ -119,10 +156,8 @@ def LoadExtraPlugins(self) -> int:
         return nb_added
 
     def setMainWindowProperty(self) -> None:
-        width = cemu.core.context.settings.getint(
-            "Global", "WindowWidth", 1600)
-        heigth = cemu.core.context.settings.getint(
-            "Global", "WindowHeigth", 800)
+        width = cemu.core.context.settings.getint("Global", "WindowWidth", 1600)
+        heigth = cemu.core.context.settings.getint("Global", "WindowHeigth", 800)
         self.resize(width, heigth)
         self.refreshWindowTitle()
 
@@ -133,12 +168,18 @@ def setMainWindowProperty(self) -> None:
         self.move(frame_geometry.topLeft())
 
         # apply the style
-        style = cemu.core.context.settings.get(
-            "Theme", "QtStyle", "Cleanlooks")
+        style = cemu.core.context.settings.get("Theme", "QtStyle", "Cleanlooks")
         self.__app.setStyle(style)
         return
 
-    def addMenuItem(self, title: str, callback: Callable, description: str = "", shortcut: str = "", **kwargs) -> QAction:
+    def addMenuItem(
+        self,
+        title: str,
+        callback: Callable,
+        description: str = "",
+        shortcut: str = "",
+        **kwargs,
+    ) -> QAction:
         """
         Helper function to create a QAction for the menu bar.
         """
@@ -157,24 +198,27 @@ def addMenuItem(self, title: str, callback: Callable, description: str = "", sho
         return action
 
     def setMainWindowMenuBar(self):
-        statusBar = self.statusBar()
+        self.statusBar()
         menubar = self.menuBar()
-        maxRecentFiles = cemu.core.context.settings.getint(
-            "Global", "MaxRecentFiles")
+        maxRecentFiles = cemu.core.context.settings.getint("Global", "MaxRecentFiles")
 
         # Create "File" menu options
         fileMenu = menubar.addMenu("&File")
 
         # "Open File" submenu
-        openAsmAction = self.addMenuItem("Open Assembly Text", self.loadCodeText,
-                                         self.shortcuts.description(
-                                             "load_assembly"),
-                                         self.shortcuts.shortcut("load_assembly"))
-
-        openBinAction = self.addMenuItem("Open Raw Binary", self.loadCodeBin,
-                                         self.shortcuts.description(
-                                             "load_binary"),
-                                         self.shortcuts.shortcut("load_binary"))
+        openAsmAction = self.addMenuItem(
+            "Open Assembly Text",
+            self.loadCodeText,
+            self.shortcuts.description("load_assembly"),
+            self.shortcuts.shortcut("load_assembly"),
+        )
+
+        openBinAction = self.addMenuItem(
+            "Open Raw Binary",
+            self.loadCodeBin,
+            self.shortcuts.description("load_binary"),
+            self.shortcuts.shortcut("load_binary"),
+        )
 
         openDumpAction = self.addMenuItem("Open Dump File", self.loadDumpFile)
 
@@ -188,15 +232,19 @@ def setMainWindowMenuBar(self):
         # "Save As" sub-menu
         saveAsSubMenu = QMenu("Save File", self)
 
-        saveAsmAction = self.addMenuItem("As Assembly", self.saveCodeText,
-                                         self.shortcuts.description(
-                                             "save_as_asm"),
-                                         self.shortcuts.shortcut("save_as_asm"))
+        saveAsmAction = self.addMenuItem(
+            "As Assembly",
+            self.saveCodeText,
+            self.shortcuts.description("save_as_asm"),
+            self.shortcuts.shortcut("save_as_asm"),
+        )
 
-        saveBinAction = self.addMenuItem("As Binary", self.saveCodeBin,
-                                         self.shortcuts.description(
-                                             "save_as_binary"),
-                                         self.shortcuts.shortcut("save_as_binary"))
+        saveBinAction = self.addMenuItem(
+            "As Binary",
+            self.saveCodeBin,
+            self.shortcuts.description("save_as_binary"),
+            self.shortcuts.shortcut("save_as_binary"),
+        )
 
         saveAsSubMenu.addAction(saveAsmAction)
         saveAsSubMenu.addAction(saveBinAction)
@@ -205,25 +253,33 @@ def setMainWindowMenuBar(self):
 
         # "Export" sub-menu
         exportAsSubMenu = QMenu("Export", self)
-        saveCAction = self.addMenuItem("Generate C code", self.saveAsCFile,
-                                       self.shortcuts.description(
-                                           "generate_c_file"),
-                                       self.shortcuts.shortcut("generate_c_file"))
-
-        saveAsAsmAction = self.addMenuItem("Generate Assembly code", self.saveAsAsmFile,
-                                           self.shortcuts.description(
-                                               "generate_asm_file"),
-                                           self.shortcuts.shortcut("generate_asm_file"))
-
-        generatePeAction = self.addMenuItem("Generate PE executable", self.generate_pe,
-                                            self.shortcuts.description(
-                                                "generate_pe_exe"),
-                                            self.shortcuts.shortcut("generate_pe_exe"))
-
-        generateElfAction = self.addMenuItem("Generate ELF executable", self.generate_elf,
-                                             self.shortcuts.description(
-                                                 "generate_elf_exe"),
-                                             self.shortcuts.shortcut("generate_elf_exe"))
+        saveCAction = self.addMenuItem(
+            "Generate C code",
+            self.saveAsCFile,
+            self.shortcuts.description("generate_c_file"),
+            self.shortcuts.shortcut("generate_c_file"),
+        )
+
+        saveAsAsmAction = self.addMenuItem(
+            "Generate Assembly code",
+            self.saveAsAsmFile,
+            self.shortcuts.description("generate_asm_file"),
+            self.shortcuts.shortcut("generate_asm_file"),
+        )
+
+        generatePeAction = self.addMenuItem(
+            "Generate PE executable",
+            self.generate_pe,
+            self.shortcuts.description("generate_pe_exe"),
+            self.shortcuts.shortcut("generate_pe_exe"),
+        )
+
+        generateElfAction = self.addMenuItem(
+            "Generate ELF executable",
+            self.generate_elf,
+            self.shortcuts.description("generate_elf_exe"),
+            self.shortcuts.shortcut("generate_elf_exe"),
+        )
 
         exportAsSubMenu.addAction(saveCAction)
         exportAsSubMenu.addAction(saveAsAsmAction)
@@ -242,8 +298,9 @@ def setMainWindowMenuBar(self):
             action.triggered.connect(self.openRecentFile)
             self.recentFileActions.append(action)
 
-        clearRecentFilesAction = self.addMenuItem("Clear Recent Files", self.clearRecentFiles,
-                                                  "Clear Recent Files", "")
+        clearRecentFilesAction = self.addMenuItem(
+            "Clear Recent Files", self.clearRecentFiles, "Clear Recent Files", ""
+        )
 
         openRecentFilesSubMenu.addActions(self.recentFileActions)
         openRecentFilesSubMenu.addSeparator()
@@ -256,10 +313,12 @@ def setMainWindowMenuBar(self):
 
         fileMenu.addSeparator()
 
-        quitAction = self.addMenuItem("Quit", QApplication.quit,
-                                      self.shortcuts.shortcut(
-                                          "exit_application"),
-                                      self.shortcuts.description("exit_application"))
+        quitAction = self.addMenuItem(
+            "Quit",
+            QApplication.quit,
+            self.shortcuts.shortcut("exit_application"),
+            self.shortcuts.description("exit_application"),
+        )
 
         fileMenu.addAction(quitAction)
 
@@ -274,17 +333,23 @@ def setMainWindowMenuBar(self):
                     self.currentAction = self.archActions[arch.name]
 
                 self.archActions[arch.name].setStatusTip(
-                    f"Change the architecture to '{arch.name}'")
+                    f"Change the architecture to '{arch.name}'"
+                )
                 self.archActions[arch.name].triggered.connect(
-                    functools.partial(self.onUpdateArchitecture, arch))
+                    functools.partial(self.onUpdateArchitecture, arch)
+                )
                 archSubMenu.addAction(self.archActions[arch.name])
 
         # Add the View Window menu bar
         viewWindowsMenu = menubar.addMenu("&View")
-        toggleFocusMode = self.addMenuItem("Toggle Focus Mode", self.toggleFocusMode,
-                                           self.shortcuts.description(
-                                               "toggle_focus_mode"),
-                                           self.shortcuts.shortcut("toggle_focus_mode"), checkable=True, checked=True)
+        toggleFocusMode = self.addMenuItem(
+            "Toggle Focus Mode",
+            self.toggleFocusMode,
+            self.shortcuts.description("toggle_focus_mode"),
+            self.shortcuts.shortcut("toggle_focus_mode"),
+            checkable=True,
+            checked=True,
+        )
         viewWindowsMenu.addAction(toggleFocusMode)
 
         viewWindowsMenu.addSeparator()
@@ -292,18 +357,26 @@ def setMainWindowMenuBar(self):
         for w in self.__dockable_widgets:
             name = w.windowTitle()
             action = self.addMenuItem(
-                name, self.onCheckWindowMenuBarItem, f"Window '{name}'", checkable=True, checked=True)
+                name,
+                self.onCheckWindowMenuBarItem,
+                f"Window '{name}'",
+                checkable=True,
+                checked=True,
+            )
             viewWindowsMenu.addAction(action)
 
         # Add Help menu bar
         helpMenu = menubar.addMenu("&Help")
-        shortcutAction = self.addMenuItem("Shortcuts", self.showShortcutPopup,
-                                          self.shortcuts.description(
-                                              "shortcut_popup"),
-                                          self.shortcuts.shortcut("shortcut_popup"))
+        shortcutAction = self.addMenuItem(
+            "Shortcuts",
+            self.showShortcutPopup,
+            self.shortcuts.description("shortcut_popup"),
+            self.shortcuts.shortcut("shortcut_popup"),
+        )
 
-        aboutAction = self.addMenuItem("About", self.about_popup,
-                                       self.shortcuts.description("about_popup"))
+        aboutAction = self.addMenuItem(
+            "About", self.about_popup, self.shortcuts.description("about_popup")
+        )
 
         helpMenu.addAction(shortcutAction)
         helpMenu.addAction(aboutAction)
@@ -395,7 +468,8 @@ def openRecentFile(self):
 
     def loadCode(self, title, filter, run_disassembler):
         qFile, _ = QFileDialog.getOpenFileName(
-            self, title, str(EXAMPLE_PATH), filter + ";;All files (*.*)")
+            self, title, str(EXAMPLE_PATH), filter + ";;All files (*.*)"
+        )
 
         fpath = pathlib.Path(qFile).resolve().absolute()
         if not fpath.is_file():
@@ -404,10 +478,15 @@ def loadCode(self, title, filter, run_disassembler):
 
         if run_disassembler:
             with tempfile.NamedTemporaryFile("w", suffix=".asm", delete=False) as fd:
-                disassembled_content = cemu.utils.disassemble_file(
-                    fpath).values()
-                fd.write('\n'.join(
-                    [f"{mnemo}, {operands}" for mnemo, operands in disassembled_content]))
+                disassembled_instructions = cemu.utils.disassemble_file(fpath)
+                fd.write(
+                    os.linesep.join(
+                        [
+                            f"{insn.mnemonic}, {insn.operands}"
+                            for insn in disassembled_instructions
+                        ]
+                    )
+                )
                 fpath = pathlib.Path(fd.name)
 
         self.loadFile(fpath)
@@ -429,7 +508,8 @@ def loadDumpFile(self):
     def saveCode(self, title, filter, run_assembler):
         dbg(f"Saving content of '{title}'")
         qFile, _ = QFileDialog().getSaveFileName(
-            self, title, str(HOME), filter=filter + ";;All files (*.*)")
+            self, title, str(HOME), filter=filter + ";;All files (*.*)"
+        )
 
         if not qFile:
             return
@@ -440,10 +520,8 @@ def saveCode(self, title, filter, run_assembler):
 
         if run_assembler:
             raw_assembly = self.get_codeview_content()
-            raw_bytecode, cnt = assemble(raw_assembly)
-            if cnt < 0:
-                error(f"Failed to compile: error at line {-cnt:d}")
-                return
+            insns: list[cemu.utils.Instruction] = cemu.utils.assemble(raw_assembly)
+            raw_bytecode = b"".join([insn.bytes for insn in insns])
             fpath.open("wb").write(raw_bytecode)
         else:
             raw_bytecode = self.get_codeview_content()
@@ -453,69 +531,69 @@ def saveCode(self, title, filter, run_assembler):
         return
 
     def saveCodeText(self):
-        return self.saveCode("Save Assembly Pane As", "Assembly files (*.asm *.s)", False)
+        return self.saveCode(
+            "Save Assembly Pane As", "Assembly files (*.asm *.s)", False
+        )
 
     def saveCodeBin(self):
-        return self.saveCode("Save Raw Binary Pane As", "Raw binary files (*.raw)", True)
+        return self.saveCode(
+            "Save Raw Binary Pane As", "Raw binary files (*.raw)", True
+        )
 
     def saveAsCFile(self):
         template = (TEMPLATE_PATH / "template.c").open("r").read()
-        insns = self.get_codeview_content().splitlines()
-        lines: list[str] = []
-        i = 0
-        for insn in insns:
-            byte, cnt = assemble(insn)
-            if cnt < 0:
-                error("Failed to compile: error at line {:d}".format(-cnt))
-                return
-
-            offset = f"/* {i:#08x} */ "
-            hexes = ", ".join([f"{b:#02x}" for b in bytearray(byte)])
-            comment = f", // {insn}"
-            line = offset + hexes + comment
-            lines.append(line)
-            i += cnt
+        output: list[str] = []
+        lines = self.get_codeview_content().splitlines()
+        insns = cemu.utils.assemble(self.get_codeview_content())
+        for i, insn in enumerate(insns):
+            hexa = ", ".join([f"{b:#02x}" for b in insn.bytes])
+            line = f"/* {i:#08x} */   {hexa}   // {lines[i]}"
+            output.append(line)
 
         with tempfile.NamedTemporaryFile("w", delete=False, suffix=".c") as fd:
             body = template % (
-                cemu.core.context.architecture.name, i, '\n'.join(lines))
+                cemu.core.context.architecture.name,
+                len(insns),
+                os.linesep.join(output),
+            )
             fd.write(body)
             ok(f"Saved as '{fd.name}'")
         return
 
     def generate_pe(self) -> None:
-        """Uses LIEF to create a valid PE from the current session
-        """
+        """Uses LIEF to create a valid PE from the current session"""
         memory_layout = self.get_memory_layout()
         code = self.get_codeview_content()
-        asm_code, nb_insns = assemble(code)
-        if nb_insns > 0:
-            try:
-                pe = build_pe_executable(
-                    asm_code, memory_layout, cemu.core.context.architecture)
-                info("PE file written as '{}'".format(pe))
-            except Exception as e:
-                error("PE creation triggered an exception: {}".format(str(e)))
+        try:
+            insns = cemu.utils.assemble(code)
+            if len(insns) > 0:
+                asm_code = b"".join([x.bytes for x in insns])
+                pe = cemu.exports.build_pe_executable(
+                    asm_code, memory_layout, cemu.core.context.architecture
+                )
+                info(f"PE file written as '{pe}'")
+        except Exception as e:
+            error(f"PE creation triggered an exception: {e}")
         return
 
     def generate_elf(self) -> None:
-        """Uses LIEF to create a valid ELF from the current session
-        """
+        """Uses LIEF to create a valid ELF from the current session"""
         memory_layout = self.get_memory_layout()
         code = self.get_codeview_content()
-        asm_code, nb_insns = assemble(code)
-        if nb_insns > 0:
-            try:
-                outfile = build_elf_executable(
-                    asm_code, memory_layout, cemu.core.context.architecture)
-                ok(f"ELF file written as '{outfile}'")
-            except Exception as e:
-                error(f"ELF creation triggered an exception: {e}")
+        try:
+            insns = cemu.utils.assemble(code)
+            if len(insns) > 0:
+                asm_code = b"".join([x.bytes for x in insns])
+                elf = cemu.exports.build_pe_executable(
+                    asm_code, memory_layout, cemu.core.context.architecture
+                )
+                info(f"ELF file written as '{elf}'")
+        except Exception as e:
+            error(f"ELF creation triggered an exception: {str(e)}")
         return
 
     def saveAsAsmFile(self) -> None:
-        """Write the content of the ASM pane to disk
-        """
+        """Write the content of the ASM pane to disk"""
         template = (TEMPLATE_PATH / "template.asm").open("r").read()
         code = self.get_codeview_content()
 
@@ -525,7 +603,9 @@ def saveAsAsmFile(self) -> None:
             ok(f"Saved as '{fd.name}'")
         return
 
-    def onUpdateArchitecture(self, arch: Architecture, endian: Optional[Endianness] = None) -> None:
+    def onUpdateArchitecture(
+        self, arch: Architecture, endian: Optional[Endianness] = None
+    ) -> None:
         """Callback triggered when there's a change of Architecture in the UI
 
         Args:
@@ -543,8 +623,7 @@ def onUpdateArchitecture(self, arch: Architecture, endian: Optional[Endianness]
         return
 
     def refreshWindowTitle(self) -> None:
-        """Refresh the main window title bar
-        """
+        """Refresh the main window title bar"""
         title = f"{TITLE} ({cemu.core.context.architecture})"
         if self.current_file:
             title += f": {self.current_file.name}"
@@ -552,11 +631,9 @@ def refreshWindowTitle(self) -> None:
         return
 
     def showShortcutPopup(self):
-        """Display a popup with all shortcuts currently defined
-        """
+        """Display a popup with all shortcuts currently defined"""
         msgbox = QMessageBox(self)
-        msgbox.setWindowTitle(
-            "CEMU Shortcuts from: {:s}".format(CONFIG_FILEPATH))
+        msgbox.setWindowTitle("CEMU Shortcuts from: {:s}".format(CONFIG_FILEPATH))
 
         wid = QWidget()
         grid = QGridLayout()
@@ -570,8 +647,8 @@ def showShortcutPopup(self):
             sc, desc = self.shortcuts._config[config_item]
             if not sc:
                 continue
-            grid.addWidget(QLabel(sc), i+1, 0)
-            grid.addWidget(QLabel(desc), i+1, 1)
+            grid.addWidget(QLabel(sc), i + 1, 0)
+            grid.addWidget(QLabel(desc), i + 1, 1)
 
         wid.setMinimumWidth(800)
         wid.setLayout(grid)
@@ -581,8 +658,9 @@ def showShortcutPopup(self):
 
     def about_popup(self):
         templ = (TEMPLATE_PATH / "about.html").open().read()
-        desc = templ.format(author=AUTHOR, version=VERSION,
-                            project_link=URL, issues_link=ISSUE_LINK)
+        desc = templ.format(
+            author=AUTHOR, version=VERSION, project_link=URL, issues_link=ISSUE_LINK
+        )
         msgbox = QMessageBox(self)
         msgbox.setIcon(QMessageBox.Icon.Information)
         msgbox.setWindowTitle("About CEMU")
@@ -593,15 +671,14 @@ def about_popup(self):
         return
 
     def updateRecentFileActions(self, insert_file=None):
-        settings = QSettings('Cemu', 'Recent Files')
-        files = settings.value('recentFileList')
+        settings = QSettings("Cemu", "Recent Files")
+        files = settings.value("recentFileList")
         if files is None:
             # if setting doesn't exist, create it
-            settings.setValue('recentFileList', [])
-            files = settings.value('recentFileList')
+            settings.setValue("recentFileList", [])
+            files = settings.value("recentFileList")
 
-        maxRecentFiles = cemu.core.context.settings.getint(
-            "Default", "MaxRecentFiles")
+        maxRecentFiles = cemu.core.context.settings.getint("Default", "MaxRecentFiles")
 
         if insert_file:
             if insert_file not in files:
@@ -609,7 +686,7 @@ def updateRecentFileActions(self, insert_file=None):
             if len(files) > maxRecentFiles:
                 files = files[0:maxRecentFiles]
 
-            settings.setValue('recentFileList', files)
+            settings.setValue("recentFileList", files)
 
         numRecentFiles = min(len(files), maxRecentFiles)
 
@@ -626,8 +703,8 @@ def updateRecentFileActions(self, insert_file=None):
         return
 
     def clearRecentFiles(self) -> None:
-        settings = QSettings('Cemu', 'Recent Files')
-        settings.setValue('recentFileList', [])
+        settings = QSettings("Cemu", "Recent Files")
+        settings.setValue("recentFileList", [])
         self.updateRecentFileActions()
         return
 
@@ -642,10 +719,78 @@ def get_registers(self) -> dict[str, int]:
         Returns the register widget values as a Dict
         """
         self.__regsWidget.updateGrid()
-        return self.__regsWidget.getRegisterValues()
+        return self.__regsWidget.getRegisterValuesFromGrid()
 
     def get_memory_layout(self) -> list[MemorySection]:
         """
         Returns the memory layout as defined by the __mapWidget values as a structured list.
         """
-        return self.__mapWidget.maps
+        return cemu.core.context.emulator.sections
+
+    def update_layout_not_running(self):
+        self.statusBar().showMessage("Not running")
+        return
+
+    def update_layout_running(self):
+        self.statusBar().showMessage("Running")
+        return
+
+    def update_layout_step_running(self):
+        self.statusBar().showMessage("Idle (Step Mode)")
+        return
+
+    def update_layout_step_finished(self):
+        self.statusBar().showMessage("Finished")
+        return
+
+
+class EmulationRunner:
+    """Rusn an emulation session"""
+
+    def run(self):
+        """
+        Runs the emulation
+        """
+        emu = cemu.core.context.emulator
+        if not emu.vm:
+            error("VM is not ready")
+            return
+
+        if not emu.is_running:
+            error("Emulator is in invalid state")
+            return
+
+        try:
+            start_address = emu.pc() or emu.start_addr
+            start_offset = start_address - emu.start_addr
+
+            #
+            # Determine where to stop
+            #
+            if emu.use_step_mode:
+                insn = emu.next_instruction(emu.code[start_offset:], start_address)
+                end_address = insn.end
+                info(f"Stepping from {start_address:#x} to {end_address:#x}")
+            else:
+                end_address = emu.start_addr + len(emu.code)
+                info(f"Running all from {start_address:#x} to {end_address:#x}")
+
+            with emu.lock:
+                #
+                # Run the emulator, let's go!
+                #
+                emu.vm.emu_start(start_address, end_address)
+
+            #
+            # If the execution is over, mark the state as finished
+            #
+            if emu.pc() == (emu.start_addr + len(emu.code)):
+                emu.set(EmulatorState.FINISHED)
+            else:
+                emu.set(EmulatorState.IDLE)
+
+        except unicorn.unicorn.UcError as e:
+            popup(f"An error occured: {str(e)} at pc={emu.pc():#x}, sp={emu.sp():#x}")
+            emu.set(EmulatorState.FINISHED)
+
+        return
diff --git a/cemu/ui/mapping.py b/cemu/ui/mapping.py
index 5c87544..19c41f7 100644
--- a/cemu/ui/mapping.py
+++ b/cemu/ui/mapping.py
@@ -1,56 +1,61 @@
-import os
-from PyQt6.QtCore import (
-    Qt,
-    QVariant,
-    QStringListModel
-)
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
 
 from PyQt6.QtWidgets import (
-    QVBoxLayout,
-    QTableWidget,
-    QHeaderView,
-    QTableWidgetItem,
+    QCheckBox,
     QDockWidget,
-    QWidget,
-    QTableView,
-    QListView,
-    QPushButton,
     QGridLayout,
     QHBoxLayout,
     QLabel,
     QLineEdit,
     QMessageBox,
-    QCheckBox,
+    QPushButton,
+    QTableWidget,
+    QTableWidgetItem,
+    QVBoxLayout,
+    QWidget,
 )
 
-from cemu.memory import (
-    MemorySection,
-    MemoryLayoutEntryType,
-)
+import cemu.core
+from cemu.emulator import Emulator, EmulatorState
+from cemu.log import error
+from cemu.memory import MemorySection
+from cemu.utils import format_address
 
-from cemu.utils import (
-    ishex,
-)
+from .utils import popup
+
+if TYPE_CHECKING:
+    from cemu.ui.main import CEmuWindow
 
 
-from typing import List, Tuple, Any
+MEMORY_MAP_DEFAULT_LAYOUT: list[MemorySection] = [
+    MemorySection(".text", 0x00004000, 0x1000, "READ|EXEC"),
+    MemorySection(".data", 0x00005000, 0x1000, "READ|WRITE"),
+    MemorySection(".stack", 0x00006000, 0x4000, "READ|WRITE"),
+    MemorySection(".misc", 0x0000A000, 0x1000, "READ|WRITE|EXEC"),
+]
 
 
 class MemoryMappingWidget(QDockWidget):
-    def __init__(self, parent, *args, **kwargs):
-        super(MemoryMappingWidget, self).__init__("Memory map", parent)
+    def __init__(self, parent: CEmuWindow):
+        super().__init__("Memory Map", parent)
+        self.memory_sections = MEMORY_MAP_DEFAULT_LAYOUT
+
         layout = QVBoxLayout()
-        self.__memory_mapping = [
-            MemorySection(".text",  0x00004000, 0x1000, "READ|EXEC"),
-            MemorySection(".data",  0x00005000, 0x1000, "READ|WRITE"),
-            MemorySection(".stack", 0x00006000, 0x4000, "READ|WRITE"),
-            MemorySection(".misc",  0x0000a000, 0x1000, "ALL"),
-        ]
-        self.model = QStringListModel()
-        self.model.setStringList(self.memory_mapping_str)
-        self.view = QListView()
-        self.view.setModel(self.model)
-        layout.addWidget(self.view)
+
+        # the memory layout table
+        self.MemoryMapTableWidget = QTableWidget(0, 4)
+        self.MemoryMapTableWidget.setSelectionBehavior(
+            QTableWidget.SelectionBehavior.SelectRows
+        )
+        self.MemoryMapTableWidget.setHorizontalHeaderLabels(
+            ["Start", "End", "Name", "Permission"]
+        )
+        self.MemoryMapTableWidget.verticalHeader().setVisible(False)
+        layout.addWidget(self.MemoryMapTableWidget)
+
+        # add/remove buttons
         buttons = QHBoxLayout()
         btn_add = QPushButton("Add Section")
         buttons.addWidget(btn_add)
@@ -60,33 +65,78 @@ def __init__(self, parent, *args, **kwargs):
         btn_del.clicked.connect(self.onDeleteSectionButtonClicked)
         buttons.addWidget(btn_del)
         layout.addLayout(buttons)
-        w = QWidget(self)
-        w.setLayout(layout)
-        self.setWidget(w)
+
+        # assign the widget layout
+        widget = QWidget()
+        widget.setLayout(layout)
+        self.setWidget(widget)
+
+        #
+        # Emulator state callback
+        #
+        emu: Emulator = cemu.core.context.emulator
+        emu.add_state_change_cb(
+            EmulatorState.NOT_RUNNING, self.onNotRunningUpdateMemoryMap
+        )
+        emu.add_state_change_cb(
+            EmulatorState.RUNNING, self.onRunningDisableMemoryMapGrid
+        )
+        emu.add_state_change_cb(EmulatorState.IDLE, self.onIdleEnableMemoryMapGrid)
+        emu.add_state_change_cb(
+            EmulatorState.FINISHED, self.onFinishedEnableMemoryMapGrid
+        )
         return
 
-    @property
-    def memory_mapping_str(self) -> None:
-        """
-        Generator for the string view model
-        """
-        for entry in self.__memory_mapping:
-            yield str(entry)
+    def onNotRunningUpdateMemoryMap(self) -> None:
+        self.SynchronizeMemoryMap()
+        return
 
-    def updateView(self) -> bool:
-        """
-        Refresh the view
-        """
-        self.model.setStringList(self.memory_mapping_str)
-        return True
+    def onRunningDisableMemoryMapGrid(self) -> None:
+        self.MemoryMapTableWidget.setDisabled(True)
+        return
 
-    @property
-    def maps(self) -> List[MemorySection]:
-        """
-        Exports the memory mapping to a format usable for Unicorn
-        """
-        self.__maps = self.__memory_mapping[::]
-        return self.__maps
+    def onIdleEnableMemoryMapGrid(self) -> None:
+        self.MemoryMapTableWidget.setDisabled(False)
+        return
+
+    onFinishedEnableMemoryMapGrid = onIdleEnableMemoryMapGrid
+
+    def SynchronizeMemoryMap(self) -> None:
+        #
+        # If unset, use a default layout
+        #
+        if not self.memory_sections:
+            self.memory_sections = MEMORY_MAP_DEFAULT_LAYOUT
+
+        #
+        # Propagate the view change to the emulator
+        #
+        cemu.core.context.emulator.sections = self.memory_sections
+
+        #
+        # Apply the values to the grid
+        #
+        self.UpdateMemoryMapGrid()
+        return
+
+    def UpdateMemoryMapGrid(self) -> None:
+        self.MemoryMapTableWidget.clearContents()
+
+        for idx, section in enumerate(self.memory_sections):
+            self.MemoryMapTableWidget.insertRow(idx)
+            name = QTableWidgetItem(section.name)
+            start_address = QTableWidgetItem(format_address(section.address))
+            end_address = QTableWidgetItem(
+                format_address(section.address + section.size)
+            )
+            permission = QTableWidgetItem(str(section.permission))
+            self.MemoryMapTableWidget.setItem(idx, 0, start_address)
+            self.MemoryMapTableWidget.setItem(idx, 1, end_address)
+            self.MemoryMapTableWidget.setItem(idx, 2, name)
+            self.MemoryMapTableWidget.setItem(idx, 3, permission)
+
+        self.MemoryMapTableWidget.setRowCount(len(self.memory_sections))
+        return
 
     def onAddSectionButtonClicked(self) -> None:
         """
@@ -99,17 +149,15 @@ def onDeleteSectionButtonClicked(self) -> None:
         """
         Callback associated with the click of the "Remove Section" button
         """
-        idx = self.view.currentIndex()
-        line_content = idx.data()
-        for i, sect in enumerate(self.__memory_mapping):
-            if line_content == str(sect):
-                if self.__memory_mapping[i].name in (".text", ".data", ".stack"):
-                    print("cannot delete required section '{}'".format(
-                        self.__memory_mapping[i].name))
-                    break
-                del self.__memory_mapping[i]
-                self.updateView()
-                break
+        selection = self.MemoryMapTableWidget.selectionModel()
+        if not selection.hasSelection():
+            return
+        indexes = [x.row() for x in selection.selectedRows()]
+
+        for idx in range(len(self.memory_sections) - 1, 0, -1):
+            if idx in indexes:
+                del self.memory_sections[idx]
+        self.UpdateMemoryMapGrid()
         return
 
     def add_or_edit_section_popup(self) -> None:
@@ -122,10 +170,10 @@ def add_or_edit_section_popup(self) -> None:
         name = QLabel("Name")
         nameEdit = QLineEdit()
 
-        startAddress = QLabel("Start Address (hex)")
+        startAddress = QLabel("Start Address")
         startAddressEdit = QLineEdit()
 
-        size = QLabel("Size (hex)")
+        size = QLabel("Size")
         sizeEdit = QLineEdit()
 
         perm = QLabel("Permissions")
@@ -157,20 +205,35 @@ def add_or_edit_section_popup(self) -> None:
         msgbox.setLayout(grid)
         msgbox.setGeometry(300, 300, 350, 300)
 
-        msgbox.setWindowTitle("Add/edit section")
+        msgbox.setWindowTitle("Add section")
         layout = msgbox.layout()
         wid.setLayout(grid)
         wid.setMinimumWidth(400)
         layout.addWidget(wid)
 
         msgbox.setStandardButtons(
-            QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel)
+            QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel
+        )
 
         ret = msgbox.exec()
         if ret == QMessageBox.StandardButton.Ok:
-            section_name = nameEdit.text()
-            section_address = int(startAddressEdit.text(), 16)
-            section_size = int(sizeEdit.text(), 16)
+            name = nameEdit.text()
+            address = int(startAddressEdit.text(), 0)
+            size = int(sizeEdit.text(), 0)
+
+            if name in (x.name for x in self.memory_sections):
+                error("section name already exists")
+                return
+
+            memory_set = (
+                set(range(x.address, x.address + x.size)) for x in self.memory_sections
+            )
+            current_set = set(range(address, address + size))
+            for m in memory_set:
+                if len(current_set & m) != 0:
+                    error("memory intersection/overlapping not allowed")
+                    return
+
             section_perm = []
             if perm_read_btn.isChecked():
                 section_perm.append("READ")
@@ -178,8 +241,21 @@ def add_or_edit_section_popup(self) -> None:
                 section_perm.append("WRITE")
             if perm_exec_btn.isChecked():
                 section_perm.append("EXEC")
-            section = MemorySection(
-                section_name, section_address, section_size, "|".join(section_perm))
-            self.__memory_mapping.append(section)
-            self.updateView()
+            try:
+                section = MemorySection(name, address, size, "|".join(section_perm))
+                self.memory_sections.append(section)
+                self.UpdateMemoryMapGrid()
+            except ValueError as ve:
+                popup(f"MemorySection is invalid, reason: Invalid {str(ve)}")
+                return
+
+            if perm_exec_btn.isChecked():
+                section_perm.append("EXEC")
+            try:
+                section = MemorySection(name, address, size, "|".join(section_perm))
+                self.memory_sections.append(section)
+                self.UpdateMemoryMapGrid()
+            except ValueError as ve:
+                popup(f"MemorySection is invalid, reason: Invalid {str(ve)}")
+
         return
diff --git a/cemu/ui/memory.py b/cemu/ui/memory.py
index b8e24bc..11cbaa6 100644
--- a/cemu/ui/memory.py
+++ b/cemu/ui/memory.py
@@ -3,21 +3,30 @@
 from typing import TYPE_CHECKING
 
 import unicorn
-from PyQt6.QtCore import QEvent, QFileInfo, pyqtSignal
 from PyQt6.QtGui import QFont
-from PyQt6.QtWidgets import (QDockWidget, QFrame, QHBoxLayout, QLabel,
-                             QLineEdit, QTextEdit, QVBoxLayout, QWidget)
+from PyQt6.QtWidgets import (
+    QComboBox,
+    QDockWidget,
+    QFrame,
+    QHBoxLayout,
+    QLabel,
+    QLineEdit,
+    QTextEdit,
+    QVBoxLayout,
+    QWidget,
+)
 
 import cemu.core
+from cemu import const, utils
+from cemu.emulator import Emulator, EmulatorState
+from cemu.log import dbg
 
 if TYPE_CHECKING:
     from cemu.ui.main import CEmuWindow
-from cemu.utils import hexdump
 
 
 class MemoryWidget(QDockWidget):
-
-    refreshMemoryEditorSignal = pyqtSignal()
+    # refreshMemoryEditorSignal = pyqtSignal()
 
     def __init__(self, parent: CEmuWindow, *args, **kwargs):
         super(MemoryWidget, self).__init__("Memory Viewer", parent)
@@ -25,48 +34,68 @@ def __init__(self, parent: CEmuWindow, *args, **kwargs):
         title_layout.addWidget(QLabel("Location"))
         self.address = QLineEdit()
         self.address.textChanged.connect(self.updateEditor)
+        self.alignement = QComboBox()
+        self.alignement.addItems(["4", "8", "16"])
+        self.alignement.setCurrentIndex(2)
+        self.alignement.currentIndexChanged.connect(self.updateEditor)
         title_layout.addWidget(self.address)
+        title_layout.addWidget(self.alignement)
         title_widget = QWidget()
         title_widget.setLayout(title_layout)
         title_widget.setMouseTracking(True)
 
         memview_layout = QVBoxLayout()
-        self.__editor = QTextEdit()
-        self.__editor.setFrameStyle(QFrame.Shape.Panel | QFrame.Shape.NoFrame)
-        self.__editor.setFont(QFont('Courier', 10))
-        self.__editor.setReadOnly(True)
+        self.editor = QTextEdit()
+        self.editor.setFrameStyle(QFrame.Shape.Panel | QFrame.Shape.NoFrame)
+        self.editor.setFont(
+            QFont(const.DEFAULT_MEMORY_VIEW_FONT, const.DEFAULT_MEMORY_VIEW_FONT_SIZE)
+        )
+        self.editor.setReadOnly(True)
         memview_layout.addWidget(title_widget)
-        memview_layout.addWidget(self.__editor)
+        memview_layout.addWidget(self.editor)
 
         widget = QWidget(self)
         widget.setLayout(memview_layout)
         self.setWidget(widget)
 
-        # define signals
-        self.refreshMemoryEditorSignal.connect(self.onRefreshMemoryEditor)
-        parent.signals["refreshMemoryEditor"] = self.refreshMemoryEditorSignal
+        #
+        # Emulator state callback
+        #
+        emu: Emulator = cemu.core.context.emulator
+        emu.add_state_change_cb(EmulatorState.IDLE, self.onIdleRefreshMemoryEditor)
+        emu.add_state_change_cb(
+            EmulatorState.FINISHED, self.onFinishedClearMemoryEditor
+        )
+
         return
 
     def updateEditor(self) -> None:
         arch = cemu.core.context.architecture
         emu = cemu.core.context.emulator
         if not emu.vm:
-            self.__editor.setText("VM not initialized")
+            self.editor.setText("VM not initialized")
             return
 
         if not emu.is_running:
-            self.__editor.setText("VM not running")
+            self.editor.setText("VM not running")
             return
 
+        addr: int
+        msg = "Displaying "
+
         value = self.address.text()
         if value.lower().startswith("0x"):
             value = value[2:]
 
         if value.startswith("@"):
             # if the value of the "memory viewer" field starts with @.<section_name>
+            section_name = value[1:]
             try:
-                addr = emu.lookup_map(value[1:])
+                section = emu.find_section(section_name)
+                addr = section.address
+                msg += f"from section name {section_name}"
             except KeyError:
+                self.editor.setText(f"No section named '{section_name}'")
                 return
 
         elif value.startswith("$"):
@@ -75,24 +104,33 @@ def updateEditor(self) -> None:
             if reg_name not in arch.registers:
                 return
             addr = emu.get_register_value(reg_name)
-            if addr is None:
+            if not addr:
                 return
+            msg += f" from register {reg_name}"
 
         else:
             if not value.isdigit():
                 return
             addr = int(value, 16)
+            msg += f" from value {value}"
+
+        dbg(msg + f": {addr:#x}")
+
+        assert isinstance(addr, int)
 
         try:
-            l = 256
-            data = emu.vm.mem_read(addr, l)
-            text = hexdump(data, base=addr)
-            self.__editor.setText(text)
+            alignment = int(self.alignement.currentText())
+            data = emu.vm.mem_read(addr, const.DEFAULT_MEMORY_VIEW_CHUNK_SIZE)
+            self.editor.setText(utils.hexdump(data, alignment, base=addr))
         except unicorn.unicorn.UcError:
-            self.__editor.setText("Cannot read at address %x" % addr)
+            self.editor.setText("Cannot read at address %x" % addr)
 
         return
 
-    def onRefreshMemoryEditor(self) -> None:
+    def onIdleRefreshMemoryEditor(self) -> None:
         self.updateEditor()
         return
+
+    def onFinishedClearMemoryEditor(self) -> None:
+        self.editor.setText("")
+        return
diff --git a/cemu/ui/registers.py b/cemu/ui/registers.py
index 257111b..57097cc 100644
--- a/cemu/ui/registers.py
+++ b/cemu/ui/registers.py
@@ -1,97 +1,122 @@
-from typing import Dict
+from PyQt6.QtCore import Qt
 
+# from PyQt6.QtCore import pyqtSignal
+from PyQt6.QtGui import QColor, QFont
 from PyQt6.QtWidgets import (
-    QVBoxLayout,
-    QHBoxLayout,
+    QDockWidget,
     QTableWidget,
-    QLabel,
     QTableWidgetItem,
+    QVBoxLayout,
     QWidget,
-    QDockWidget,
-)
-
-from PyQt6.QtGui import(
-    QFont,
-    QColor
-)
-
-from PyQt6.QtCore import (
-    Qt,
-    pyqtSignal,
-    QEvent,
 )
 
 import cemu.core
-
-from ..utils import format_address
+from cemu.const import (
+    DEFAULT_REGISTER_VIEW_CHANGED_REGISTER_COLOR,
+    DEFAULT_REGISTER_VIEW_REGISTER_FONT,
+    DEFAULT_REGISTER_VIEW_REGISTER_FONT_SIZE,
+)
+from cemu.emulator import Emulator, EmulatorState
+from cemu.utils import format_address
 
 
 class RegistersWidget(QDockWidget):
-
-    refreshRegisterGridSignal = pyqtSignal()
-
     def __init__(self, parent, *args, **kwargs):
         super(RegistersWidget, self).__init__("Registers", parent)
         self.root = self.parentWidget()
         self.__row_size = 15
         self.__old_register_values = {}
         layout = QVBoxLayout()
-        self.__values = QTableWidget(10, 2)
-        self.__values.horizontalHeader().setStretchLastSection(True)
-        self.__values.setHorizontalHeaderLabels(["Register", "Value"])
-        layout.addWidget(self.__values)
+        self.RegisterTableWidget = QTableWidget(10, 2)
+        self.RegisterTableWidget.horizontalHeader().setStretchLastSection(True)
+        self.RegisterTableWidget.setHorizontalHeaderLabels(["Register", "Value"])
+        self.RegisterTableWidget.verticalHeader().setVisible(False)
+        self.RegisterTableWidget.setColumnWidth(0, 60)
+        layout.addWidget(self.RegisterTableWidget)
+
+        #
+        # Setup the widget layout
+        #
         widget = QWidget()
         widget.setLayout(layout)
         self.setWidget(widget)
-        self.updateGrid()
+        # self.updateGrid()
 
-        # define signals
-        self.refreshRegisterGridSignal.connect(self.onRefreshRegisterGrid)
-        self.parent(
-        ).signals["refreshRegisterGrid"] = self.refreshRegisterGridSignal
+        #
+        # Emulator state callback
+        #
+        emu: Emulator = cemu.core.context.emulator
+        emu.add_state_change_cb(EmulatorState.IDLE, self.onIdleRefreshRegisterGrid)
+        emu.add_state_change_cb(
+            EmulatorState.FINISHED, self.onFinishedRefreshRegisterGrid
+        )
+        emu.add_state_change_cb(EmulatorState.NOT_RUNNING, self.updateGrid)
         return
 
     def updateGrid(self) -> None:
-        """
-        Refresh the grid values from the current values of the
+        """Refresh the grid values from the current values of the
         VM CPU registers
+
         """
-        emu = cemu.core.context.emulator
+        emu: Emulator = cemu.core.context.emulator
         arch = cemu.core.context.architecture
         registers = arch.registers
-        self.__values.setRowCount(len(registers))
+        self.RegisterTableWidget.setRowCount(len(registers))
         for i, reg in enumerate(registers):
-            self.__values.setRowHeight(i, self.__row_size)
+            self.RegisterTableWidget.setRowHeight(i, self.__row_size)
             name = QTableWidgetItem(reg)
             name.setFlags(Qt.ItemFlag.NoItemFlags)
-            val = emu.get_register_value(reg) if emu.vm else 0
+            name.setFont(
+                QFont(
+                    DEFAULT_REGISTER_VIEW_REGISTER_FONT,
+                    DEFAULT_REGISTER_VIEW_REGISTER_FONT_SIZE,
+                )
+            )
+            val = emu.registers[reg]
             old_val = self.__old_register_values.get(reg, 0)
             if type(val) in (int, int):
                 value = format_address(val, arch)
             else:
                 value = str(val)
             value = QTableWidgetItem(value)
+            value.setFont(
+                QFont(
+                    DEFAULT_REGISTER_VIEW_REGISTER_FONT,
+                    DEFAULT_REGISTER_VIEW_REGISTER_FONT_SIZE,
+                )
+            )
             if old_val != val:
                 self.__old_register_values[reg] = val
-                value.setForeground(QColor("red"))
-            value.setFlags(Qt.ItemFlag.ItemIsEnabled |
-                           Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable)
-            self.__values.setItem(i, 0, name)
-            self.__values.setItem(i, 1, value)
+                value.setForeground(
+                    QColor(DEFAULT_REGISTER_VIEW_CHANGED_REGISTER_COLOR)
+                )
+            value.setFlags(
+                Qt.ItemFlag.ItemIsEnabled
+                | Qt.ItemFlag.ItemIsSelectable
+                | Qt.ItemFlag.ItemIsEditable
+            )
+            self.RegisterTableWidget.setItem(i, 0, name)
+            self.RegisterTableWidget.setItem(i, 1, value)
+
+        #
+        # Propagate the change to the emulator
+        #
+        cemu.core.context.emulator.registers = self.getRegisterValuesFromGrid()
         return
 
-    def getRegisterValues(self) -> Dict[str, int]:
-        """
-        Returns the current values of the registers, as shown by the widget grid
-        """
+    def getRegisterValuesFromGrid(self) -> dict[str, int]:
+        """Returns the current values of the registers, as shown by the widget grid"""
         regs = {}
-        arch = cemu.core.context.architecture
-        for i in range(len(arch.registers)):
-            name = self.__values.item(i, 0).text()
-            value = self.__values.item(i, 1).text()
+        registers = cemu.core.context.emulator.registers.keys()
+        for i in range(len(registers)):
+            name = self.RegisterTableWidget.item(i, 0).text()
+            value = self.RegisterTableWidget.item(i, 1).text()
             regs[name] = int(value, 16)
         return regs
 
-    def onRefreshRegisterGrid(self) -> None:
+    def onIdleRefreshRegisterGrid(self) -> None:
         self.updateGrid()
         return
+
+    onNotRunningRefreshRegisterGrid = onIdleRefreshRegisterGrid
+    onFinishedRefreshRegisterGrid = onIdleRefreshRegisterGrid
diff --git a/cemu/ui/utils.py b/cemu/ui/utils.py
new file mode 100644
index 0000000..7348c54
--- /dev/null
+++ b/cemu/ui/utils.py
@@ -0,0 +1,67 @@
+import enum
+import os
+
+from PyQt6.QtWidgets import QMessageBox, QTextEdit
+
+from cemu.log import dbg
+
+
+def get_cursor_row_number(widget: QTextEdit) -> int:
+    """Get the cursor row number from the QTextEdit widget
+
+    Args:
+        widget (QTextEdit): _description_
+
+    Returns:
+        int: _description_
+    """
+    assert isinstance(widget, QTextEdit)
+    pos = widget.textCursor().position()
+    text = widget.toPlainText()
+    return text[:pos].count(os.linesep)
+
+
+def get_cursor_column_number(widget: QTextEdit) -> int:
+    """Get the cursor column number from the QTextEdit widget
+
+    Args:
+        widget (QTextEdit): _description_
+
+    Returns:
+        int: _description_
+    """
+    assert isinstance(widget, QTextEdit)
+    pos = widget.textCursor().position()
+    text = widget.toPlainText()
+    return len(text[:pos].split(os.linesep)[-1])
+
+
+def get_cursor_position(widget: QTextEdit) -> tuple[int, int]:
+    """Returns the position of a cursor like (nb_row, nb_col) from a textedit widget
+
+    Args:
+        widget (QTextEdit): _description_
+
+    Returns:
+        tuple[int, int]: _description_
+    """
+    return (get_cursor_row_number(widget), get_cursor_column_number(widget))
+
+
+class PopupType(enum.IntEnum):
+    Information = 0
+    Error = 1
+
+
+def popup(msg: str, type: PopupType = PopupType.Error, title: str = ""):
+    if type == PopupType.Information:
+        icon = QMessageBox.Icon.Information
+        title = f"Info - {title}"
+    elif type == PopupType.Error:
+        icon = QMessageBox.Icon.Critical
+        title = f"Error - {title}"
+    else:
+        raise ValueError("invalid type")
+
+    dbg(f"{title} - {msg}")
+    QMessageBox(icon, title, msg, buttons=QMessageBox.StandardButton.Discard).exec()
diff --git a/cemu/utils.py b/cemu/utils.py
index 26cc2db..21b4c6c 100644
--- a/cemu/utils.py
+++ b/cemu/utils.py
@@ -2,44 +2,69 @@
 import pathlib
 import random
 import string
-from typing import Optional, Tuple
-
-import capstone
-import keystone
-import unicorn
-from PyQt6.QtWidgets import QTextEdit
+from dataclasses import dataclass
+from typing import Optional, TYPE_CHECKING
 
+if TYPE_CHECKING:
+    import cemu.arch
 import cemu.core
-from cemu.arch import (Architecture, Architectures, Endianness, is_aarch64,
-                       is_arm, is_arm_thumb, is_mips, is_mips64, is_sparc,
-                       is_sparc64, is_x86_16, is_x86_32, is_x86_64)
+import cemu.errors
+import cemu.utils
 from cemu.const import COMMENT_MARKER, PROPERTY_MARKER
 from cemu.log import dbg
 
 DISASSEMBLY_DEFAULT_BASE_ADDRESS = 0x40000
 
 
-def hexdump(source: bytearray, length: int = 0x10, separator: str = ".", show_raw: bool = False, base: int = 0x00) -> str:
-    """
-    Produces a `hexdump` command like output version of the bytearray given.
+def hexdump(
+    source: bytes,
+    alignment: int = 0x10,
+    separator: str = ".",
+    show_raw: bool = False,
+    base: int = 0x00,
+) -> str:
+    """Produces a `hexdump` command like output version of the bytearray given.
+
+    Args:
+        source (bytes): _description_
+        alignment (int, optional): _description_. Defaults to 0x10.
+        separator (str, optional): _description_. Defaults to ".".
+        show_raw (bool, optional): _description_. Defaults to False.
+        base (int, optional): _description_. Defaults to 0x00.
+
+    Returns:
+        str: _description_
     """
     result: list[str] = []
-    for i in range(0, len(source), length):
-        s = source[i:i+length]
-
-        hexa = ' '.join(["%02X" % c for c in s])
-        text = ''.join([chr(c) if 0x20 <= c < 0x7F else separator for c in s])
+    for i in range(0, len(source), alignment):
+        chunk = source[i : i + alignment]
+        hexa = " ".join([f"{c:02X}" for c in chunk])
+        text = "".join([chr(c) if 0x20 <= c < 0x7F else separator for c in chunk])
 
         if show_raw:
             result.append(hexa)
         else:
-            result.append("%#-.*x   %-*s  %s" %
-                          (16, base+i, 3*length, hexa, text))
+            result.append(f"{format_address(base)}  {hexa}  {text}")
 
-    return '\n'.join(result)
+    return os.linesep.join(result)
 
 
-def format_address(addr: int, arch: Architecture) -> str:
+def format_address(addr: int, arch: Optional[cemu.arch.Architecture] = None) -> str:
+    """Format an address to string, aligned to the given architecture
+
+    Args:
+        addr (int): _description_
+        arch (Optional[Architecture], optional): _description_. Defaults to None.
+
+    Raises:
+        ValueError: _description_
+
+    Returns:
+        str: _description_
+    """
+    if arch is None:
+        arch = cemu.core.context.architecture
+
     if arch.ptrsize == 2:
         return f"{addr:#04x}"
     elif arch.ptrsize == 4:
@@ -50,188 +75,89 @@ def format_address(addr: int, arch: Architecture) -> str:
         raise ValueError(f"Invalid value for '{arch.ptrsize=}'")
 
 
-def get_arch_mode(lib: str) -> Tuple[int, int, int]:
+@dataclass
+class Instruction:
+    address: int
+    mnemonic: str
+    operands: str
+    bytes: bytes
 
-    arch = cemu.core.context.architecture
+    @property
+    def size(self):
+        return len(self.bytes)
 
-    # x86
-    if is_x86_16(arch):
-        if lib == "keystone":
-            return (keystone.KS_ARCH_X86, keystone.KS_MODE_16, keystone.KS_MODE_LITTLE_ENDIAN)
-        elif lib == "capstone":
-            return (capstone.CS_ARCH_X86, capstone.CS_MODE_16, capstone.CS_MODE_LITTLE_ENDIAN)
-        elif lib == "unicorn":
-            return (unicorn.UC_ARCH_X86, unicorn.UC_MODE_16, unicorn.UC_MODE_LITTLE_ENDIAN)
-        else:
-            raise ValueError(f"Unknown module '{lib}' for {arch}")
-
-    if is_x86_32(arch):
-        if lib == "keystone":
-            return (keystone.KS_ARCH_X86, keystone.KS_MODE_32, keystone.KS_MODE_LITTLE_ENDIAN)
-        elif lib == "capstone":
-            return (capstone.CS_ARCH_X86, capstone.CS_MODE_32, capstone.CS_MODE_LITTLE_ENDIAN)
-        elif lib == "unicorn":
-            return (unicorn.UC_ARCH_X86, unicorn.UC_MODE_32, unicorn.UC_MODE_LITTLE_ENDIAN)
-        else:
-            raise ValueError(f"Unknown module '{lib}' for {arch}")
-
-    if is_x86_64(arch):
-        if lib == "keystone":
-            return (keystone.KS_ARCH_X86, keystone.KS_MODE_64, keystone.KS_MODE_LITTLE_ENDIAN)
-        elif lib == "capstone":
-            return (capstone.CS_ARCH_X86, capstone.CS_MODE_64, capstone.CS_MODE_LITTLE_ENDIAN)
-        elif lib == "unicorn":
-            return (unicorn.UC_ARCH_X86, unicorn.UC_MODE_64, unicorn.UC_MODE_LITTLE_ENDIAN)
-        else:
-            raise ValueError(f"Unknown module '{lib}' for {arch}")
-
-    # arm
-    if is_arm(arch):
-        if lib == "keystone":
-            return (keystone.KS_ARCH_ARM, keystone.KS_MODE_ARM, keystone.KS_MODE_LITTLE_ENDIAN)
-        elif lib == "capstone":
-            return (capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM, capstone.CS_MODE_LITTLE_ENDIAN)
-        elif lib == "unicorn":
-            return (unicorn.UC_ARCH_ARM, unicorn.UC_MODE_ARM, unicorn.UC_MODE_LITTLE_ENDIAN)
-        else:
-            raise ValueError(f"Unknown module '{lib}' for {arch}")
-
-    if is_arm_thumb(arch):
-        if lib == "keystone":
-            return (keystone.KS_ARCH_ARM, keystone.KS_MODE_THUMB, keystone.KS_MODE_LITTLE_ENDIAN)
-        elif lib == "capstone":
-            return (capstone.CS_ARCH_ARM, capstone.CS_MODE_THUMB, capstone.CS_MODE_LITTLE_ENDIAN)
-        elif lib == "unicorn":
-            return (unicorn.UC_ARCH_ARM, unicorn.UC_MODE_THUMB, unicorn.UC_MODE_LITTLE_ENDIAN)
-        else:
-            raise ValueError(f"Unknown module '{lib}' for {arch}")
-
-    # aarch64
-    if is_aarch64(arch):
-        if lib == "keystone":
-            return (keystone.KS_ARCH_ARM64, 0, keystone.KS_MODE_LITTLE_ENDIAN)
-        elif lib == "capstone":
-            return (capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM, capstone.CS_MODE_LITTLE_ENDIAN)
-        elif lib == "unicorn":
-            return (unicorn.UC_ARCH_ARM64, unicorn.UC_MODE_ARM, unicorn.UC_MODE_LITTLE_ENDIAN)
-        else:
-            raise ValueError(f"Unknown module '{lib}' for {arch}")
-
-    # mips/mips64
-    if is_mips(arch):
-        if arch.endianness == Endianness.LITTLE_ENDIAN:
-            if lib == "keystone":
-                return (keystone.KS_ARCH_MIPS, keystone.KS_MODE_MIPS32, keystone.KS_MODE_LITTLE_ENDIAN)
-            elif lib == "capstone":
-                return (capstone.CS_ARCH_MIPS, capstone.CS_MODE_MIPS32, capstone.CS_MODE_LITTLE_ENDIAN)
-            elif lib == "unicorn":
-                return (unicorn.UC_ARCH_MIPS, unicorn.UC_MODE_MIPS32, unicorn.UC_MODE_LITTLE_ENDIAN)
-            else:
-                raise ValueError(f"Unknown module '{lib}' for {arch}")
-        else:
-            if lib == "keystone":
-                return (keystone.KS_ARCH_MIPS, keystone.KS_MODE_MIPS32, keystone.KS_MODE_BIG_ENDIAN)
-            elif lib == "capstone":
-                return (capstone.CS_ARCH_MIPS, capstone.CS_MODE_MIPS32, capstone.CS_MODE_BIG_ENDIAN)
-            elif lib == "unicorn":
-                return (unicorn.UC_ARCH_MIPS, unicorn.UC_MODE_MIPS32, unicorn.UC_MODE_BIG_ENDIAN)
-            else:
-                raise ValueError(f"Unknown module '{lib}' for {arch}")
-
-    if is_mips64(arch):
-        if arch.endianness == Endianness.LITTLE_ENDIAN:
-            if lib == "keystone":
-                return (keystone.KS_ARCH_MIPS, keystone.KS_MODE_MIPS64, keystone.KS_MODE_LITTLE_ENDIAN)
-            elif lib == "capstone":
-                return (capstone.CS_ARCH_MIPS, capstone.CS_MODE_MIPS64, capstone.CS_MODE_LITTLE_ENDIAN)
-            elif lib == "unicorn":
-                return (unicorn.UC_ARCH_MIPS, unicorn.UC_MODE_MIPS64, unicorn.UC_MODE_LITTLE_ENDIAN)
-            else:
-                raise ValueError(f"Unknown module '{lib}' for {arch}")
-        else:
-            if lib == "keystone":
-                return (keystone.KS_ARCH_MIPS, keystone.KS_MODE_MIPS64, keystone.KS_MODE_BIG_ENDIAN)
-            elif lib == "capstone":
-                return (capstone.CS_ARCH_MIPS, capstone.CS_MODE_MIPS64, capstone.CS_MODE_BIG_ENDIAN)
-            elif lib == "unicorn":
-                return (unicorn.UC_ARCH_MIPS, unicorn.UC_MODE_MIPS64, unicorn.UC_MODE_BIG_ENDIAN)
-            else:
-                raise ValueError(f"Unknown module '{lib}' for {arch}")
-
-    # sparc/sparc64
-    if is_sparc(arch):
-        if lib == "keystone":
-            return (keystone.KS_ARCH_SPARC, keystone.KS_MODE_SPARC32, keystone.KS_MODE_LITTLE_ENDIAN)
-        elif lib == "capstone":
-            return (capstone.CS_ARCH_SPARC, 0, capstone.CS_MODE_LITTLE_ENDIAN)
-        elif lib == "unicorn":
-            return (unicorn.UC_ARCH_SPARC, unicorn.UC_MODE_SPARC32, unicorn.UC_MODE_LITTLE_ENDIAN)
-        else:
-            raise ValueError(f"Unknown module '{lib}' for {arch}")
-    if is_sparc64(arch):
-        if lib == "keystone":
-            return (keystone.KS_ARCH_SPARC, keystone.KS_MODE_SPARC64, keystone.KS_MODE_LITTLE_ENDIAN)
-        elif lib == "capstone":
-            return (capstone.CS_ARCH_SPARC, 0, capstone.CS_MODE_LITTLE_ENDIAN)
-        elif lib == "unicorn":
-            return (unicorn.UC_ARCH_SPARC, unicorn.UC_MODE_SPARC64, unicorn.UC_MODE_LITTLE_ENDIAN)
-        else:
-            raise ValueError(f"Unknown module '{lib}' for {arch}")
+    @property
+    def end(self) -> int:
+        return self.address + self.size
 
-    # default, just throw
-    raise ValueError(f"Unknown module '{lib}' for {arch}")
+    def __str__(self):
+        return f'Instruction({self.address:#x}, "{self.mnemonic} {self.operands}")'
 
 
-def disassemble(raw_data: bytes, count: int = -1, base: int = DISASSEMBLY_DEFAULT_BASE_ADDRESS) -> dict[int, Tuple[str, str]]:
+def disassemble(
+    raw_data: bytes, count: int = -1, base: int = DISASSEMBLY_DEFAULT_BASE_ADDRESS
+) -> list[Instruction]:
     """Disassemble the code given as raw data, with the given architecture.
 
     Args:
         raw_data (bytes): the raw byte code to disassemble
         arch (Architecture): the architecture to use for disassembling
         count (int, optional): the maximum number of instruction to disassemble. Defaults to -1.
-        count (int, optional): the disassembled code base address. Defaults to DISASSEMBLY_DEFAULT_BASE_ADDRESS
+        base (int, optional): the disassembled code base address. Defaults to DISASSEMBLY_DEFAULT_BASE_ADDRESS
 
     Returns:
         str: the text representation of the disassembled code
     """
-    cs_arch, cs_mode, cs_endian = get_arch_mode("capstone")
-    cs = capstone.Cs(cs_arch, cs_mode | cs_endian)
-    insns: dict[int, Tuple[str, str]] = {}
-    for idx, ins in enumerate(cs.disasm(bytes(raw_data), base)):
-        insns[ins.address] = (ins.mnemonic, ins.op_str)
+    arch = cemu.core.context.architecture
+    insns: list[Instruction] = []
+    for idx, ins in enumerate(arch.cs.disasm(raw_data, base)):
+        insn = Instruction(ins.address, ins.mnemonic, ins.op_str, ins.bytes)
+        insns.append(insn)
         if idx == count:
             break
 
+    dbg(f"{insns=}")
     return insns
 
 
-def disassemble_file(fpath: pathlib.Path) -> dict[int, Tuple[str, str]]:
-    with fpath.open('rb') as f:
+def disassemble_file(fpath: pathlib.Path) -> list[Instruction]:
+    with fpath.open("rb") as f:
         return disassemble(f.read())
 
 
-def assemble(asm_code: str, as_bytes: bool = True) -> Tuple[bytes, int]:
+def assemble(
+    code: str, base_address: int = DISASSEMBLY_DEFAULT_BASE_ADDRESS
+) -> list[Instruction]:
     """
     Helper function to assemble code receive in parameter `asm_code` using Keystone.
 
-    @param asm_code : assembly code in bytes (multiple instructions must be separated by ';')
-    @param mode : defines the mode to use Keystone with
-    @return a tuple of bytecodes as bytearray, along with the number of instruction compiled. If failed, the
-    bytearray will be empty, the count of instruction will be the negative number for the faulty line.
+    @param code : assembly code in bytes (multiple instructions must be separated by ';')
+    @param base_address : (opt) the base address to use
+
+    @return a list of Instruction
     """
     arch = cemu.core.context.architecture
-    ks_arch, ks_mode, ks_endian = get_arch_mode("keystone")
-    ks = keystone.Ks(ks_arch, ks_mode | ks_endian)
 
-    try:
-        bytecode, cnt = ks.asm(asm_code, as_bytes=as_bytes)
-        if not bytecode or not cnt:
-            return (b'', 0)
-        bytecode = bytes(bytecode)
-    except keystone.keystone.KsError as kse:
-        return (b'', kse.get_asm_count())
+    #
+    # Compile the entire given code
+    #
+    bytecode, assembled_insn_count = arch.ks.asm(code, as_bytes=True, addr=base_address)
+    if not bytecode or assembled_insn_count == 0:
+        raise cemu.errors.AssemblyException("Not instruction compiled")
+
+    assert isinstance(bytecode, bytes)
+
+    #
+    # Decompile it and return the stuff
+    #
+    insns = disassemble(bytecode, base=base_address)
+    dbg(f"{insns=}")
+    return insns
+
 
-    return (bytecode, cnt)
+def assemble_file(fpath: pathlib.Path) -> list[Instruction]:
+    with fpath.open("r") as f:
+        return assemble(f.read())
 
 
 def ishex(x: str) -> bool:
@@ -240,48 +166,6 @@ def ishex(x: str) -> bool:
     return all([c in string.hexdigits for c in x])
 
 
-def get_cursor_row_number(widget: QTextEdit) -> int:
-    """Get the cursor row number from the QTextEdit widget
-
-    Args:
-        widget (QTextEdit): _description_
-
-    Returns:
-        int: _description_
-    """
-    assert isinstance(widget, QTextEdit)
-    pos = widget.textCursor().position()
-    text = widget.toPlainText()
-    return text[:pos].count(os.linesep)
-
-
-def get_cursor_column_number(widget: QTextEdit) -> int:
-    """Get the cursor column number from the QTextEdit widget
-
-    Args:
-        widget (QTextEdit): _description_
-
-    Returns:
-        int: _description_
-    """
-    assert isinstance(widget, QTextEdit)
-    pos = widget.textCursor().position()
-    text = widget.toPlainText()
-    return len(text[:pos].split(os.linesep)[-1])
-
-
-def get_cursor_position(widget: QTextEdit) -> Tuple[int, int]:
-    """Returns the position of a cursor like (nb_row, nb_col) from a textedit widget
-
-    Args:
-        widget (QTextEdit): _description_
-
-    Returns:
-        Tuple[int, int]: _description_
-    """
-    return (get_cursor_row_number(widget), get_cursor_column_number(widget))
-
-
 def generate_random_string(length: int) -> str:
     """Returns a random string
 
@@ -295,7 +179,9 @@ def generate_random_string(length: int) -> str:
     return "".join(random.choice(charset) for _ in range(length))
 
 
-def get_metadata_from_stream(content: str) -> Optional[Tuple[Architecture, Endianness]]:
+def get_metadata_from_stream(
+    content: str,
+) -> Optional[tuple[cemu.arch.Architecture, cemu.arch.Endianness]]:
     """Parse a file content to automatically extract metadata. Metadata can only be passed in the file
     header, and *must* be a commented line (i.e. starting with `;;; `) followed by the property marker (i.e. `@@@`).
     Both the architecture and endianess *must* be provided
@@ -308,15 +194,15 @@ def get_metadata_from_stream(content: str) -> Optional[Tuple[Architecture, Endia
         content (str): _description_
 
     Returns:
-        Optional[Tuple[str, str]]: _description_
+        Optional[tuple[str, str]]: _description_
 
     Raises:
         KeyError:
             - if an architecture metadata is found, but invalid
             - if an endianess metadata is found, but invalid
     """
-    arch: Optional[Architecture] = None
-    endian: Optional[Endianness] = None
+    arch: Optional[cemu.arch.Architecture] = None
+    endian: Optional[cemu.arch.Endianness] = None
 
     for line in content.splitlines():
         part = line.strip().split()
@@ -333,15 +219,15 @@ def get_metadata_from_stream(content: str) -> Optional[Tuple[Architecture, Endia
         metadata_value = part[2].lower()
 
         if metadata_type == "architecture" and not arch:
-            arch = Architectures.find(metadata_value)
+            arch = cemu.arch.Architectures.find(metadata_value)
             dbg(f"Forcing architecture '{arch}'")
             continue
 
         if metadata_type == "endianness" and not endian:
             if metadata_value == "little":
-                endian = Endianness.LITTLE_ENDIAN
+                endian = cemu.arch.Endianness.LITTLE_ENDIAN
             elif metadata_value == "big":
-                endian = Endianness.BIG_ENDIAN
+                endian = cemu.arch.Endianness.BIG_ENDIAN
             else:
                 continue
             dbg(f"Forcing endianness '{endian}'")
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..d5dcb7d
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,150 @@
+
+#
+# https://packaging.python.org/en/latest/tutorials/packaging-projects
+#
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+
+[project]
+name = "CEmu"
+version = "0.7"
+authors = [{ name = "hugsy", email = "hugsy@blah.cat" }]
+description = "Cemu is a simple assembly/dissembly/emulation IDE that provides an easy Plug-n-Play environment to start playing with many architectures (currently supports x86-{32,64}, ARM, AARCH64, MIPS, MIPS64, SPARC and PPC)."
+readme = "README.md"
+requires-python = ">=3.10"
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "License :: OSI Approved :: MIT License",
+    "Programming Language :: Python :: 3 :: Only",
+    "Programming Language :: Python :: 3.10",
+    "Topic :: Software Development :: Assemblers",
+    "Natural Language :: English",
+    "Environment :: Console",
+]
+
+keywords = [
+    "assembly",
+    "disassembly",
+    "emulation",
+    "x86",
+    "x64",
+    "arm",
+    "aarch64",
+    "mips",
+    "powerpc",
+    "sparc",
+]
+
+dependencies = [
+    "capstone",
+    "unicorn",
+    "keystone-engine",
+    "PyQt6",
+    "Pygments",
+    "lief",
+    "loguru",
+    "prompt_toolkit",
+]
+
+[project.entry-points.console_scripts]
+cemu = "cemu.__main__:main"
+
+[project.entry-points.gui_scripts]
+cemu = "cemu.__main__:main"
+
+[project.urls]
+"Homepage" = "https://github.com/hugsy/cemu"
+"Bug Tracker" = "https://github.com/hugsy/cemu/issues"
+
+[tool.pytest.ini_options]
+log_cli = true
+log_cli_level = "INFO"
+log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
+log_cli_date_format = "%Y-%m-%d %H:%M:%S"
+
+[tool.isort]
+profile = "black"
+
+[tool.ruff]
+select = ["E", "F"]
+ignore = []
+fixable = [
+    "A",
+    "B",
+    "C",
+    "D",
+    "E",
+    "F",
+    "G",
+    "I",
+    "N",
+    "Q",
+    "S",
+    "T",
+    "W",
+    "ANN",
+    "ARG",
+    "BLE",
+    "COM",
+    "DJ",
+    "DTZ",
+    "EM",
+    "ERA",
+    "EXE",
+    "FBT",
+    "ICN",
+    "INP",
+    "ISC",
+    "NPY",
+    "PD",
+    "PGH",
+    "PIE",
+    "PL",
+    "PT",
+    "PTH",
+    "PYI",
+    "RET",
+    "RSE",
+    "RUF",
+    "SIM",
+    "SLF",
+    "TCH",
+    "TID",
+    "TRY",
+    "UP",
+    "YTT",
+]
+unfixable = []
+
+exclude = [
+    ".bzr",
+    ".direnv",
+    ".eggs",
+    ".git",
+    ".git-rewrite",
+    ".hg",
+    ".mypy_cache",
+    ".nox",
+    ".pants.d",
+    ".pytype",
+    ".ruff_cache",
+    ".svn",
+    ".tox",
+    ".venv",
+    "__pypackages__",
+    "_build",
+    "buck-out",
+    "build",
+    "dist",
+    "node_modules",
+    "venv",
+]
+
+line-length = 120
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+target-version = "py310"
+
+[tool.ruff.mccabe]
+max-complexity = 10
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 02579ac..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-capstone==4.0.2
-unicorn==1.0.3
-keystone-engine==0.9.2
-PyQt6==6.3.0
-Pygments==2.12.0
-lief==0.12.1
-loguru==0.6.0
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index b88034e..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[metadata]
-description-file = README.md
diff --git a/setup.py b/setup.py
deleted file mode 100644
index e15354d..0000000
--- a/setup.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from setuptools import find_packages, setup
-
-import cemu.const as const
-
-
-def readme():
-    import io
-    with io.open("README.md", "r", encoding="utf-8") as f:
-        return f.read()
-
-
-def get_required_packages():
-    return [x.strip() for x in open("./requirements.txt", "r").readlines()]
-
-
-setup(
-    name=const.PROGNAME,
-    description=const.DESCRIPTION,
-    long_description=readme(),
-    url=const.URL,
-    download_url=const.RELEASE_LINK,
-    classifiers=[
-        "Development Status :: 4 - Beta",
-        "License :: OSI Approved :: MIT License",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.9",
-        "Topic :: Software Development :: Assemblers",
-        "Natural Language :: English",
-    ],
-    python_requires=">=3.9.0",
-    author=const.AUTHOR,
-    author_email=const.EMAIL,
-    version=const.VERSION,
-    license=const.LICENSE,
-    include_package_data=True,
-    packages=find_packages(),
-    install_requires=get_required_packages(),
-    entry_points={
-        "console_scripts": ["cemu=cemu.__main__:main"],
-    },
-    keywords=[
-        "assembly", "disassembly", "emulation", "x86", "x64", "arm", "aarch64", "mips",
-        "powerpc", "sparc"
-    ],
-)


Test patch:
diff --git a/tests/pytest.ini b/tests/pytest.ini
new file mode 100644
index 0000000..ed023af
--- /dev/null
+++ b/tests/pytest.ini
@@ -0,0 +1,18 @@
+[pytest]
+log_level = DEBUG
+minversion = 6.0
+required_plugins =
+    pytest-benchmark
+    pytest-cov
+    pytest-xdist
+python_functions =
+    test_*
+    time_*
+python_files = *.py
+testpaths =
+    .
+pythonpath =
+    ..
+markers =
+    slow: flag test as slow (deselect with '-m "not slow"')
+    online: flag test as requiring internet to work (deselect with '-m "not online"')
diff --git a/tests/requirements.txt b/tests/requirements.txt
new file mode 100644
index 0000000..ef7798f
--- /dev/null
+++ b/tests/requirements.txt
@@ -0,0 +1,6 @@
+pytest
+pytest-cov
+pytest-xdist
+pytest-benchmark
+pytest-forked
+coverage
diff --git a/tests/test_basic.py b/tests/test_basic.py
new file mode 100644
index 0000000..189e768
--- /dev/null
+++ b/tests/test_basic.py
@@ -0,0 +1,210 @@
+import logging
+import os
+import unittest
+from dataclasses import dataclass
+from typing import Callable, ClassVar
+
+import cemu.arch
+import cemu.core
+import cemu.log
+from cemu.core import GlobalContext
+from cemu.emulator import Emulator, EmulatorState
+from cemu.memory import MemorySection
+
+MEMORY_MAP_DEFAULT_LAYOUT: list[MemorySection] = [
+    MemorySection(".text", 0x00004000, 0x1000, "READ|EXEC"),
+    MemorySection(".data", 0x00005000, 0x1000, "READ|WRITE"),
+    MemorySection(".stack", 0x00006000, 0x4000, "READ|WRITE"),
+]
+
+LOGGER = logging.getLogger(__name__)
+cemu.log.register_sink(LOGGER.debug)
+
+
+@dataclass
+class GenericTestCase:
+    arch: cemu.arch.Architecture
+    codelines: str
+    result: Callable
+
+
+@dataclass
+class X86_16TestCase(GenericTestCase):
+    arch: ClassVar[cemu.arch.Architecture] = cemu.arch.x86.X86()
+
+
+@dataclass
+class X86_32TestCase(GenericTestCase):
+    arch: ClassVar[cemu.arch.Architecture] = cemu.arch.x86.X86_32()
+
+
+@dataclass
+class X86_64TestCase(GenericTestCase):
+    arch: ClassVar[cemu.arch.Architecture] = cemu.arch.x86.X86_64()
+
+
+@dataclass
+class ArmTestCase(GenericTestCase):
+    arch: ClassVar[cemu.arch.Architecture] = cemu.arch.arm.ARM()
+
+
+@dataclass
+class Arm64TestCase(GenericTestCase):
+    arch: ClassVar[cemu.arch.Architecture] = cemu.arch.arm.AARCH64()
+
+
+@dataclass
+class MipsTestCase(GenericTestCase):
+    arch: ClassVar[cemu.arch.Architecture] = cemu.arch.mips.MIPS()
+
+
+@dataclass
+class Mips64TestCase(GenericTestCase):
+    arch: ClassVar[cemu.arch.Architecture] = cemu.arch.mips.MIPS64()
+
+
+@dataclass
+class SparcTestCase(GenericTestCase):
+    arch: ClassVar[cemu.arch.Architecture] = cemu.arch.sparc.SPARC()
+
+
+@dataclass
+class Sparc64TestCase(GenericTestCase):
+    arch: ClassVar[cemu.arch.Architecture] = cemu.arch.sparc.SPARC64()
+
+
+class EmulationTestRunner:
+    """Runs an emulation test session"""
+
+    def __init__(self, emu: Emulator):
+        self.emu = emu
+        return
+
+    def run(self):
+        assert self.emu
+        assert self.emu.code
+        assert self.emu.codelines
+        assert self.emu.sections
+
+        start_address = self.emu.pc() or self.emu.start_addr
+        start_offset = start_address - self.emu.start_addr
+        last_insn_address = self.emu.start_addr + len(self.emu.code)
+
+        if self.emu.use_step_mode:
+            insn = self.emu.next_instruction(
+                self.emu.code[start_offset:], start_address + start_offset
+            )
+            end_address = insn.end
+        else:
+            end_address = last_insn_address
+
+        self.emu.start(start_address, end_address)
+
+        if self.emu.pc() == last_insn_address:
+            self.emu.set(EmulatorState.FINISHED)
+        else:
+            self.emu.set(EmulatorState.IDLE)
+
+        return
+
+
+class TestEmulatorBasic(unittest.TestCase):
+    def __repr__(self) -> str:
+        return "TestEmulatorBasic"
+
+    def setUp(self) -> None:
+        cemu.core.context = GlobalContext()
+        assert cemu.core.context
+        self.emu = cemu.core.context.emulator
+        self.runner = EmulationTestRunner(self.emu)
+        self.emu.set_threaded_runner(self.runner)
+        self.emu.reset()
+        return super().setUp()
+
+    def tearDown(self) -> None:
+        return super().tearDown()
+
+    def test_default_execute_assembly_regs(self):
+        """The most basic test, using the default architecture (x64), execute until the end"""
+        self.emu.sections = MEMORY_MAP_DEFAULT_LAYOUT
+        self.emu.codelines = os.linesep.join(["xor rax, rax", "inc rax"])
+        self.emu.set(EmulatorState.RUNNING)
+        assert self.emu.pc() in self.emu.sections[0]
+        assert self.emu.pc() == self.emu.sections[0].address + len(self.emu.code)
+        assert self.emu.state == EmulatorState.FINISHED
+        assert self.emu.registers["RAX"] == 1
+
+    def test_matrix_execute_assembly_regs(self):
+        """Test the emulation for all supported architectures, with very basic instructions"""
+
+        matrix: list[GenericTestCase] = [
+            X86_16TestCase(
+                os.linesep.join(["nop", "xor ax, ax", "inc ax", "nop"]),
+                lambda: self.emu.registers["AX"] == 1,
+            ),
+            X86_32TestCase(
+                os.linesep.join(["nop", "xor eax, eax", "inc eax", "nop"]),
+                lambda: self.emu.registers["EAX"] == 1,
+            ),
+            X86_64TestCase(
+                os.linesep.join(["nop", "xor rax, rax", "inc rax", "nop"]),
+                lambda: self.emu.registers["RAX"] == 1,
+            ),
+            ArmTestCase(
+                os.linesep.join(["mov r0, 1", "add r0, r0, 1"]),
+                lambda: self.emu.registers["R0"] == 2,
+            ),
+            Arm64TestCase(
+                os.linesep.join(["mov x0, 1", "add x0, x0, 1"]),
+                lambda: self.emu.registers["X0"] == 2,
+            ),
+            MipsTestCase(
+                os.linesep.join(["addiu $v0, $zero, 1", "addi $v0, $v0, 1"]),
+                lambda: self.emu.registers["V0"] == 2,
+            ),
+            SparcTestCase(
+                os.linesep.join(["mov  1, %o0", "add %o0, 1, %o0"]),
+                lambda: self.emu.registers["O0"] == 2,
+            ),
+            # Mips64TestCase(
+            #     os.linesep.join(["addiu $v0, $zero, 1", "addi $v0, $v0, 1"]),
+            #     lambda: self.emu.registers["V0"] == 2,
+            # ),
+            # Sparc64TestCase(
+            #     os.linesep.join(["mov  1, %o0", "add %o0, 1, %o0"]),
+            #     lambda: self.emu.registers["O0"] == 2,
+            # ),
+        ]
+
+        for tc in matrix:
+            #
+            # (Re-)Initialize the context
+            #
+            cemu.core.context.architecture = tc.arch
+            self.emu.reset()
+            self.emu.sections = MEMORY_MAP_DEFAULT_LAYOUT[:]
+            self.emu.codelines = tc.codelines[:]
+            self.emu.use_step_mode = False
+
+            #
+            # Run
+            #
+            self.emu.set(EmulatorState.RUNNING)
+
+            #
+            # Test
+            #
+            # TODO Buggy - last PC value is not refreshed in unicorn for some archs
+            if (
+                isinstance(tc.arch, cemu.arch.x86.X86)
+                or isinstance(tc.arch, cemu.arch.x86.X86_32)
+                or isinstance(tc.arch, cemu.arch.x86.X86_64)
+                or isinstance(tc.arch, cemu.arch.arm.ARM)
+                or isinstance(tc.arch, cemu.arch.arm.AARCH64)
+            ):
+                assert self.emu.pc() == self.emu.sections[0].address + len(
+                    self.emu.code
+                )
+                assert self.emu.state == EmulatorState.FINISHED
+
+            assert tc.result()
diff --git a/tests/test_memory.py b/tests/test_memory.py
new file mode 100644
index 0000000..364ffd5
--- /dev/null
+++ b/tests/test_memory.py
@@ -0,0 +1,48 @@
+import pytest
+import unittest
+
+from cemu.memory import MemoryPermission
+
+
+class EmulationTestMemoryBasic(unittest.TestCase):
+    def test_memory_conversion_from_string(self):
+        assert MemoryPermission.from_string("Read") == MemoryPermission.READ
+        assert MemoryPermission.from_string("wRIte") == MemoryPermission.WRITE
+        assert MemoryPermission.from_string("exeC") == MemoryPermission.EXECUTE
+        assert (
+            MemoryPermission.from_string("Read|wRIte|wRIte")
+            == MemoryPermission.READ | MemoryPermission.WRITE
+        )
+        assert MemoryPermission.from_string("Read|wRIte|exec") == MemoryPermission.ALL
+
+        with pytest.raises(ValueError) as _:
+            MemoryPermission.from_string("Read|wRIte|AAA")
+
+    def test_memory_conversion_from_windows(self):
+        assert MemoryPermission.from_windows(0x01) == MemoryPermission.NONE
+        assert MemoryPermission.from_windows(0x02) == MemoryPermission.READ
+        assert (
+            MemoryPermission.from_windows(0x04)
+            == MemoryPermission.READ | MemoryPermission.WRITE
+        )
+        assert (
+            MemoryPermission.from_windows(0x08)
+            == MemoryPermission.READ | MemoryPermission.WRITE
+        )
+        assert MemoryPermission.from_windows(0x10) == MemoryPermission.EXECUTE
+        assert (
+            MemoryPermission.from_windows(0x20)
+            == MemoryPermission.EXECUTE | MemoryPermission.READ
+        )
+        assert MemoryPermission.from_windows(0x40) == (
+            MemoryPermission.READ | MemoryPermission.WRITE | MemoryPermission.EXECUTE
+        )
+        assert MemoryPermission.from_windows(0x80) == (
+            MemoryPermission.READ | MemoryPermission.WRITE | MemoryPermission.EXECUTE
+        )
+
+        with pytest.raises(NotImplementedError) as _:
+            MemoryPermission.from_windows(0x100)
+
+        with pytest.raises(ValueError) as _:
+            MemoryPermission.from_windows(0x41414141)