Repository: hugsy/cemu (language: python)

Issue:
Separate backend + UI
 * separate backend to use as a lib
 * implement cli 


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/workflows/build.yml b/.github/workflows/build.yml
index 9c38c23..1462c50 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -15,28 +15,28 @@ jobs:
       fail-fast: false
       matrix:
         os: [windows-latest, ubuntu-latest, macos-latest]
-        python-version: [3.9, 3.11]
+        python-version: ['3.10', '3.11']
     name: ${{ matrix.os }} / ${{ matrix.python-version }}
     runs-on: ${{ matrix.os }}
     outputs:
-      windows-latest-3-9: ${{ join(steps.*.outputs.windows-latest-3-9,'') }}
+      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-9: ${{ join(steps.*.outputs.ubuntu-latest-3-9,'') }}
+      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-9: ${{ join(steps.*.outputs.macos-latest-3-9,'') }}
+      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: startsWith(matrix.os, 'ubuntu')
+      if: matrix.os == 'ubuntu-latest'
       shell: bash
       run: |
         sudo apt update
@@ -44,7 +44,7 @@ jobs:
         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')
+      if: matrix.os == 'windows-latest'
       shell: pwsh
       run: |
         echo todo
@@ -57,27 +57,31 @@ jobs:
         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')
+      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: startsWith(matrix.os, 'ubuntu')
+      if: matrix.os == 'ubuntu-latest'
       shell: bash
       run: |
         cp -v ~/.local/bin/cemu build/bin/
 
     - name: "Post build actions for MacOS"
-      if: startsWith(matrix.os, '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
@@ -92,15 +96,15 @@ jobs:
       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() }} 
+      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() }}
@@ -108,7 +112,7 @@ jobs:
       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() }}
@@ -140,14 +144,14 @@ jobs:
           ● Branch [`${{ env.GITHUB_REF_SLUG }}`](${{ env.BRANCH_URL }})
           ● [Detail Page](${{ env.RUN_URL }})
 
-          ${{ needs.build.outputs.windows-latest-3-9 }}
+          ${{ needs.build.outputs.windows-latest-3-10 }}
           ${{ needs.build.outputs.windows-latest-3-11 }}
-          ${{ needs.build.outputs.ubuntu-latest-3-9 }}
+          ${{ needs.build.outputs.ubuntu-latest-3-10 }}
           ${{ needs.build.outputs.ubuntu-latest-3-11 }}
-          ${{ needs.build.outputs.macos-latest-3-9 }}
+          ${{ needs.build.outputs.macos-latest-3-10 }}
           ${{ needs.build.outputs.macos-latest-3-11 }}
 
 
         color: 0x0000ff
         username: ${{ github.actor }} via GithubBot
-        avatar_url: ${{ github.actor.avatar_url }}
+        avatar_url: "https://github.com/hugsy/cemu/blob/dev/cemu/img/new_logo.png?raw=true"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..a56013d
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,41 @@
+name: Publish CEmu to PyPI
+
+on:
+  workflow_dispatch:
+
+jobs:
+  publish:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out repository code
+        uses: actions/checkout@v3
+
+      - name: Setup Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.x"
+
+      - name: Install Requirements
+        run: |
+          python -m pip install --upgrade build wheel twine
+
+      - id: cache-pipenv
+        uses: actions/cache@v1
+        with:
+          path: ~/.local/share/virtualenvs
+          key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
+
+      - name: Install (if not cached)
+        if: steps.cache-pipenv.outputs.cache-hit != 'true'
+        run: |
+          python -m pip install --user --upgrade -r requirements.txt
+          python -m pip install --user --upgrade .
+
+      - name: Build a binary wheel and source tarball
+        run: |
+          python -m build
+
+      - name: Publish to PyPI
+        uses: pypa/gh-action-pypi-publish@master
+        with:
+          password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.gitignore b/.gitignore
index d1978d1..f0fd01d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,10 @@
-.vscode
-.vs
-*.pyc
-__pycache__
+.benchmarks
+.pytest_cache
+.vscode
+.vs
+*.pyc
+*.egg-info
+__pycache__
+.ruff_cache
+build
+dist
diff --git a/README.md b/README.md
index 2510fb8..04ee0b3 100644
--- a/README.md
+++ b/README.md
@@ -14,15 +14,16 @@
 </p>
 
 
