Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / commits / b945a27

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

C++ 72.3% C 26.2%
git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gz

Cleanup restructore and co

Christian Immanuel · 2026-06-05 21:38 · b945a27a56f0db54737eda8d4d11739813f243b7

 benchmark/bench_physics.cpp     |   3 +-
 src/states/game_state.cpp       | 630 +++++++++-------------------------------
 src/states/game_state.hpp       |  48 +--
 src/systems/freelook_system.cpp |  34 +--
 src/systems/freelook_system.hpp |  33 +--
 src/systems/wall_connection.cpp | 342 ++++++++++++++++++++++
 src/systems/wall_connection.hpp |  63 ++++
 src/wall/text_wall_scroll.hpp   |  18 +-
 8 files changed, 568 insertions(+), 603 deletions(-)

diff --git a/benchmark/bench_physics.cpp b/benchmark/bench_physics.cpp
index 77b935f..c9ec5a5 100644
--- a/benchmark/bench_physics.cpp
+++ b/benchmark/bench_physics.cpp
@@ -27,11 +27,10 @@ static void BM_PhysicsUpdateObjects(benchmark::State& state)
     }
 
     PhysicsSystem sys;
-    constexpr float ws = 16.0f;
     constexpr double dt = 1.0 / 60.0;
 
     for (auto _ : state) {
-        sys.update_Objects(objects, dt, 25.0, ws);
+        sys.update_Objects(objects, dt, 25.0);
     }
 
     state.SetLabel("N=" + std::to_string(N));
diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp
index 0bb6d94..93dab8d 100644
--- a/src/states/game_state.cpp
+++ b/src/states/game_state.cpp
@@ -98,68 +98,45 @@ GameState::debug_SpawnWords([[maybe_unused]]size_t n)
 
 
 
-std::tuple<Vec3, Vec3, double>
-GameState::anchorframe_Current() const
-{
-    const FreelookState& fs = _freelook_sys->state;
-
-    // Frozen in place while freelook is actively held.
-    if (fs.active)
-        return { fs.frozen_anchor, fs.frozen_right, fs.frozen_glyph_yaw };
-
-    // Live carry frame, in front of wherever the camera is looking now.
-    const Vec3   live_anchor = CarryMath::line_Anchor(*_ctx.camera, CarryMath::CARRY_DIST);
-    const Vec3   live_right  = _ctx.camera->right;
-    const double live_yaw    = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z);
-
-    // Returning: ease the whole carry frame from the frozen spot back to the
-    // live one. The line flies to the player; the camera is never moved.
-    if (fs.lerping) {
-        double t = fs.lerp_t;
-        t = (t < 0.0) ? 0.0 : (t > 1.0 ? 1.0 : t);
-        const double s = t * t * (3.0 - 2.0 * t);   // smoothstep
-
-        auto lerp3 = [s](const Vec3& a, const Vec3& b) {
-            return Vec3{ a.x + (b.x - a.x) * s,
-                         a.y + (b.y - a.y) * s,
-                         a.z + (b.z - a.z) * s };
-        };
-
-        Vec3 anchor = lerp3(fs.frozen_anchor, live_anchor);
-        Vec3 right  = lerp3(fs.frozen_right,  live_right);
-        const double rl = SDL_sqrt(right.x*right.x + right.y*right.y + right.z*right.z);
-        if (rl > 1e-9) { right.x /= rl; right.y /= rl; right.z /= rl; }
-
-        // Shortest-path angular interpolation for the glyph yaw.
-        double dyaw = live_yaw - fs.frozen_glyph_yaw;
-        while (dyaw >  SDL_PI_D) dyaw -= 2.0 * SDL_PI_D;
-        while (dyaw < -SDL_PI_D) dyaw += 2.0 * SDL_PI_D;
-        const double yaw = fs.frozen_glyph_yaw + dyaw * s;
-
-        return { anchor, right, yaw };
-    }
-
-    return { live_anchor, live_right, live_yaw };
-}
-
-
 void
 GameState::carriedline_Update()
 {
     if (_carried.empty()) return;
 
-    auto [anchor, right, glyph_yaw] = anchorframe_Current();
-
-    DynArray<Vec3>
-        positions = CarryMath::slot_positions(_carried, anchor, right, CarryMath::CARRY_GAP);
-    DynArray<CarriedWordNode*>
-        nodes     = _carried.reverse_nodes();
+    const FreelookState& fs = _freelook_sys->state;
 
-    for (size_t i = 0; i < nodes.size() && i < positions.size(); ++i) {
-        if (nodes[i]->word) {
-            nodes[i]->word->pos = positions[i];
-            nodes[i]->word->yaw = static_cast<float>(glyph_yaw);
+    // Frozen: leave every word exactly where it is.
+    if (fs.active) return;
+
+    // Live carry frame in front of the camera.
+    const Vec3   anchor    = CarryMath::line_Anchor(*_ctx.camera, CarryMath::CARRY_DIST);
+    const Vec3   right     = _ctx.camera->right;
+    const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z);
+
+    DynArray<Vec3> slots = CarryMath::slot_positions(_carried, anchor, right, CarryMath::CARRY_GAP);
+    DynArray<CarriedWordNode*> nodes = _carried.reverse_nodes();
+
+    // Easing back from the frozen spots: lerp each word from its release
+    // position toward its live slot. Yaw snaps to live (cheap, reads fine).
+    const bool easing = fs.lerping && !_freelook_return_from.empty();
+    double s = 1.0;
+    if (easing) {
+        double t = fs.lerp_t; t = (t < 0.0) ? 0.0 : (t > 1.0 ? 1.0 : t);
+        s = t * t * (3.0 - 2.0 * t);   // smoothstep
+    }
+
+    for (size_t i = 0; i < nodes.size() && i < slots.size(); ++i) {
+        if (!nodes[i]->word) continue;
+        Vec3 target = slots[i];
+        if (easing && i < _freelook_return_from.size()) {
+            const Vec3& from = _freelook_return_from[i];
+            nodes[i]->word->pos = { from.x + (target.x - from.x) * s,
+                                    from.y + (target.y - from.y) * s,
+                                    from.z + (target.z - from.z) * s };
+        } else {
+            nodes[i]->word->pos = target;
         }
+        nodes[i]->word->yaw = static_cast<float>(glyph_yaw);
     }
 }
 
@@ -169,29 +146,20 @@ GameState::pult_DispatchAction(PultAction act)
 {
     _step_ledger.buttons_pressed.push_back(act);
 
-    const double floor_y = World::get_FloorY<double>();
-
     switch (act) {
-        case PultAction::Compile:
-            on_Compile();
-            break;
-        case PultAction::RunProgram:
-            on_Run();
-            break;
-        case PultAction::ScrollEditorUp:
-            TextWallScroll::scroll_Up(_ctx.wall_texts, Wall::Role::Editor);
-            _editor_wall.scroll(ConfigWall::scroll_step);
-            break;
-        case PultAction::ScrollEditorDown:
-            TextWallScroll::scroll_Down(_ctx.wall_texts, Wall::Role::Editor);
-            _editor_wall.scroll(-ConfigWall::scroll_step);
-            break;
-        case PultAction::ClearEditor:
-            TextWallScroll::clear_ToFloor(_ctx.wall_texts, _ctx.words, Wall::Role::Editor, floor_y);
-            _editor_wall.clear();
-            break;
-        case PultAction::ConnectEditor:  connect_BeginEditor(); break;
-        case PultAction::ConnectProgram: connect_Begin(Conn::Program); break;
+        case PultAction::Compile:          on_Compile();               break;
+        case PultAction::RunProgram:       on_Run();                   break;
+        case PultAction::ScrollEditorUp:   TextWallScroll::scroll_Up(_ctx.wall_texts, Wall::Role::Editor);
+                                           _editor_wall.scroll(ConfigWall::scroll_step);
+                                                                       break;
+        case PultAction::ScrollEditorDown: TextWallScroll::scroll_Down(_ctx.wall_texts, Wall::Role::Editor);
+                                           _editor_wall.scroll(-ConfigWall::scroll_step);
+                                                                       break;
+        case PultAction::ClearEditor:      TextWallScroll::clear_ToFloor(_ctx.wall_texts, _ctx.words, Wall::Role::Editor);
+                                           _editor_wall.clear();
+                                                                       break;
+        case PultAction::ConnectEditor:    wallconnect_BeginEditor();  break;
+        case PultAction::ConnectProgram:   wallconnect_BeginProgram(); break;
     }
 }
 
@@ -380,17 +348,19 @@ GameState::on_Jump()
 void
 GameState::on_FreelookStart()
 {
-    Vec3   anchor = CarryMath::line_Anchor(*_ctx.camera, CarryMath::CARRY_DIST);
-    double yaw    = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z);
-    _freelook_sys->begin(anchor, _ctx.camera->right, yaw);
+    _freelook_sys->begin();
 }
 
-
 void
 GameState::on_FreelookEnd()
 {
-    if (_freelook_sys->state.active)
-        _freelook_sys->end();
+    if (!_freelook_sys->state.active) return;
+
+    _freelook_return_from.clear();
+    for (CarriedWordNode* node : _carried.reverse_nodes())
+        if (node->word) _freelook_return_from.push_back(node->word->pos);
+
+    _freelook_sys->end();
 }
 
 void
@@ -613,19 +583,28 @@ GameState::handle_Input(FrameInput& input, double dt) {
             case InputCommand::TypingCommit:  on_TypingCommit(input); break;
             case InputCommand::TypingCancel:  on_TypingCancel(input); break;
             case InputCommand::SandboxRestore:    sandbox_Restore();  break;
-            case InputCommand::ConnectPultSubmit: conn_Submit();    break;
-            case InputCommand::ConnectPultExit:   connect_End();    break;
+            case InputCommand::ConnectPultSubmit: on_WallConnectSubmit(); break;
+            case InputCommand::ConnectPultExit:   on_WallConnectExit();   break;
         }
     }
-    if (input.capture_vim) {
-        editor_FeedVim(input.vim_keys);
+    if (_wall_connection.target() == WallConnection::Target::Editor) {
+        WallConnection::Request req = _wall_connection.feed_Vim(input.vim_keys, _ctx.camera->pos.y);
         input.vim_keys.clear();
-    } else if (input.capture_typing) {
-        if (input.typing_buffer != _conn_last) { conn_RebuildPreview(); _conn_last = input.typing_buffer; }
+        if (req == WallConnection::Request::Exit) {
+            editor_CommitToWall(_wall_connection.lines());
+            _wall_connection.end();
+            _frame_input.capture_vim = false;
+        } else if (req == WallConnection::Request::EditorWrite) {
+            editor_CommitToWall(_wall_connection.lines());
+            _wall_connection.load_Editor(editor_AbsorbLines(), _ctx.camera->pos.y);
+        }
+    } else if (_wall_connection.target() == WallConnection::Target::Program) {
+        _wall_connection.set_ProgramBuffer(input.typing_buffer);
     } else if (input.typing_mode) {
         input.forward = input.backward = input.left = input.right = false;
         if (input.typing_buffer != _typing_last) { typing_RebuildPreview(input.typing_buffer); _typing_last = input.typing_buffer; }
     }
+
     if (_pick_cooldown > 0.0) _pick_cooldown -= dt;
     if (input.pick && _pick_cooldown <= 0.0)
         { if (on_Pick()) _pick_cooldown = 0.15; }
@@ -683,7 +662,7 @@ GameState::update(double dt)
         _plate_sys-> update(dt, *_ctx.player, _step_plates);
         _editor_wall.update(dt, _ctx.camera->pos);
 
-        if (_conn != Conn::None) _conn_blink += dt;
+        _wall_connection.update(dt);
     }
 
 