-![cemu-linux](https://i.imgur.com/iHtWvTL.png)
+## 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.
 
-## Description ##
+**CEmu** is built upon:
+ * [Keystone](https://github.com/keystone-engine/keystone) for compiling assembly to bytecode
+ * [Unicorn](https://github.com/unicorn-engine/unicorn/) for disassembling bytecode
+ * and [Capstone](https://github.com/aquynh/capstone) libraries
 
-
-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** 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:
 
@@ -64,7 +65,43 @@ 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
+
+### 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)
+
+### CLI
+
+```bash
+python -m cemu cli
+```
+
+From where you'll end up in a REPL, allowing you to build and run the emulation environment.
+
+
+### Lib
+
+```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 e5b7967..3fff75c 100644
--- a/cemu/__main__.py
+++ b/cemu/__main__.py
@@ -13,8 +13,8 @@ def main():
         cemu.log.register_sink(print)
         cemu.log.dbg("Starting in Debug Mode")
 
-    if "--cli" in sys.argv:
-        cemu.core.CemuCli(sys.argv)
+    if len(sys.argv) >= 2 and sys.argv[1] == "cli":
+        cemu.core.CemuCli(sys.argv[2:])
         return
 
     cemu.core.CemuGui(sys.argv)
diff --git a/cemu/arch/__init__.py b/cemu/arch/__init__.py
index 15fd8d4..3f92d3c 100644
--- a/cemu/arch/__init__.py
+++ b/cemu/arch/__init__.py
@@ -1,6 +1,6 @@
 import enum
 import importlib
-from typing import Optional
+from typing import Optional, TYPE_CHECKING
 
 import capstone
 import keystone
@@ -8,6 +8,9 @@
 
 from cemu.const import SYSCALLS_PATH
 
+if TYPE_CHECKING:
+    import cemu.core
+
 
 class Endianness(enum.Enum):
     LITTLE_ENDIAN = 1
@@ -56,15 +59,22 @@ 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:
@@ -194,6 +204,10 @@ 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):
@@ -213,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 706d6c6..9fa7a4a 100644
--- a/cemu/arch/arm.py
+++ b/cemu/arch/arm.py
@@ -94,7 +94,7 @@ def unicorn(self) -> tuple[int, int, int]:
         )
 
     def uc_register(self, name: str) -> int:
-        return getattr(unicorn.x86_const, f"UC_ARM_REG_{name.upper()}")
+        return getattr(unicorn.arm_const, f"UC_ARM_REG_{name.upper()}")
 
 
 class AARCH64(Architecture):
@@ -163,4 +163,4 @@ def unicorn(self) -> tuple[int, int, int]:
         )
 
     def uc_register(self, name: str) -> int:
-        return getattr(unicorn.x86_const, f"UC_ARM64_REG_{name.upper()}")
+        return getattr(unicorn.arm64_const, f"UC_ARM64_REG_{name.upper()}")
diff --git a/cemu/arch/mips.py b/cemu/arch/mips.py
index 42a3363..aa4b9ec 100644
--- a/cemu/arch/mips.py
+++ b/cemu/arch/mips.py
@@ -82,7 +82,7 @@ def unicorn(self) -> tuple[int, int, int]:
         )
 
     def uc_register(self, name: str) -> int:
-        return getattr(unicorn.x86_const, f"UC_MIPS_REG_{name.upper()}")
+        return getattr(unicorn.mips_const, f"UC_MIPS_REG_{name.upper()}")
 
 
 class MIPS64(MIPS):
diff --git a/cemu/arch/sparc.py b/cemu/arch/sparc.py
index 6f92570..e9b81e2 100644
--- a/cemu/arch/sparc.py
+++ b/cemu/arch/sparc.py
@@ -45,30 +45,41 @@ class SPARC(Architecture):
         "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,
+            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)
+        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,
+            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.x86_const, f"UC_SPARC_REG_{name.upper()}")
+        return getattr(unicorn.sparc_const, f"UC_SPARC_REG_{name.upper()}")
 
 
 class SPARC64(SPARC):
@@ -79,19 +90,25 @@ def keystone(self) -> tuple[int, int, int]:
         return (
             keystone.KS_ARCH_SPARC,
             keystone.KS_MODE_SPARC64,
-            keystone.KS_MODE_LITTLE_ENDIAN,
+            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,
+            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,
+            unicorn.UC_MODE_LITTLE_ENDIAN
+            if self.endianness == Endianness.LITTLE_ENDIAN
+            else unicorn.UC_MODE_BIG_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..5e74725
--- /dev/null
+++ b/cemu/cli/repl.py
@@ -0,0 +1,288 @@
+import subprocess
+import pathlib
+import tempfile
+import unicorn
+import os
+
+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.core
+import cemu.arch
+import cemu.memory
+from cemu.utils import hexdump
+from cemu.emulator import EmulatorState
+from cemu.log import dbg, error, info, ok, warn
+
+bindings = KeyBindings()
+
+TEXT_EDITOR = os.getenv("EDITOR") or "nano -c"
+
+
+@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
+            #
+            completer = NestedCompleter.from_nested_dict(
+                {
+                    ".quit": 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": {
+                        "list": None,
+                        "add": None,
+                        "del": None,
+                        "view": None,
+                        "edit": None,
+                    },
+                    ".reset": None,
+                    ".run": None,
+                    ".code": {
+                        "show": None,
+                        "check": None,
+                        "edit": None,
+                    },
+                    # TODO everything below
+                    ".load": None,
+                    ".save": {
+                        "asm": None,
+                        "bin": None,
+                        "pe": None,
+                        "elf": None,
+                    },
+                }
+            )
+
+            #
+            # 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 "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":
+                            print(f"{args[1]}={emu.registers[args[1]]:#x}")
+                        case "set":
+                            emu.registers[args[1]] = int(args[2])
+
+                case "mem":
+                    match args[0]:
+                        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 not emu.is_running:
+                                warn("Emulator not running")
+                            else:
+                                assert emu.vm
+                                address = int(args[1])
+                                size = int(args[2])
+                                data = emu.vm.mem_read(address, size)
+                                print(hexdump(data))
+
+                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":
+                    pass
+
+                case "reset":
+                    emu.set(EmulatorState.FINISHED)
+
+                case _:
+                    dbg(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/core.py b/cemu/core.py
index bbbf697..d77b648 100644
--- a/cemu/core.py
+++ b/cemu/core.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+import argparse
+
 import sys
 from typing import TYPE_CHECKING, Union
 
@@ -7,10 +9,11 @@
     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
 
 
@@ -18,12 +21,14 @@ class GlobalContext:
     settings: cemu.settings.Settings
     __emulator: cemu.emulator.Emulator
     __architecture: cemu.arch.Architecture
+    __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")
         self.__architecture = cemu.arch.Architectures.find(default_arch)
+        self.__os = cemu.os.Linux
         return
 
     @property
@@ -42,6 +47,17 @@ 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
@@ -55,6 +71,9 @@ def root(self, root: cemu.ui.main.CEmuWindow):
         self.__root = root
 
 
+#
+# The global application context. This **must** defined for cemu to operate
+#
 context: Union[GlobalContext, GlobalGuiContext]
 
 
@@ -81,16 +100,28 @@ def CemuGui(args: list[str]) -> None:
     sys.exit(app.exec())
 
 
-def CemuCli(args: list[str]) -> None:
-    """Run cemu from the terminal
+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()
-    # TODO build a repl like with prompt-toolkit + rich
-    return
+
+    #
+    # 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 de07e58..2a0f8d2 100644
--- a/cemu/emulator.py
+++ b/cemu/emulator.py
@@ -1,9 +1,11 @@
 from enum import IntEnum, unique
 from multiprocessing import Lock
-from typing import Any, Callable, Optional, Union
+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
@@ -15,22 +17,48 @@
 @unique
 class EmulatorState(IntEnum):
     # fmt: off
-    NOT_RUNNING = 0              # Nothing is initialized
-    # SETUP = 1
+    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
+    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:
     def __init__(self):
         self.use_step_mode = False
         self.widget = None
-        self.lock: Lock = Lock()
-        self.state: EmulatorState = EmulatorState.NOT_RUNNING
+        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: [],
@@ -42,7 +70,7 @@ def __init__(self):
         self.code: bytes = b""
         self.codelines: str = ""
         self.sections: list[MemorySection] = []
-        self.registers: dict[str, int] = {}
+        self.registers: EmulationRegisters = EmulationRegisters({})
         self.start_addr: int = 0
 
         #
@@ -55,7 +83,9 @@ def reset(self):
         self.code = b""
         self.codelines = ""
         self.sections = []
-        self.registers = {name: 0 for name in cemu.core.context.architecture.registers}
+        self.registers = EmulationRegisters(
+            {name: 0 for name in cemu.core.context.architecture.registers}
+        )
         self.start_addr = 0
         self.set(EmulatorState.NOT_RUNNING)
         return
@@ -65,20 +95,19 @@ def __str__(self) -> str:
             return f"Emulator is running, IP={self.pc()}, SP={self.sp()}"
         return "Emulator instance is not running"
 
-    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:
-            error("get_register_value() failed: VM not initialized")
-            return -1
+            return None
 
         arch = cemu.core.context.architecture
         ur = arch.uc_register(regname)
         val = self.vm.reg_read(ur)
 
-        # TODO handle xmmreg later
+        # TODO handle extended regs later
         assert isinstance(val, int)
         return val
 
@@ -88,13 +117,15 @@ 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 setup(self) -> None:
         """
@@ -128,7 +159,7 @@ def setup(self) -> None:
         if not self.__populate_memory():
             raise Exception("populate_memory() failed")
 
-        if not self.__populate_registers():
+        if not self.__populate_vm_registers():
             raise Exception("populate_registers() failed")
 
         if not self.__populate_text_section():
@@ -158,7 +189,7 @@ def __populate_memory(self) -> bool:
         self.end_addr = -1
         return True
 
-    def __populate_registers(self) -> bool:
+    def __populate_vm_registers(self) -> bool:
         """
         Populates the VM memory layout according to the values given as parameter.
         """
@@ -166,7 +197,7 @@ def __populate_registers(self) -> bool:
             return False
 
         arch = cemu.core.context.architecture
-        registers: dict[str, int] = self.registers
+        registers: EmulationRegisters = self.registers
 
         #
         # Set the initial IP if unspecified
@@ -174,13 +205,16 @@ def __populate_registers(self) -> bool:
         if registers[arch.pc] == 0:
             section_text = self.find_section(".text")
             registers[arch.pc] = section_text.address
+            dbg(f"Default PC set to {registers[arch.pc]:#x}")
 
         #
-        # Set the initial SP if unspecified
+        # Set the initial SP if unspecified, in the middle of the stack section
         #
         if registers[arch.sp] == 0:
             section_stack = self.find_section(".stack")
-            registers[arch.sp] = section_stack.address
+            offset = (section_stack.end - section_stack.address) // 2
+            registers[arch.sp] = section_stack.address + offset
+            dbg(f"Default SP set to {registers[arch.sp]:#x}")
 
         #
         # Populate all the registers for unicorn
@@ -188,27 +222,32 @@ def __populate_registers(self) -> bool:
         if is_x86_32(arch):
             # create fake selectors
             ## required
+            text = self.find_section(".text")
             registers["CS"] = int(
                 x86.X86_32.SegmentDescriptor(
-                    0,
+                    text.address >> 8,
                     x86.X86_32.SegmentType.Code | x86.X86_32.SegmentType.Accessed,
                     False,
                     3,
                     True,
                 )
             )
+
+            data = self.find_section(".data")
             registers["DS"] = int(
                 x86.X86_32.SegmentDescriptor(
-                    0,
+                    data.address >> 8,
                     x86.X86_32.SegmentType.Data | x86.X86_32.SegmentType.Accessed,
                     False,
                     3,
                     True,
                 )
             )
+
+            stack = self.find_section(".stack")
             registers["SS"] = int(
                 x86.X86_32.SegmentDescriptor(
-                    0,
+                    stack.address >> 8,
                     x86.X86_32.SegmentType.Data
                     | x86.X86_32.SegmentType.Accessed
                     | x86.X86_32.SegmentType.ExpandDown,
@@ -223,12 +262,33 @@ def __populate_registers(self) -> bool:
             registers["ES"] = 0
 
         for r in registers.keys():
+            # TODO figure out segmentation on  unicorn
+            if r in x86.X86_32.selector_registers:
+                continue
+
             ur = arch.uc_register(r)
             self.vm.reg_write(ur, registers[r])
 
         dbg(f"[vm::setup] Registers {registers}")
         return True
 
+    def __refresh_registers_from_vm(self) -> bool:
+        """Refresh the emulation register hashmap by reading values from the VM
+
+        Returns:
+            bool: True on success, False otherwise
+        """
+        if not self.vm or not self.is_running:
+            return False
+
+        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 True if all went well,
@@ -252,6 +312,9 @@ def __generate_text_bytecode(self) -> bool:
         self.end_addr = self.start_addr + len(self.code)
         return True
 
+    def validate_assembly_code(self) -> bool:
+        return self.__generate_text_bytecode()
+
     def __populate_text_section(self) -> bool:
         if not self.vm:
             return False
@@ -296,24 +359,19 @@ def hook_code(
         """
         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: cemu.utils.Instruction = self.next_instruction(code, address)
-        # if self.stop_now:
-        #     self.start_addr = self.get_register_value(arch.pc)
-        #     emu.emu_stop()
-        #     return True
 
         if self.use_step_mode:
             dbg(f"[vm::runtime] Stepping @ {insn}")
         else:
             dbg(f"[vm::runtime] Executing @ {insn}")
-
-        # if self.use_step_mode:
-        #     self.stop_now = True
         return True
 
     def hook_block(self, emu: unicorn.Uc, addr: int, size: int, misc: Any) -> int:
@@ -409,24 +467,38 @@ def set(self, new_state: EmulatorState):
             new_state (EmulatorState): the new state
         """
 
-        def assign_state(__new_state: EmulatorState):
+        def assign_state(__new_state: EmulatorState) -> EmulatorState:
             if self.state == __new_state:
-                return
-
+                return self.state
             info(f"Emulator is now {__new_state.name}")
-
+            __old_state = self.state
             self.state = __new_state
             assert int(self.state) == int(__new_state), f"{self.state} != {__new_state}"
+            return __old_state
+
+        old_state = assign_state(new_state)
 
-        assign_state(new_state)
+        if old_state == self.state:
+            return
+
+        dbg(f"Emulator state transition: {old_state} -> {self.state}")
 
-        match new_state:
+        match self.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 old_state == EmulatorState.RUNNING:
+                    self.__refresh_registers_from_vm()
+
+            case EmulatorState.FINISHED:
+                self.__refresh_registers_from_vm()
+
             case _:
                 pass
 
@@ -439,7 +511,7 @@ def assign_state(__new_state: EmulatorState):
             res = new_state_cb()
             dbg(f"{function_name}() return {res}")
 
-        match new_state:
+        match self.state:
             case EmulatorState.RUNNING:
                 #
                 # This will effectively trigger the execution in unicorn
@@ -476,14 +548,69 @@ def is_running(self) -> bool:
 
     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`
 
-    def context(self) -> dict[str, Union[int, str]]:
-        """Get the current context for the registers as a hash table
+        Args:
+            address (int): _description_
+            size (int): _description_
 
         Returns:
-            dict[str, Union[int, str]]: _description_
+            bytearray:
+            None:
         """
-        if not self.vm:
-            return {}
-        # regs = {name: self.get_register_value(name) for name in self.registers}
-        return self.registers
+        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:
+        """
+        if not self.vm or not self.is_running:
+            return None
+
+        self.vm.mem_write(address, data)
+        return len(data)
+
+    def start(self, start_address: int, end_address: int) -> None:
+        """Public wrapper for `vm.emu_start`
+
+        Args:
+
+        Returns:
+            bytearray:
+            None:
+        """
+        assert self.vm
+        assert self.is_running
+
+        with self.lock:
+            self.vm.emu_start(start_address, end_address)
+
+        return
+
+    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/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 44b4a68..03c95fb 100644
--- a/cemu/plugins/__init__.py
+++ b/cemu/plugins/__init__.py
@@ -26,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/settings.py b/cemu/settings.py
index 971768a..931f77d 100644
--- a/cemu/settings.py
+++ b/cemu/settings.py
@@ -22,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)
 
@@ -32,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}'")
@@ -79,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/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/main.py b/cemu/ui/main.py
index b415c05..49a650e 100644
--- a/cemu/ui/main.py
+++ b/cemu/ui/main.py
@@ -725,7 +725,7 @@ 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")
diff --git a/cemu/ui/registers.py b/cemu/ui/registers.py
index 40a0dd8..57097cc 100644
--- a/cemu/ui/registers.py
+++ b/cemu/ui/registers.py
@@ -40,7 +40,7 @@ def __init__(self, parent, *args, **kwargs):
         widget = QWidget()
         widget.setLayout(layout)
         self.setWidget(widget)
-        self.updateGrid()
+        # self.updateGrid()
 
         #
         # Emulator state callback
@@ -50,6 +50,7 @@ def __init__(self, parent, *args, **kwargs):
         emu.add_state_change_cb(
             EmulatorState.FINISHED, self.onFinishedRefreshRegisterGrid
         )
+        emu.add_state_change_cb(EmulatorState.NOT_RUNNING, self.updateGrid)
         return
 
     def updateGrid(self) -> None:
@@ -71,7 +72,7 @@ def updateGrid(self) -> None:
                     DEFAULT_REGISTER_VIEW_REGISTER_FONT_SIZE,
                 )
             )
-            val = emu.get_register_value(reg) if emu.vm else 0
+            val = emu.registers[reg]
             old_val = self.__old_register_values.get(reg, 0)
             if type(val) in (int, int):
                 value = format_address(val, arch)
@@ -106,8 +107,8 @@ def updateGrid(self) -> None:
     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)):
+        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)
diff --git a/cemu/utils.py b/cemu/utils.py
index 04ea871..21b4c6c 100644
--- a/cemu/utils.py
+++ b/cemu/utils.py
@@ -3,12 +3,13 @@
 import random
 import string
 from dataclasses import dataclass
-from typing import Optional
+from typing import Optional, TYPE_CHECKING
 
+if TYPE_CHECKING:
+    import cemu.arch
 import cemu.core
 import cemu.errors
 import cemu.utils
-from cemu.arch import Architecture, Architectures, Endianness
 from cemu.const import COMMENT_MARKER, PROPERTY_MARKER
 from cemu.log import dbg
 
@@ -48,7 +49,7 @@ def hexdump(
     return os.linesep.join(result)
 
 
-def format_address(addr: int, arch: Optional[Architecture] = None) -> str:
+def format_address(addr: int, arch: Optional[cemu.arch.Architecture] = None) -> str:
     """Format an address to string, aligned to the given architecture
 
     Args:
@@ -144,6 +145,8 @@ def assemble(
     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
     #
@@ -176,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
@@ -196,8 +201,8 @@ def get_metadata_from_stream(content: str) -> Optional[tuple[Architecture, Endia
             - 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()
@@ -214,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
index a3ea24d..d5dcb7d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,121 @@
+
+#
+# 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"]
+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 = [
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index b52ce55..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-capstone>=4
-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 1898934..0000000
--- a/setup.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from pathlib import Path
-
-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 Path("./requirements.txt").open("r").readlines()]
-
-
-setup(
-    name=const.PROGNAME,
-    description=const.DESCRIPTION,
-    long_description=(Path(__file__).parent / "README.md").read_text(),
-    long_description_content_type="text/markdown",
-    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=(Path(__file__).parent / "requirements.txt").open().readlines(),
-    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..c707fdf
--- /dev/null
+++ b/tests/test_basic.py
@@ -0,0 +1,163 @@
+import logging
+import os
+import unittest
+from dataclasses import dataclass
+from typing import Callable
+
+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)
+
+
+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"""
+
+        @dataclass
+        class GenericTestCase:
+            arch: cemu.arch.Architecture
+            codelines: str
+            result: Callable
+
+        matrix: list[GenericTestCase] = [
+            GenericTestCase(
+                cemu.arch.x86.X86(),
+                os.linesep.join(["nop", "xor ax, ax", "inc ax", "nop"]),
+                lambda: self.emu.registers["AX"] == 1,
+            ),
+            GenericTestCase(
+                cemu.arch.x86.X86_32(),
+                os.linesep.join(["nop", "xor eax, eax", "inc eax", "nop"]),
+                lambda: self.emu.registers["EAX"] == 1,
+            ),
+            GenericTestCase(
+                cemu.arch.x86.X86_64(),
+                os.linesep.join(["nop", "xor rax, rax", "inc rax", "nop"]),
+                lambda: self.emu.registers["RAX"] == 1,
+            ),
+            GenericTestCase(
+                cemu.arch.arm.ARM(),
+                os.linesep.join(["mov r0, 1", "add r0, r0, 1"]),
+                lambda: self.emu.registers["R0"] == 2,
+            ),
+            GenericTestCase(
+                cemu.arch.arm.AARCH64(),
+                os.linesep.join(["mov x0, 1", "add x0, x0, 1"]),
+                lambda: self.emu.registers["X0"] == 2,
+            ),
+            # TODO Buggy - last PC value is not refreshed in unicorn
+            # GenericTestCase(
+            #     cemu.arch.mips.MIPS(),
+            #     os.linesep.join(["addiu $v0, $zero, 1", "addi $v0, $v0, 1"]),
+            #     lambda: self.emu.registers["V0"] == 2,
+            # ),
+            # GenericTestCase(
+            #     cemu.arch.mips.MIPS64(),
+            #     os.linesep.join(["addiu $v0, $zero, 1", "addi $v0, $v0, 1"]),
+            #     lambda: self.emu.registers["V0"] == 2,
+            # ),
+            # GenericTestCase(
+            #     cemu.arch.sparc.SPARC(),
+            #     os.linesep.join(["mov  1, %g0", "add %g0, 1, %g0"]),
+            #     lambda: self.emu.registers["G0"] == 2,
+            # ),
+            # GenericTestCase(
+            #     cemu.arch.sparc.SPARC64(),
+            #     os.linesep.join(["mov  1, %g0", "add %g0, 1, %g0"]),
+            #     lambda: self.emu.registers["V0"] == 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
+            #
+            assert self.emu.pc() == self.emu.sections[0].address + len(self.emu.code)
+            assert self.emu.state == EmulatorState.FINISHED
+            assert tc.result()