@@ -767,17 +746,7 @@ GameState::render(SDL_Window& window, GlRenderer& renderer)
     }
 
     { FP_ZONE("render.wall_edit");
-        if (_conn == Conn::Program) {
-            if (_conn_input) _conn_input->draw(renderer, *_ctx.camera, *_ctx.light);
-            conn_DrawCursor(renderer);
-            conn_DrawElectric(renderer);
-        } else if (_conn == Conn::Editor) {
-            for (const Text3dPtr& t : _edit_render)   if (t) t->draw(renderer, *_ctx.camera, *_ctx.light);
-            for (const Text3dPtr& n : _edit_linenums) if (n) n->draw(renderer, *_ctx.camera, *_ctx.light);
-            if (_edit_cmdline) _edit_cmdline->draw(renderer, *_ctx.camera, *_ctx.light);
-            edit_DrawCursor(renderer);
-            conn_DrawElectric(renderer);
-        }
+        _wall_connection.render(renderer, *_ctx.camera, *_ctx.light);
     }
 
     { FP_ZONE("render._carried");
@@ -961,7 +930,7 @@ GameState::update_WorldSimulation(double dt)
 {
     _ctx.player->prev_pos = _ctx.player->pos;
 
-    { FP_ZONE("subst.phys_player");
+    { FP_ZONE("updateworldsim.phys_player");
         _physics_sys->update_Player(*_ctx.player,
                                      _ctx.player->get_AccX(),
                                      _ctx.player->get_AccZ(),
@@ -973,11 +942,11 @@ GameState::update_WorldSimulation(double dt)
     for (const GameObjectPtr& p : _ctx.scenery) if (p) colliders.push_back(p.get());
     for (const Text3dPtr&     p : _ctx.words)   if (p) colliders.push_back(p.get());
 
-    { FP_ZONE("subst.phys_objects");
+    { FP_ZONE("updateworldsim.phys_objects");
         _physics_sys->update_Objects(colliders, dt, 25.0);
     }
 
-    { FP_ZONE("subst.word_ai");
+    { FP_ZONE("updateworldsim.word_ai");
         DynArray<Text3dPtr> ai_dropped;
         _word_ai_sys->update(_ctx.words, *_ctx.player, _carried, ai_dropped, dt);
         for (Text3dPtr& d : ai_dropped) {
@@ -985,14 +954,14 @@ GameState::update_WorldSimulation(double dt)
         }
     }
 
-    { FP_ZONE("subst.phys_projectiles");
+    { FP_ZONE("updateworldsim.phys_projectiles");
         _physics_sys->update_Projectiles(_projectiles, dt, _ctx.words);
     }
 
     DynArray<ProjectileContactEvent> projectile_contacts;
 
     DynArray<TextLabel*> all_labels = _pult_sys->get_Labels();
-    { FP_ZONE("subst.hint_and_labels");
+    { FP_ZONE("updateworldsim.hint_and_labels");
         _hint_sys->update(dt, *_ctx.camera);
         for (TextLabel* lbl : all_labels)
             lbl->update(dt, *_ctx.camera);
@@ -1002,12 +971,12 @@ GameState::update_WorldSimulation(double dt)
             all_labels.push_back(&wl.label);
     }
 
-    { FP_ZONE("subst.resolve_projectiles");
+    { FP_ZONE("updateworldsim.resolve_projectiles");
         _physics_sys->resolve_Projectiles(_projectiles, &colliders, &all_labels, &projectile_contacts);
     }
 
     DynArray<WallTextHitTest> wall_text_hits;
-    { FP_ZONE("subst.build_wall_hits");
+    { FP_ZONE("updateworldsim.build_wall_hits");
         wall_text_hits.reserve(_ctx.wall_texts.size());
         for (size_t i = 0; i < _ctx.wall_texts.size(); ++i) {
             const TextWallPtr& wt = _ctx.wall_texts[i];
@@ -1040,7 +1009,7 @@ GameState::update_WorldSimulation(double dt)
     }
 
     DynArray<ProjectileResult> proj_results;
-    { FP_ZONE("subst.projectile_sys");
+    { FP_ZONE("updateworldsim.projectile_sys");
         _projectiles_sys->update(_projectiles, proj_results,
                                  wall_text_hits, dt,
                                  _program_sys->is_Running());
@@ -1048,7 +1017,7 @@ GameState::update_WorldSimulation(double dt)
 
     DynArray<size_t> wall_text_to_erase;
 
-    { FP_ZONE("subst.projectile_land");
+    { FP_ZONE("updateworldsim.projectile_land");
         for (ProjectileResult& result : proj_results) {
             switch (result.landing) {
                 case ProjectileLanding::Floor:    projectile_LandOnFloor     (result); break;
@@ -1087,7 +1056,7 @@ GameState::update_WorldSimulation(double dt)
         }
     }
 
-    { FP_ZONE("subst.player_collision");
+    { FP_ZONE("updateworldsim.player_collision");
         PlayerCollision col = _ctx.world->resolve_Player(*_ctx.player);
         _ctx.player->apply_Collision(col);
 
@@ -1146,7 +1115,7 @@ GameState::level_TryAdvance()
     if (!_level_runner_sys || _level_runner_sys->complete())
         return false;
 
-    auto [anchor, right, glyph_yaw] = anchorframe_Current();
+    double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z);
     _celebration_sys->start(*_ctx.camera, glyph_yaw);
     return false;
 }
@@ -1241,49 +1210,19 @@ GameState::sandbox_LoadFileToWall()
 
     std::string src;
     if (!SourceTokens::read_File(path, src)) {
-        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
-                     "Sandbox: cannot read %s", path.c_str());
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Sandbox: cannot read %s", path.c_str());
         return;
     }
 
-    // Clear any existing editor-wall words first.
-    _editor_wall.clear();
-    _ctx.wall_texts.erase(
-        std::remove_if(_ctx.wall_texts.begin(), _ctx.wall_texts.end(),
-            [](const TextWallPtr& wt){ return wt->role == Wall::Role::Editor; }),
-        _ctx.wall_texts.end());
-
-    const float ws = World::get_BoxScale();
-    const double x_wall = -static_cast<double>(ws) + 0.05;   // NegX wall surface
-    Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws);
-
-    auto lines = SourceTokens::tokenize_Lines(src);
-
-    // Top line highest; step down by the editor's line height.
-    double y = static_cast<double>(ws) - 2.0;   // start near top, tweak to taste
-    const double z_start = static_cast<double>(ws) - 1.0;   // leftmost (large z)
-
-    for (const auto& toks : lines) {
-        double z = z_start;
-        for (const std::string& tk : toks) {
-            Text3dPtr t = make_Text3d(tk, Text3d::depth_wall, 0.003f,
-                                      Vec3{ x_wall, y, z }, ConfigColor::wall);
-            t->yaw = tf.yaw;
+    editor_Clear();
 
-            // Snap into the editor wall's line tracking, like a thrown word.
-            Vec3 snapped = _editor_wall.on_WordPlaced(t.get(), Vec3{ x_wall, y, z });
-            t->pos = snapped;
-
-            _ctx.wall_texts.push_back(cpputils::make_unique<TextWall>(TextWall{
-                Wall::Position::NegX, Wall::Role::Editor, std::move(t)
-            }));
-
-            // Advance left for the next token. on_WordPlaced already spaced
-            // it; nudge our cursor past where it landed.
-            z = snapped.z - 0.5;   // coarse step; place_OnLine handles exact gaps
-        }
-        y -= ConfigWall::line_height;
+    DynArray<std::string> lines;
+    for (const auto& toks : SourceTokens::tokenize_Lines(src)) {
+        std::string joined;
+        for (const std::string& tk : toks) { if (!joined.empty()) joined += ' '; joined += tk; }
+        lines.push_back(joined);
     }
+    editor_PlaceLines(lines);
 }
 
 
@@ -1297,10 +1236,10 @@ void GameState::sandbox_Restore() {
 void
 GameState::typing_RebuildPreview(const std::string& s)
 {
-    // Remove the previous preview words from the carry line.
     typing_ClearPreview();
 
-    auto [anchor, right, glyph_yaw] = anchorframe_Current();
+    const Vec3   anchor    = CarryMath::line_Anchor(*_ctx.camera, CarryMath::CARRY_DIST);
+    const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z);
     std::istringstream in(s);
     std::string tok;
     while (in >> tok) {
@@ -1308,7 +1247,7 @@ GameState::typing_RebuildPreview(const std::string& s)
                                   anchor, ConfigColor::floor);
         t->yaw = static_cast<float>(glyph_yaw);
         t->body.mode = PhysicsMode::Carried;
-        _typing_words.push_back(t.get());   // track raw ptr for later removal
+        _typing_words.push_back(t.get());
         _carried.push(std::move(t));
     }
 }
@@ -1317,11 +1256,9 @@ GameState::typing_RebuildPreview(const std::string& s)
 void
 GameState::typing_ClearPreview()
 {
-    // Preview words are the most recently pushed → at the tail. Pop exactly
-    // as many as we added; let them be destroyed (don't re-add to scene).
     for (size_t i = 0; i < _typing_words.size(); ++i) {
         if (_carried.empty()) break;
-        _carried.pop_back();   // returns Text3dPtr; dropping it frees the word
+        _carried.pop_back();
     }
     _typing_words.clear();
 }
@@ -1341,35 +1278,42 @@ GameState::carry_BeginAnim(Text3dPtr word, bool with_pitch)
     anim.t           = 0.0;
     anim.complete    = false;
 
-    auto [anchor, right, glyph_yaw] = anchorframe_Current();
-    anim.target_pos  = anchor;
-    anim.yaw_target  = static_cast<float>(glyph_yaw);
+    const Vec3   anchor    = CarryMath::line_Anchor(*_ctx.camera, CarryMath::CARRY_DIST);
+    const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z);
+    anim.target_pos = anchor;
+    anim.yaw_target = static_cast<float>(glyph_yaw);
 
     _anims_add.push_back(std::move(anim));
 }
 
 
 void
-GameState::connect_Begin(Conn c)
+GameState::wallconnect_BeginProgram()
 {
-    if (c != Conn::Program) return;   // Editor connect is Step 2
-    _conn          = c;
-    _conn_pult_pos = _pult_sys->get_ConnectProgramPos();
     _frame_input.capture_typing = true;
     _frame_input.typing_buffer.clear();
-    _conn_last.clear();
-    _conn_input.reset();
+    _wall_connection.begin_Program(_pult_sys->get_ConnectProgramPos());
 }
 
+
 void
-GameState::conn_Submit()
+GameState::wallconnect_BeginEditor()
 {
-    const std::string line = _frame_input.typing_buffer;
+    DynArray<std::string> lines = editor_AbsorbLines();
+    _frame_input.capture_vim = true;
+    _frame_input.vim_keys.clear();
+    _wall_connection.begin_Editor(_pult_sys->get_ConnectEditorPos(), lines, _ctx.camera->pos.y);
+}
+
+
+void
+GameState::on_WallConnectSubmit()
+{
+    std::string line = _frame_input.typing_buffer;
     _frame_input.typing_buffer.clear();
-    _conn_last.clear();
-    _conn_input.reset();
+    _wall_connection.clear_ProgramPreview();
 
-    if (line == ":exit") { connect_End(); return; }
+    if (line == ":exit") { on_WallConnectExit(); return; }
     if (line == ":help") {
         for (const char* h : { ":exit            leave the connection",
                                "ctrl+WASD        move while connected",
@@ -1377,107 +1321,35 @@ GameState::conn_Submit()
             _program_sys->echo_Input(_ctx.wall_texts, h);
         return;
     }
-
     _program_sys->feed_Input(line);
     _program_sys->echo_Input(_ctx.wall_texts, line);
 }
 
-void
-GameState::conn_RebuildPreview()
-{
-    Wall::Transform tf = Wall::get_Transform(Wall::Role::Program, World::get_BoxScale());
-    Vec3 pos = tf.position;
-    pos.y = ProgramSystem::start_y;
-
-    _conn_input = make_Text3d("> " + _frame_input.typing_buffer,
-                              Text3d::depth_wall, 0.003f, pos, ConfigColor::white);
-    _conn_input->yaw = tf.yaw;
-}
 
 void
-GameState::conn_DrawCursor(GlRenderer& gl)
+GameState::on_WallConnectExit()
 {
-    if (!_conn_input) return;
-    if (SDL_fmod(_conn_blink, 0.8) > 0.4) return;
-
-    Vec3 c = _conn_input->pos;
-    c.z   = _conn_input->pos.z - (static_cast<double>(_conn_input->half_extent) + 0.15);
-
-    Mat4 view  = buildViewMat4(*_ctx.camera);
-    Mat4 model = buildModelMat4(c, _conn_input->yaw);
-    Mat4 mvp   = Mat4::mul(gl.projection(), Mat4::mul(view, model));
-
-    gl.set_Wireframe(false);
-    gl.set_MVP(model, mvp);
-    gl.set_NormalMat(model);
-    gl.set_Light(*_ctx.light);
-    gl.set_Color(0.6f, 1.0f, 1.0f);
-    gl.set_Emissive(0.6f, 1.0f, 1.0f, 0.9f);
-    gl.draw_Box(0.04f, 0.22f, 0.02f);
-    gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f);
-}
-
-void
-GameState::conn_DrawElectric(GlRenderer& gl)
-{
-    const Vec3 a = _ctx.camera->pos;
-    const Vec3 b = _conn_pult_pos;
-
-    constexpr int SEG = 16;
-    DynArray<float> pts;
-    pts.reserve(SEG * 6);
-    Vec3 prev = a;
-    for (int i = 1; i <= SEG; ++i) {
-        const double t = static_cast<double>(i) / SEG;
-        Vec3 p { a.x + (b.x - a.x) * t,
-                 a.y + (b.y - a.y) * t,
-                 a.z + (b.z - a.z) * t };
-        if (i < SEG) {                                  // jitter the interior for the spark look
-            const double j = 0.18;
-            p.x += ((rand() % 1000) / 1000.0 - 0.5) * j;
-            p.y += ((rand() % 1000) / 1000.0 - 0.5) * j;
-            p.z += ((rand() % 1000) / 1000.0 - 0.5) * j;
-        }
-        pts.push_back((float)prev.x); pts.push_back((float)prev.y); pts.push_back((float)prev.z);
-        pts.push_back((float)p.x);    pts.push_back((float)p.y);    pts.push_back((float)p.z);
-        prev = p;
-    }
-
-    Mat4 ident = Mat4::identity();
-    Mat4 mvp   = Mat4::mul(gl.projection(), buildViewMat4(*_ctx.camera));
-    gl.set_MVP(ident, mvp);
-    gl.set_NormalMat(ident);
-    gl.set_Wireframe(true);                 // wireframe path emits u_Color directly
-    gl.set_Color(0.5f, 0.9f, 1.0f);
-    gl.draw_Lines(pts.data(), static_cast<int>(pts.size()) / 3);
-    gl.set_Wireframe(false);
+    _wall_connection.end();
+    _frame_input.capture_typing = false;
+    _frame_input.typing_buffer.clear();
 }
 
 
 void
-GameState::connect_BeginEditor()
+GameState::editor_Clear()
 {
-    _conn          = Conn::Editor;
-    _conn_pult_pos = _pult_sys->get_ConnectEditorPos();
-    _frame_input.capture_vim = true;
-    _edit_status.clear();
-
-    editor_Absorb();                  // loads _vim, removes real wall words
-
-    if (_vim.is_Empty())
-        _vim.start_Insert();          // empty wall → fresh line, ready to type
-    // else: load() left cursor at row 0 / col 0 in Normal mode
-
-    _vim_seen_version = _vim.version();
-    editor_RebuildRender();
+    _editor_wall.clear();
+    _ctx.wall_texts.erase(
+        std::remove_if(_ctx.wall_texts.begin(), _ctx.wall_texts.end(),
+            [](const TextWallPtr& wt){ return wt->role == Wall::Role::Editor; }),
+        _ctx.wall_texts.end());
 }
 
-void
-GameState::editor_Absorb()
+
+DynArray<std::string>
+GameState::editor_AbsorbLines()
 {
-    // Pull existing editor source (lines joined by spaces, '\n' separated).
     std::string src = TextWall::extract_Text(_ctx.wall_texts, Wall::Role::Editor);
-
     DynArray<std::string> lines;
     std::string cur;
     for (char ch : src) {
@@ -1486,246 +1358,8 @@ GameState::editor_Absorb()
     }
     if (!cur.empty()) lines.push_back(cur);
 
-    // VimBuffer wants std::vector; convert (DynArray ↔ vector).
-    std::vector<std::string> v(lines.begin(), lines.end());
-    _vim.load(v);
-
-    // Remove the real editor words — the buffer is the source of truth now.
-    _editor_wall.clear();
-    _ctx.wall_texts.erase(
-        std::remove_if(_ctx.wall_texts.begin(), _ctx.wall_texts.end(),
-            [](const TextWallPtr& wt){ return wt->role == Wall::Role::Editor; }),
-        _ctx.wall_texts.end());
-}
-
-
-void
-GameState::editor_RebuildRender()
-{
-    _edit_render.clear();
-    _edit_linenums.clear();
-
-    const float ws = World::get_BoxScale();
-    Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws);
-    const double lh    = ConfigWall::line_height;
-    const double eye_y = _ctx.camera->pos.y;
-    const int    R     = _vim.row();
-    const int    N     = (int)_vim.lines().size();
-    const double bottom_y = -static_cast<double>(ws) + 1.2;
-
-    const double anchor_cursor = eye_y + static_cast<double>(R) * lh;
-    const double anchor_bottom = bottom_y + static_cast<double>(N - 1) * lh;
-    _edit_row0_y = SDL_min(anchor_cursor, anchor_bottom);
-
-    const auto& lines = _vim.lines();
-
-    // ── Pass 1: build the renderables in a LOCAL left-aligned frame ──────────
-    // u = 0 at block's left edge, increasing rightward. We track each line's
-    // text width to find the block width (gutter + widest line).
-    struct Row { Text3dPtr num; Text3dPtr txt; double txt_w; };
-    DynArray<Row> rows;
-    rows.reserve(lines.size());
-
-    // Gutter width: built from the widest line-number ("N"), plus a gap.
-    // Build numbers first to measure.
-    double gutter_w = 0.0;
-    for (size_t i = 0; i < lines.size(); ++i) {
-        Text3dPtr num = make_Text3d(std::to_string(i + 1), Text3d::depth_wall, 0.0022f,
-                                    tf.position, ConfigColor::editor);
-        num->yaw = tf.yaw;
-        gutter_w = SDL_max(gutter_w, 2.0 * static_cast<double>(num->half_extent));
-        Row r; r.num = std::move(num); rows.push_back(std::move(r));
-    }
-    const double gutter_gap  = 0.6;                 // space between number and code
-    const double code_u0     = gutter_w + gutter_gap;   // code's left edge in u
-
-    double block_w = code_u0;   // at least the gutter; grows with widest line
-    for (size_t i = 0; i < lines.size(); ++i) {
-        const bool drawable = lines[i].find_first_not_of(' ') != std::string::npos;
-        if (drawable) {
-            Text3dPtr t = make_Text3d(lines[i], Text3d::depth_wall, 0.003f,
-                                      tf.position, ConfigColor::wall);
-            t->yaw = tf.yaw;
-            rows[i].txt_w = 2.0 * static_cast<double>(t->half_extent);
-            rows[i].txt   = std::move(t);
-        } else {
-            rows[i].txt_w = 0.0;
-        }
-        block_w = SDL_max(block_w, code_u0 + rows[i].txt_w);
-    }
-
-    // ── Pass 2: center the block on the wall (z = 0) and emit ────────────────
-    // Block spans u ∈ [0, block_w]; we want it centered, so its world left edge
-    // (largest z) is at z = block_w/2, and z = z_left - u.
-    const double z_block_left = block_w * 0.5;
-
-    for (size_t i = 0; i < rows.size(); ++i) {
-        const double row_y = _edit_row0_y - static_cast<double>(i) * lh;
-
-        // number: left-aligned at u = 0 → its left edge sits at z_block_left
-        if (rows[i].num) {
-            const double he = static_cast<double>(rows[i].num->half_extent);
-            rows[i].num->pos   = tf.position;
-            rows[i].num->pos.y = row_y;
-            rows[i].num->pos.z = z_block_left - he;     // left edge at u=0, center inward
-            _edit_linenums.push_back(std::move(rows[i].num));
-        }
-
-        // code: left edge at u = code_u0
-        if (rows[i].txt) {
-            const double he = static_cast<double>(rows[i].txt->half_extent);
-            rows[i].txt->pos   = tf.position;
-            rows[i].txt->pos.y = row_y;
-            rows[i].txt->pos.z = z_block_left - code_u0 - he;   // left-aligned within block
-            _edit_render.push_back(std::move(rows[i].txt));
-        } else {
-            _edit_render.push_back(Text3dPtr{});        // keep row index alignment
-        }
-    }
-
-    // stash for the cursor (which needs the code column's world left edge)
-    _edit_code_left_z = z_block_left - code_u0;
-
-    editor_RebuildCmdline();
-}
-
-
-void
-GameState::editor_RebuildCmdline()
-{
-    _edit_cmdline.reset();
-
-    std::string text;
-    if (_vim.mode() == VimBuffer::Mode::Command) text = _vim.cmdline();   // includes ':'
-    else if (!_edit_status.empty())              text = _edit_status[0];  // help/first status line
-    else if (!_vim.status().empty())             text = _vim.status();
-    else {
-        switch (_vim.mode()) {
-            case VimBuffer::Mode::Insert: text = "-- INSERT --"; break;
-            default:                      text = "-- NORMAL --"; break;
-        }
-    }
-    if (text.empty()) return;
-
-    const float ws = World::get_BoxScale();
-    Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws);
-
-    Vec3 pos = tf.position;
-    pos.x += 0.8;                              // pull toward room center (off the wall, in front)
-    pos.y  = -static_cast<double>(ws) + 0.8;   // fixed near the floor — always visible
-    pos.z  = static_cast<double>(ws) - 1.0;    // left-aligned with the gutter
-
-    _edit_cmdline = make_Text3d(text, Text3d::depth_wall, 0.0028f, pos, ConfigColor::white);
-    _edit_cmdline->yaw   = tf.yaw;
-    _edit_cmdline->pos.z = 0.0;
-}
-
-
-void
-GameState::editor_FeedVim(DynArray<int>& keys)
-{
-    for (int k : keys) {
-        if      (k == -1) _vim.key_enter();
-        else if (k == -2) _vim.key_escape();
-        else if (k == -3) _vim.key_backspace();
-        else if (k  >  0) _vim.key_char(static_cast<char>(k));
-    }
-
-    switch (_vim.take_Signal()) {
-        case VimBuffer::Signal::Exit:  editor_CommitToWall(); connect_End(); return;
-        case VimBuffer::Signal::Write: editor_CommitToWall(); editor_Absorb();
-                                       _vim_seen_version = _vim.version() - 1; break;
-        case VimBuffer::Signal::Help:
-            _edit_status.clear();
-            _edit_status.push_back("-- VIM HELP --");
-            _edit_status.push_back("i a A o O  insert    Esc normal    h j k l move");
-            _edit_status.push_back("0 $  line ends   gg G :N  jump line   x dd  delete");
-            _edit_status.push_back(":w write to wall   :exit leave   ctrl+WASD move");
-            break;
-        default: break;
-    }
-
-    if (_vim.version() != _vim_seen_version) {
-        _vim_seen_version = _vim.version();
-        editor_RebuildRender();
-    }
-}
-
-void
-GameState::editor_CommitToWall()
-{
-    // buffer lines → real editor-wall words (throwing/compile/scroll work again)
-    DynArray<std::string> lines(_vim.lines().begin(), _vim.lines().end());
-    editor_PlaceLines(lines);
-    _edit_render.clear();
-    _edit_status.clear();
-}
-
-void
-GameState::connect_End()   // editor-aware version (replaces the Step-1 one)
-{
-    if (_conn == Conn::Editor) {
-        _frame_input.capture_vim = false;
-        _frame_input.vim_keys.clear();
-        _edit_render.clear();
-        _edit_linenums.clear();
-        _edit_cmdline.reset();
-        _edit_status.clear();
-    } else {
-        _frame_input.capture_typing = false;
-        _frame_input.typing_buffer.clear();
-        _conn_last.clear();
-        _conn_input.reset();
-    }
-    _conn = Conn::None;
-}
-
-void
-GameState::edit_DrawCursor(GlRenderer& gl)
-{
-    if (_conn != Conn::Editor) return;
-    if (SDL_fmod(_conn_blink, 0.8) > 0.4) return;
-
-    const int row = _vim.row();
-    if (row < 0 || row >= (int)_vim.lines().size()) return;
-
-    const float ws      = World::get_BoxScale();
-    Wall::Transform tf  = Wall::get_Transform(Wall::Role::Editor, ws);
-    const double lh     = ConfigWall::line_height;
-
-    const std::string& s = _vim.lines()[(size_t)row];
-    const double row_y   = _edit_row0_y - static_cast<double>(row) * lh;   // same formula
-
-    double cw, line_x = tf.position.x, he_y = 0.2;
-    if (row < (int)_edit_render.size() && _edit_render[(size_t)row]) {
-        const Text3dPtr& line = _edit_render[(size_t)row];
-        const int N = s.empty() ? 1 : (int)s.size();
-        cw     = (2.0 * static_cast<double>(line->half_extent)) / static_cast<double>(N);
-        line_x = line->pos.x;
-        he_y   = static_cast<double>(line->half_extent_y);
-    } else {
-        cw = 0.18;   // empty line: nominal advance
-    }
-
-    const int    col = _vim.col();
-    // left edge of the code column (set in editor_RebuildRender after centering)
-    const double zL  = _edit_code_left_z;
-    const double cz  = zL - (static_cast<double>(col) + 0.5) * cw;
-
-    Vec3 c { line_x, row_y - (he_y + 0.06), cz };   // underline: just below the glyphs
-
-    Mat4 view  = buildViewMat4(*_ctx.camera);
-    Mat4 model = buildModelMat4(c, tf.yaw);
-    Mat4 mvp   = Mat4::mul(gl.projection(), Mat4::mul(view, model));
-
-    gl.set_Wireframe(false);
-    gl.set_MVP(model, mvp);
-    gl.set_NormalMat(model);
-    gl.set_Light(*_ctx.light);
-    gl.set_Color(0.6f, 1.0f, 1.0f);
-    gl.set_Emissive(0.6f, 1.0f, 1.0f, 0.9f);
-    gl.draw_Box(static_cast<float>(cw * 0.45), 0.015f, 0.03f);   // wide+thin = underline
-    gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f);
+    editor_Clear();
+    return lines;
 }
 
 
@@ -1756,3 +1390,9 @@ GameState::editor_PlaceLines(const DynArray<std::string>& lines)
         y -= ConfigWall::line_height;
     }
 }
+
+void
+GameState::editor_CommitToWall(const DynArray<std::string>& lines)
+{
+    editor_PlaceLines(lines);
+}
diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp
index 9c7e67e..93961c6 100644
--- a/src/states/game_state.hpp
+++ b/src/states/game_state.hpp
@@ -23,6 +23,7 @@
 #include "systems/program_system.hpp"
 #include "systems/projectile_system.hpp"
 #include "systems/pult_system.hpp"
+#include "systems/wall_connection.hpp"
 #include "systems/word_ai_system.hpp"
 #include "systems/word_anim.hpp"
 #include "text_3d/vim_buffer.hpp"
@@ -71,38 +72,20 @@ class GameState : public IState
     double _shoot_cooldown { 0.0 };
     double _step_age       { 0.0 };
 
-    // TODO START: OBJECT ORIENT ME 
-    VimBuffer             _vim;
-    unsigned              _vim_seen_version { 0 };
-    DynArray<Text3dPtr>   _edit_render;
-    DynArray<std::string> _edit_status;
-    double _edit_code_left_z { 0.0 };   // world z of the code column's left edge (for cursor)
-    double _edit_row0_y { 0.0 };   // world-y of buffer row 0 (scroll anchor)
-
-    enum class Conn : uint8_t { None, Program, Editor };
-    Conn        _conn        { Conn::None };
-    std::string _conn_last;            // last previewed buffer
-    Text3dPtr   _conn_input;           // live typed-line preview on the wall
-    double      _conn_blink  { 0.0 };  // cursor blink accumulator
-    Vec3        _conn_pult_pos {};     // anchor for the electric line
-    void connect_Begin       (Conn c);
-    void connect_End         ();
-    void conn_Submit         ();
-    void conn_RebuildPreview ();
-    void conn_DrawCursor     (GlRenderer& gl);
-    void conn_DrawElectric   (GlRenderer& gl);
-    void connect_BeginEditor ();
-    void editor_Absorb       ();
-    void editor_RebuildRender();
-    void editor_CommitToWall ();
-    void editor_FeedVim      (DynArray<int>& keys);
-    void edit_DrawCursor     (GlRenderer& gl);
+
+    WallConnection _wall_connection;
+
+    void wallconnect_BeginProgram();
+    void wallconnect_BeginEditor ();
+    void on_WallConnectSubmit       ();
+    void on_WallConnectExit         ();
+
+    DynArray<std::string> editor_AbsorbLines();
+    void editor_Clear        ();
+    void editor_CommitToWall (const DynArray<std::string>& lines);
     void editor_PlaceLines   (const DynArray<std::string>& lines);
-    void editor_RebuildCmdline();
-    DynArray<Text3dPtr> _edit_linenums;   // per-row "1,2,3…" labels (left gutter)
-    Text3dPtr           _edit_cmdline;    // pinned command/status bar (in front of wall)
-    // TODO END: OBJECT ORIENT ME 
 
+    DynArray<Vec3>      _freelook_return_from;
 
     DynArray<WallLabel> _wall_labels;
 
@@ -131,7 +114,7 @@ class GameState : public IState
     cpputils::unique_ptr<ParticleSystem>   _particle_sys;
     cpputils::unique_ptr<FloorMandelbrot>  _floor_sys;
     cpputils::unique_ptr<LevelRunner>      _level_runner_sys;
-    cpputils::unique_ptr<DynLoaderSystem>        _dyn_loader_sys;
+    cpputils::unique_ptr<DynLoaderSystem>  _dyn_loader_sys;
     cpputils::unique_ptr<CelebrationSystem>_celebration_sys;
 
 
@@ -181,9 +164,6 @@ class GameState : public IState
 
     void debug_SpawnWords    (size_t n);
 
-    std::tuple<Vec3, Vec3, double>
-         anchorframe_Current () const;
-
     void handle_Input       (FrameInput&, double dt);
     void update_PlayerCamera(FrameInput& input, double dt);
 
diff --git a/src/systems/freelook_system.cpp b/src/systems/freelook_system.cpp
index d28add3..42e0de5 100644
--- a/src/systems/freelook_system.cpp
+++ b/src/systems/freelook_system.cpp
@@ -7,21 +7,16 @@ FREELOOK_LERP_DUR = 0.20;
 
 
 void
-FreelookSystem::begin(const Vec3& anchor, const Vec3& right, double glyph_yaw)
+FreelookSystem::begin()
 {
-    state.active           = true;
-    state.lerping          = false;
-    state.frozen_anchor    = anchor;
-    state.frozen_right     = right;
-    state.frozen_glyph_yaw = glyph_yaw;
+    state.active  = true;
+    state.lerping = false;
 }
 
 
 void
 FreelookSystem::end()
 {
-    // Hand control of the carry anchor back to the live camera frame and let
-    // the line ease there. The camera angle is NOT changed.
     state.active  = false;
     state.lerping = true;
     state.lerp_t  = 0.0;
@@ -39,26 +34,3 @@ FreelookSystem::update(double dt)
         state.lerping = false;
     }
 }
-
-
-void
-FreelookSystem::look_At(const Vec3& eye, const Vec3& target,
-                              uint32_t& out_yaw, int32_t& out_pitch)
-{
-    double dx = target.x - eye.x;
-    double dy = target.y - eye.y;
-    double dz = target.z - eye.z;
-
-    double yaw_r   = SDL_atan2(-dx, -dz);
-    double xz_dist = SDL_sqrt(dx*dx + dz*dz);
-    double pitch_r = SDL_atan2(dy, xz_dist);
-
-    const double bin = SDL_PI_D / 2147483648.0;
-
-    out_yaw   = static_cast<uint32_t>(static_cast<int32_t>(yaw_r / bin));
-    out_pitch = std::clamp(
-        static_cast<int32_t>(pitch_r / bin),
-        INT32_MIN / 2,
-        INT32_MAX / 2
-    );
-}
diff --git a/src/systems/freelook_system.hpp b/src/systems/freelook_system.hpp
index 1d138e4..facedb5 100644
--- a/src/systems/freelook_system.hpp
+++ b/src/systems/freelook_system.hpp
@@ -5,37 +5,16 @@
 
 struct FreelookState {
     bool   active{};
-
-    Vec3   frozen_anchor{};
-    Vec3   frozen_right{};
-    double frozen_glyph_yaw{};
-
-    bool     lerping{};
-    double   lerp_t{};
-    uint32_t lerp_yaw_start{};
-    int32_t  lerp_pitch_start{};
-    uint32_t lerp_yaw_target{};
-    int32_t  lerp_pitch_target{};
+    bool   lerping{};
+    double lerp_t{};
 };
 
+
 class FreelookSystem {
 public:
     FreelookState state;
 
-    // Call when shift pressed — freezes anchor from current camera+carried state
-    void
-    begin(const Vec3& anchor, const Vec3& right, double glyph_yaw);
-
-    // Call when shift released — starts the carried-line fly-back. The camera
-    // is left exactly where the player is looking.
-    void
-    end();
-
-    // Call every frame — advances the fly-back timer only.
-    void
-    update(double dt);
-
-    static void
-    look_At(const Vec3& eye, const Vec3& target,
-            uint32_t& out_yaw, int32_t& out_pitch);
+    void begin ();
+    void end   ();
+    void update(double dt);
 };
diff --git a/src/systems/wall_connection.cpp b/src/systems/wall_connection.cpp
new file mode 100644
index 0000000..a906076
--- /dev/null
+++ b/src/systems/wall_connection.cpp
@@ -0,0 +1,342 @@
+#include <SDL3/SDL.h>
+#include <cstdlib>
+
+#include "entities/camera.hpp"
+#include "entities/world.hpp"
+#include "rendering/gl_renderer.hpp"
+#include "systems/program_system.hpp"
+#include "systems/wall_connection.hpp"
+#include "utils/config.hpp"
+#include "utils/light.hpp"
+#include "wall/wall.hpp"
+
+
+void
+WallConnection::begin_Program(const Vec3& pult_pos)
+{
+    _target   = Target::Program;
+    _pult_pos = pult_pos;
+    _prog_input.reset();
+    _prog_last.clear();
+}
+
+void
+WallConnection::begin_Editor(const Vec3& pult_pos, const DynArray<std::string>& lines, double eye_y)
+{
+    _target   = Target::Editor;
+    _pult_pos = pult_pos;
+    load_Editor(lines, eye_y);
+}
+
+void
+WallConnection::load_Editor(const DynArray<std::string>& lines, double eye_y)
+{
+    std::vector<std::string> v(lines.begin(), lines.end());
+    _vim.load(v);
+    if (_vim.is_Empty()) _vim.start_Insert();
+    _vim_seen_version = _vim.version();
+    _eye_y = eye_y;
+    _status.clear();
+    rebuild_Editor();
+}
+
+void
+WallConnection::end()
+{
+    _target = Target::None;
+    _prog_input.reset();  _prog_last.clear();
+    _render.    clear();  _linenums. clear();
+    _cmdline.   reset();  _status.   clear();
+    _blink = 0.0;
+}
+
+
+void
+WallConnection::set_ProgramBuffer(const std::string& buffer)
+{
+    if (buffer == _prog_last) return;
+    rebuild_ProgramPreview(buffer);
+    _prog_last = buffer;
+}
+
+void
+WallConnection::clear_ProgramPreview()
+{
+    _prog_input.reset();
+    _prog_last. clear();
+}
+
+void
+WallConnection::rebuild_ProgramPreview(const std::string& buffer)
+{
+    Wall::Transform tf = Wall::get_Transform(Wall::Role::Program, World::get_BoxScale());
+    Vec3 pos = tf.position;
+    pos.y    = ProgramSystem::start_y;
+
+    _prog_input      = make_Text3d("> " + buffer, Text3d::depth_wall, 0.003f, pos, ConfigColor::white);
+    _prog_input->yaw = tf.yaw;
+}
+
+
+WallConnection::Request
+WallConnection::feed_Vim(DynArray<int>& keys, double eye_y)
+{
+    _eye_y = eye_y;
+
+    for (int k : keys) {
+        if      (k == -1) _vim.key_enter();
+        else if (k == -2) _vim.key_escape();
+        else if (k == -3) _vim.key_backspace();
+        else if (k  >  0) _vim.key_char(static_cast<char>(k));
+    }
+
+    Request req = Request::None;
+    switch (_vim.take_Signal()) {
+        case VimBuffer::Signal::Exit:  return Request::Exit;
+        case VimBuffer::Signal::Write: req = Request::EditorWrite; break;
+        case VimBuffer::Signal::Help:
+            _status.clear();
+            _status.push_back("-- VIM HELP --");
+            _status.push_back("i a A o O  insert    Esc normal    h j k l move");
+            _status.push_back("0 $  line ends   gg G :N  jump line   x dd  delete");
+            _status.push_back(":w write to wall   :exit leave   ctrl+WASD move");
+            break;
+        default: break;
+    }
+
+    if (_vim.version() != _vim_seen_version) {
+        _vim_seen_version = _vim.version();
+        rebuild_Editor();
+    }
+    return req;
+}
+
+DynArray<std::string>
+WallConnection::lines() const
+{
+    DynArray<std::string> out;
+    for (const std::string& l : _vim.lines()) out.push_back(l);
+    return out;
+}
+
+
+void
+WallConnection::rebuild_Editor()
+{
+    _render.clear();
+    _linenums.clear();
+
+    const float ws = World::get_BoxScale();
+    Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws);
+    const double lh    = ConfigWall::line_height;
+    const int    R     = _vim.row();
+    const int    N     = (int)_vim.lines().size();
+    const double bottom_y = -static_cast<double>(ws) + 1.2;
+
+    const double anchor_cursor = _eye_y + static_cast<double>(R) * lh;
+    const double anchor_bottom = bottom_y + static_cast<double>(N - 1) * lh;
+    _row0_y = SDL_min(anchor_cursor, anchor_bottom);
+
+    const auto& lines = _vim.lines();
+
+    // Pass 1: build renderables in a local left-aligned frame, find block width.
+    struct Row { Text3dPtr num; Text3dPtr txt; double txt_w; };
+    DynArray<Row> rows;
+    rows.reserve(lines.size());
+
+    double gutter_w = 0.0;
+    for (size_t i = 0; i < lines.size(); ++i) {
+        Text3dPtr num = make_Text3d(std::to_string(i + 1), Text3d::depth_wall, 0.0022f,
+                                    tf.position, ConfigColor::editor);
+        num->yaw = tf.yaw;
+        gutter_w = SDL_max(gutter_w, 2.0 * static_cast<double>(num->half_extent));
+        Row r; r.num = std::move(num); r.txt_w = 0.0; rows.push_back(std::move(r));
+    }
+    const double gutter_gap = 0.6;
+    const double code_u0    = gutter_w + gutter_gap;
+
+    double block_w = code_u0;
+    for (size_t i = 0; i < lines.size(); ++i) {
+        const bool drawable = lines[i].find_first_not_of(' ') != std::string::npos;
+        if (drawable) {
+            Text3dPtr t = make_Text3d(lines[i], Text3d::depth_wall, 0.003f, tf.position, ConfigColor::wall);
+            t->yaw      = tf.yaw;
+            rows[i].txt_w = 2.0 * static_cast<double>(t->half_extent);
+            rows[i].txt   = std::move(t);
+        }
+        block_w = SDL_max(block_w, code_u0 + rows[i].txt_w);
+    }
+
+    // Pass 2: center the whole block on the wall (z = block_w/2 → -block_w/2).
+    const double z_block_left = block_w * 0.5;
+
+    for (size_t i = 0; i < rows.size(); ++i) {
+        const double row_y = _row0_y - static_cast<double>(i) * lh;
+
+        if (rows[i].num) {
+            const double he = static_cast<double>(rows[i].num->half_extent);
+            rows[i].num->pos   = tf.position;
+            rows[i].num->pos.y = row_y;
+            rows[i].num->pos.z = z_block_left - he;
+            _linenums.push_back(std::move(rows[i].num));
+        }
+        if (rows[i].txt) {
+            const double he = static_cast<double>(rows[i].txt->half_extent);
+            rows[i].txt->pos   = tf.position;
+            rows[i].txt->pos.y = row_y;
+            rows[i].txt->pos.z = z_block_left - code_u0 - he;
+            _render.push_back(std::move(rows[i].txt));
+        } else {
+            _render.push_back(Text3dPtr{});
+        }
+    }
+
+    _code_left_z = z_block_left - code_u0;
+    rebuild_Cmdline();
+}
+
+void
+WallConnection::rebuild_Cmdline()
+{
+    _cmdline.reset();
+
+    std::string text;
+    if      (_vim.mode() == VimBuffer::Mode::Command) text = _vim.cmdline();
+    else if (!_status.empty())                        text = _status[0];
+    else if (!_vim.status().empty())                  text = _vim.status();
+    else text = (_vim.mode() == VimBuffer::Mode::Insert) ? "-- INSERT --" : "-- NORMAL --";
+    if (text.empty()) return;
+
+    const float ws = World::get_BoxScale();
+    Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws);
+
+    Vec3 pos = tf.position;
+    pos.x += 0.8;
+    pos.y  = -static_cast<double>(ws) + 0.8;
+    pos.z  = 0.0;
+
+    _cmdline = make_Text3d(text, Text3d::depth_wall, 0.0028f, pos, ConfigColor::white);
+    _cmdline->yaw = tf.yaw;
+}
+
+
+void
+WallConnection::render(GlRenderer& gl, const Camera& cam, const Light& light) const
+{
+    if (_target == Target::Program) {
+        if (_prog_input) _prog_input->draw(gl, cam, light);
+
+        draw_ProgramCursor(gl, cam, light);
+        draw_Electric     (gl, cam);
+    } else if (_target == Target::Editor) {
+        for (const Text3dPtr& t : _render)   if (t) t->draw(gl, cam, light);
+        for (const Text3dPtr& n : _linenums) if (n) n->draw(gl, cam, light);
+
+        if (_cmdline) _cmdline->draw(gl, cam, light);
+
+        draw_EditorCursor(gl, cam, light);
+        draw_Electric    (gl, cam);
+    }
+}
+
+void
+WallConnection::draw_ProgramCursor(GlRenderer& gl, const Camera& cam, const Light& light) const
+{
+    if (!_prog_input) return;
+    if (SDL_fmod(_blink, 0.8) > 0.4) return;
+
+    Vec3 c = _prog_input->pos;
+    c.z    = _prog_input->pos.z - (static_cast<double>(_prog_input->half_extent) + 0.15);
+
+    Mat4 view  = buildViewMat4(cam);
+    Mat4 model = buildModelMat4(c, _prog_input->yaw);
+    Mat4 mvp   = Mat4::mul(gl.projection(), Mat4::mul(view, model));
+
+    gl.set_Wireframe(false);
+    gl.set_MVP(model, mvp);
+    gl.set_NormalMat(model);
+    gl.set_Light(light);
+    gl.set_Color(0.6f, 1.0f, 1.0f);
+    gl.set_Emissive(0.6f, 1.0f, 1.0f, 0.9f);
+    gl.draw_Box(0.04f, 0.22f, 0.02f);
+    gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f);
+}
+
+void
+WallConnection::draw_EditorCursor(GlRenderer& gl, const Camera& cam, const Light& light) const
+{
+    if (SDL_fmod(_blink, 0.8) > 0.4) return;
+
+    const int row = _vim.row();
+    if (row < 0 || row >= (int)_vim.lines().size()) return;
+
+    const float ws     = World::get_BoxScale();
+    Wall::Transform tf = Wall::get_Transform(Wall::Role::Editor, ws);
+    const double lh    = ConfigWall::line_height;
+
+    const std::string& s = _vim.lines()[(size_t)row];
+    const double row_y   = _row0_y - static_cast<double>(row) * lh;
+
+    double cw, line_x = tf.position.x, he_y = 0.2;
+    if (row < (int)_render.size() && _render[(size_t)row]) {
+        const Text3dPtr& line = _render[(size_t)row];
+        const int N = s.empty() ? 1 : (int)s.size();
+        cw     = (2.0 * static_cast<double>(line->half_extent)) / static_cast<double>(N);
+        line_x = line->pos.x;
+        he_y   = static_cast<double>(line->half_extent_y);
+    } else {
+        cw = 0.18;
+    }
+
+    const int    col = _vim.col();
+    const double cz  = _code_left_z - (static_cast<double>(col) + 0.5) * cw;
+
+    Vec3 c { line_x, row_y - (he_y + 0.06), cz };
+
+    Mat4 view  = buildViewMat4(cam);
+    Mat4 model = buildModelMat4(c, tf.yaw);
+    Mat4 mvp   = Mat4::mul(gl.projection(), Mat4::mul(view, model));
+
+    gl.set_Wireframe(false);
+    gl.set_MVP(model, mvp);
+    gl.set_NormalMat(model);
+    gl.set_Light(light);
+    gl.set_Color(0.6f, 1.0f, 1.0f);
+    gl.set_Emissive(0.6f, 1.0f, 1.0f, 0.9f);
+    gl.draw_Box(static_cast<float>(cw * 0.45), 0.015f, 0.03f);
+    gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f);
+}
+
+void
+WallConnection::draw_Electric(GlRenderer& gl, const Camera& cam) const
+{
+    const Vec3 a = cam.pos;
+    const Vec3 b = _pult_pos;
+
+    constexpr int SEG = 16;
+    DynArray<float> pts;
+    pts.reserve(SEG * 6);
+    Vec3 prev = a;
+    for (int i = 1; i <= SEG; ++i) {
+        const double t = static_cast<double>(i) / SEG;
+        Vec3 p { a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t };
+        if (i < SEG) {
+            const double j = 0.18;
+            p.x += ((rand() % 1000) / 1000.0 - 0.5) * j;
+            p.y += ((rand() % 1000) / 1000.0 - 0.5) * j;
+            p.z += ((rand() % 1000) / 1000.0 - 0.5) * j;
+        }
+        pts.push_back((float)prev.x); pts.push_back((float)prev.y); pts.push_back((float)prev.z);
+        pts.push_back((float)p.x);    pts.push_back((float)p.y);    pts.push_back((float)p.z);
+        prev = p;
+    }
+
+    Mat4 ident = Mat4::identity();
+    Mat4 mvp   = Mat4::mul(gl.projection(), buildViewMat4(cam));
+    gl.set_MVP(ident, mvp);
+    gl.set_NormalMat(ident);
+    gl.set_Wireframe(true);
+    gl.set_Color(0.5f, 0.9f, 1.0f);
+    gl.draw_Lines(pts.data(), static_cast<int>(pts.size()) / 3);
+    gl.set_Wireframe(false);
+}
diff --git a/src/systems/wall_connection.hpp b/src/systems/wall_connection.hpp
new file mode 100644
index 0000000..5735810
--- /dev/null
+++ b/src/systems/wall_connection.hpp
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include "text_3d/text_3d.hpp"
+#include "text_3d/vim_buffer.hpp"
+#include "utils/globals.hpp"
+#include "utils/math.hpp"
+
+class Camera;
+class GlRenderer;
+struct Light;
+
+class WallConnection
+{
+public:
+    enum class Target  : uint8_t { None, Program, Editor };
+    enum class Request : uint8_t { None, Exit, EditorWrite };
+
+    bool   active() const { return _target != Target::None; }
+    Target target() const { return _target; }
+
+    void begin_Program(const Vec3& pult_pos);
+    void begin_Editor (const Vec3& pult_pos, const DynArray<std::string>& lines, double eye_y);
+    void load_Editor  (const DynArray<std::string>& lines, double eye_y);
+    void end();
+
+    void set_ProgramBuffer   (const std::string& buffer);
+    void clear_ProgramPreview();
+
+    Request feed_Vim(DynArray<int>& keys, double eye_y);
+    DynArray<std::string>
+            lines   () const;
+
+    void update(double dt) { if (active()) _blink += dt; }
+    void render(GlRenderer& gl, const Camera& cam, const Light& light) const;
+
+private:
+    Target _target { Target::None };
+    Vec3   _pult_pos {};
+    double _blink { 0.0 };
+
+    Text3dPtr   _prog_input;
+    std::string _prog_last;
+
+    VimBuffer             _vim;
+    unsigned              _vim_seen_version { 0 };
+    DynArray<Text3dPtr>   _render;
+    DynArray<Text3dPtr>   _linenums;
+    Text3dPtr             _cmdline;
+    DynArray<std::string> _status;
+    double _row0_y      { 0.0 };
+    double _code_left_z { 0.0 };
+    double _eye_y       { 0.0 };
+
+    void rebuild_Editor ();
+    void rebuild_Cmdline();
+    void rebuild_ProgramPreview(const std::string& buffer);
+    void draw_EditorCursor (GlRenderer& gl, const Camera& cam, const Light& light) const;
+    void draw_ProgramCursor(GlRenderer& gl, const Camera& cam, const Light& light) const;
+    void draw_Electric     (GlRenderer& gl, const Camera& cam) const;
+};
diff --git a/src/wall/text_wall_scroll.hpp b/src/wall/text_wall_scroll.hpp
index a71ee9e..5fa89c9 100644
--- a/src/wall/text_wall_scroll.hpp
+++ b/src/wall/text_wall_scroll.hpp
@@ -1,18 +1,12 @@
 #pragma once
-// ─────────────────────────────────────────────────────────────────────────────
-// TextWall scroll utilities
-//
-// These free functions live alongside TextWall and operate on TextWallList.
-// Include this header wherever you need to scroll or clear wall content.
-// ─────────────────────────────────────────────────────────────────────────────
 
+#include "entities/world.hpp"
 #include "text_wall.hpp"
 #include "utils/config.hpp"
 #include "wall/wall.hpp"
 
 namespace TextWallScroll
 {
-    // Scroll all words on the given wall role up (+) or down (-)
     inline void
     scroll(TextWallList& texts, Wall::Role role, float delta)
     {
@@ -31,27 +25,23 @@ namespace TextWallScroll
     scroll_Down(TextWallList& texts, Wall::Role role)
     { scroll(texts, role, -ConfigWall::scroll_step); }
 
-    // Drop all words of the given wall role back to the floor as dynamic objects.
-    // They are moved out of wall_texts and into floor_objects with Dynamic physics.
     inline void
     clear_ToFloor(TextWallList& texts,
                    DynArray<Text3dPtr>& words,
-                   Wall::Role role,
-                   double floor_y)
+                   Wall::Role role)
     {
         for (TextWallPtr& wt : texts) {
             if (wt->role != role || !wt->text3d) continue;
 
             Text3dPtr word = std::move(wt->text3d);
             word->body.mode    = PhysicsMode::Dynamic;
-            word->body.vel     = { 0.0, 1.5, 0.0 };   // small upward nudge
-            word->body.hover_height = floor_y + 1.5;
+            word->body.vel     = { 0.0, 1.5, 0.0 };
+            word->body.hover_height = World::get_FloorY<double>() + 1.5;
             word->set_Look(ConfigColor::floor, Text3d::depth_floor);
 
             words.push_back(std::move(word));
         }
 
-        // Remove cleared entries from wall_texts
         texts.erase(
             std::remove_if(texts.begin(), texts.end(),
                 [role](const TextWallPtr& wt){