v1.0.0 — Stable Release · Apache 2.0 · Open Source

A scripting language
built for the web.

Routing built into the language. Persistent DB connections. WebSocket, SSE, parallel(), jobs::, template:: — all in a single binary. No framework to set up — just write.

⇓ Download — Free Code examples →
index.lk — real-time dashboard
app::set("db", db::connect("mysql://root:@127.0.0.1/shop"))
# REST — 3 queries in parallel, capture-free DB access (app::db)
route("GET", "/api/dashboard", function() {
    $ch = channel(3)
    parallel(function() use ($ch) { send($ch, db::col(app::db(), "SELECT count(*) FROM orders", [])) })
    parallel(function() use ($ch) { send($ch, db::col(app::db(), "SELECT sum(total) FROM orders", [])) })
    parallel(function() use ($ch) { send($ch, db::col(app::db(), "SELECT count(*) FROM users", [])) })
    response::json(["orders" => receive($ch), "revenue" => receive($ch), "users" => receive($ch)])
})
# SSE — live notification stream
route("SSE", "/events", function($sse) {
    $id = timer::every(3000, function() use ($sse) {
        sse::send($sse, json::encode(["t" => date::now()]), "tick")
    })
    sse::on($sse, "close", function() use ($id) { timer::cancel($id) })
})
Warm Start Runtime
WebSocket + SSE
parallel() + channel()
SQLite · MySQL · PostgreSQL
jobs:: + queue:: + cache::
Bytecode VM · 7.8× faster
Apache / nginx / standalone
Everything a web app needs

LOOK is a scripting language designed for server-side web development. Routing, DB, concurrency, real-time connections, background jobs and a template engine — all inside a single binary, with zero dependencies.

Warm Start Runtime

The app starts once. DB connection, route registrations, setup code — it all stays in memory. Every HTTP request only runs dispatch_routes().

32 workers, 4.7 million requests, 10 minutes — 0 errors, 8 MB RAM.

🔀

Routing Built Into the Language

route("GET", "/product/{id}", fn) — this isn't a library call, it's the core of the language. No router to set up, no config, no middleware stack.

REST, WebSocket, SSE — all with the same route() syntax.

🚀

parallel() + channel()

Lightweight concurrency, in a web scripting language. Run 3 DB queries at once and collect the results with receive() — total time is max(t₁, t₂, t₃).

No extra runtime, no extra library, nothing new to learn for concurrency.

📦

Real-time: WS + SSE

route("WS", "/chat", fn) — WebSocket, fully RFC 6455 compliant. route("SSE", "/events", fn) — Server-Sent Events, together with timer::every().

500 concurrent WebSocket connections, 72 hours, 0 memory leaks.

When to choose LOOK

Not a framework — a language. If any of the needs below is yours, LOOK is a great fit.

🐘

Coming from PHP, wanting more

You love the ease of shared hosting / Plesk but want WebSocket, SSE and concurrency. Drop the file, it runs — plus real-time connections and parallel().

🚀

REST + DB without a framework

Routing, DB (MySQL/SQLite/PostgreSQL), validation, cache, JWT — all built into the language. No composer/npm dependency tree, one binary.

📦

Single binary, zero-dependency deploy

Copy one file to a VPS, run it with systemd/Plesk. No separate runtime, no package manager, no surprises. 28 MB RAM, steady across 1M requests.

✉️

You want to run a mail system

Built-in SMTP + IMAP server — no Postfix/Dovecot needed. Build a secure mail system, e-commerce notifications, a blog with LOOK. How? ↓

Performance
Real numbers, real server

look.codlook.com · AlmaLinux 8 · MariaDB · measured on a direct port, 64 workers.

0
RPS
HTTP + VM, router
c=200, 0 errors
0
requests
Endurance run
c=10,000, 0 errors
0
concurrent
Concurrency ceiling
single-machine ab limit, 0 errors
7.8
× faster
Bytecode VM speedup
vs. interpreter
28
MB
Memory — steady
not a single byte grew over 1.75M requests
0
errors
All load tests
c=1k / 5k / 10k / 20k + 1M

All tests were measured with ab on the live look.codlook.com environment. Throughput is steady from c=1,000 to c=20,000 (~7.5–10k) — the bottleneck is CPU; kernel walls (backlog, syn_backlog) were cleared with tuning. The TLS chain (nginx→FCGI) does ~1,100–1,240 RPS; the difference comes from double TLS termination, not LOOK. DB-bound endpoints are limited by MySQL RTT.

Proven resilience

Zero dependencies, every line tested. Passed fuzzing, ThreadSanitizer and data-integrity tests.

16,000+
Fuzzing iterations
Random/malformed HTTP bombardment under AddressSanitizer + UndefinedBehaviorSanitizer — 0 crashes, 0 UB, 0 memory errors.
0 races
ThreadSanitizer clean
Concurrent-dispatch data-race scan. The one race found (date::now tz) was fixed — now zero.
20,000
Cross-contamination test
Each request a unique token + DB round-trip. 0 data leaks in blocking and fiber mode — every request saw only its own data.
Zero
Supply-chain risk
MySQL, PostgreSQL, SQLite, Redis (RESP2), TLS client — all written from scratch in C++23. No third-party packages, no dependency hell.
4 rounds
Manual security audit
Beyond fuzzing: protocol parsers, VM arithmetic and network paths reviewed line by line. Every hardening point found (unguarded number conversion, unbounded allocation, signed-overflow UB) was closed and tested.

Vulnerabilities closed & protections

Topic LOOK protection
SQL injection db::query($c, sql, [params]) — auto-parameterised ?, driver-correct escaping
DoS — huge body LOOK_MAX_BODY_SIZE (10 MB) exceeded → 413; unbounded-body memory exhaustion prevented
Request smuggling Content-Length + Transfer-Encoding together → 400 (RFC 7230 §3.3.3)
Malformed header Malformed Content-Length → 400 (worker doesn't crash); full RFC 7230 chunked TE support
Slowloris / slow requests Read/write timeout (30s) + kernel backlog 16384 + syn_backlog tuning
Session security 128-bit /dev/urandom token; cookie HttpOnly + Secure + SameSite
Password storage auth::hash / verify — PBKDF2-SHA256, in the core
Upload security Magic-byte type validation (blocks extension spoofing) + SHA-256
JSON stack overflow Deeply nested JSON body → recursion depth limit (JSON_MAX_DEPTH) — stack overflow prevented
Protocol parser DoS RESP2/HTTP/PG/SMTP: unguarded number conversion + unbounded allocation closed (a malformed server response can't crash a worker)
Arithmetic UB Signed-integer overflow, shift and unary-minus undefined behaviour eliminated (promote to float on overflow) — UBSan clean

Before every build, 9 regression tests + automated ASan/UBSan fuzzing and ThreadSanitizer run in CI (.github/workflows/security.yml). Memory: RSS steady over 1.75M requests — no leak.

Embedded Mail Server — SMTP + IMAP

Most scripting languages send mail; LOOK serves it. A built-in SMTP (RFC 5321) and IMAP4rev1 (RFC 3501 + IDLE) server — no Postfix/Dovecot needed. You can build a fully secure mail system end to end with LOOK.

# SMTP receives → Maildir → IMAP serves (one command)
SMTP :25/:587 ─deliver─▶ <LOOK_MAIL_DIR>/<user>/inbox ◀─reads─ IMAP :143/:993

lk --mode http --port 7400 # HTTP + SMTP + IMAP + IMAPS together
STARTTLS + IMAPS (993) SEARCH · FETCH · APPEND IDLE — live push mail_users PBKDF2 auth ASan+UBSan fuzz: 0 UB

Thunderbird, Roundcube, Apple Mail and mobile clients connect directly — list, read, write, search, receive live notifications. All over TLS.

One binary, a full runtime

Write directly — nothing to install, nothing new to learn.

Core
🔀

Routing built into the language

route("GET", "/product/{id}", fn) — REST, WS, SSE, same syntax. URL params parsed automatically.

Syntax

Short lambda — fn + arrow

fn($x) => $x * 2 — a function alias + expression-bodied arrow with implicit return. route("GET","/ping", fn() => response::json(["ok"=>true])). Classic function works too.

Syntax
🧩

app:: service registry

No more passing a shared connection to every route with use ($conn). app::set("db", $conn) once at setup → routes use app::db() with no capture. The explicit-capture philosophy is preserved.

Core
🗄️

MySQL · SQLite · PostgreSQL

Zero dependency, wire protocol implemented directly. Same API: db::query / db::exec / db::col. Transactions: db::begin / commit / rollback.

Concurrency
⚙️

parallel() + channel()

The LOOK task model. Fan-out, pipeline, producer-consumer — over channels. No blocking, no callbacks. Monitor with parallel::active() / limit() / at_capacity().

Real-time
🔌

WebSocket

route("WS", "/chat", fn) — RFC 6455, SHA-1+Base64 handshake. ws::broadcast() to all connected clients.

Real-time
📡

SSE + timer::

route("SSE", "/events", fn) — Server-Sent Events. Periodic push with timer::every(3000, fn).

Background
📬

jobs:: — job queue

SQLite-backed durable queue. jobs::push / worker / run. Delayed jobs, retry, dead-letter. Not lost when the process exits.

Background
📥

queue:: — FIFO queue

In-memory, thread-safe, named queue. Passing data between requests. queue::push / pop / peek / size.

Cache
🧠

cache:: — in-memory cache

TTL-aware, thread-safe singleton. Shared across all workers with warm start. cache::get / set / has / flush.

Template
🎨

template:: — template engine

Layout inheritance, partial includes, {#each} loops, {#if} conditions. Svelte-like — no runtime, server render only.

VM
🚀

Bytecode VM

AST → register-based bytecode → switch dispatch. 7.8× faster than the interpreter on compute-heavy workloads.

Module
📧

mail:: — sending email

Mailgun, SendGrid, Postmark supported. mail::send / send_html. Choose the provider with the MAIL_PROVIDER env var.

Security
🔐

crypto:: + JWT

SHA-256, HMAC-SHA256, RS256, UUID v4, timing-safe compare — zero dependency. Sign and verify HS256/RS256 tokens with the official pkg/jwt package.

Security
🛡️

Rate Limiter — Token Bucket

Two-layer protection: per-IP burst tolerance + global botnet cap. LOOK_RATE_LIMIT_RPM, LOOK_RATE_LIMIT_BURST, LOOK_RATE_LIMIT_GLOBAL_RPM. Your LOOK code doesn't change — it's at the runtime level.

Security
✉️

DKIM + SMTP Protection

Outbound e-mail is automatically signed with RSA-SHA256 (LOOK_SMTP_DKIM_*). Port 25 relay is blocked — authenticated SMTP only (587/465).

Mail Server
📥

Embedded SMTP + IMAP

Zero-dependency MTA (25/587) + IMAP4rev1 server (143/993). SMTP delivers to Maildir, IMAP serves it to Thunderbird/Roundcube. STARTTLS, SEARCH, live IDLE push. Same mail_users pbkdf2 identity. No Dovecot/Postfix required.

Error Handling
⚠️

error:: — Type-Safe Errors

Structured error objects with error::new($code, $message). error::is($e), error::code($e), error::message($e). No use required — always on.

Module
🌐

http:: — Outbound HTTP Client

GET, POST, PUT, PATCH, DELETE. JSON body, custom headers, timeout. TLS included. Fan-out parallel API calls with parallel().

Ecosystem
📦

Package + Module System

lk install github.com/codlook/look-packages/... — JWT, payments and more. Explore the Codlook-approved ecosystem at packages.codlook.com. No registry, no config.

Developer
🧪

Test + REPL + look install

look test — 10 assertions, before_each/after_each. look repl — interactive. Install packages from GitHub with look install github.com/user/repo.

Meet the real code
index.lk — REST API, auth, validatorBasics
$conn = db::connect("mysql://root:@127.0.0.1/blog")
use auth
use validator
use cache
function admin_check() {
    session::start()
    if (session::get("admin_id") == null) {
        response::error(401, "Unauthorized")
        return false
    }
    return true
}

route("GET", "/api/posts", function() use ($conn) {
    # 5-minute cache — don't hit the DB for the same data over and over
    $data = cache::get("posts")
    if ($data == null) {
        $data = db::query($conn, "SELECT * FROM posts ORDER BY id DESC", [])
        cache::set("posts", $data, 300)
    }
    response::json(["ok" => true, "data" => $data])
})
route("POST", "/api/posts", function() use ($conn) {
    if (!admin_check()) { return }
    $v = validator::check(request::json(), [
        "title"  => ["required", "min:3", "max:200"],
        "content"  => ["required", "min:10"]
    ])
    if (!$v["ok"]) { response::json($v, 422) return }
    $data = request::json()
    db::exec($conn, "INSERT INTO posts (title,content) VALUES (?,?)", [$data["title"], $data["content"]])
    cache::delete("posts")  # cache invalidate
    response::json(["ok" => true, "id" => db::last_id($conn)])
})
chat.lk — Real-time chat, broadcast hubWebSocket
# Hub: the shared broadcast channel for all connections (setup phase)
$hub = channel()
route("WS", "/chat", function($ws) use ($hub) {

    # One thread per connection: listen on the hub → write to this client
    parallel(function() use ($ws, $hub) {
        while (true) {
            $msg = receive($hub)
            if ($msg == null) { break }
            ws::send($ws, $msg)
        }
    })
    # On incoming message, push to the hub → every client hears it
    ws::on($ws, "message", function($data) use ($hub) {
        $p = json::decode($data)
        send($hub, json::encode(["from" => request::ip(), "msg" => $p["msg"]]))
    })
    ws::on($ws, "close", function() {
        log::info("Connection closed — active: " . ws::clients())
    })
})
# Keepalive ping — to all clients every 30 seconds
timer::every(30000, function() {
    ws::broadcast(json::encode(["type" => "ping"]))
})
parallel.lk — 3 queries at once, total time = max(t1,t2,t3)parallel()
$conn = db::connect("mysql://root:@127.0.0.1/shop")
route("GET", "/panel/summary", function() use ($conn) {

    $ch = channel(3)  # waiting for 3 results

    parallel(function() use ($ch, $conn) {
        send($ch, db::col($conn, "SELECT count(*) FROM orders", []))
    })
    parallel(function() use ($ch, $conn) {
        send($ch, db::col($conn, "SELECT sum(total) FROM orders WHERE status='paid'", []))
    })
    parallel(function() use ($ch, $conn) {
        send($ch, db::col($conn, "SELECT count(*) FROM users WHERE active=1", []))
    })
    # Won't continue until all parallel jobs finish
    $orders  = receive($ch)
    $revenue       = receive($ch)
    $users = receive($ch)
    response::json([
        "orders"   => $orders,
        "revenue"        => $revenue,
        "users" => $users
    ])
})
sse.lk — Live notification stream, periodic pushSSE + timer::
$conn = db::connect("mysql://root:@127.0.0.1/shop")
# SSE: once the browser connects, a continuous data stream is pushed
route("SSE", "/live/orders", function($sse) use ($conn) {

    # Check for new orders every 2 seconds
    $tick = timer::every(2000, function() use ($sse, $conn) {
        $new = db::query($conn,
            "SELECT * FROM orders WHERE created_at > date_sub(now(), interval 5 second)", [])
        if (count($new) > 0) {
            $ok = sse::send($sse, json::encode($new), "new_order")
            if (!$ok) { timer::cancel($tick) }   # connection closed
        }
    })
    # 30s keepalive — so the browser doesn't time out
    $ping = timer::every(30000, function() use ($sse) {
        sse::send($sse, "keepalive", "ping")
    })
    sse::on($sse, "close", function() use ($tick, $ping) {
        timer::cancel($tick)
        timer::cancel($ping)
        log::info("SSE closed — active: " . sse::clients())
    })
})
jobs.lk — SQLite durable queue, retry, mail deliveryjobs::
use jobs
use mail
# Route: respond instantly (1ms), enqueue the work
route("POST", "/signup", function() use ($conn) {
    $data = request::json()
    db::exec($conn, "INSERT INTO users (email,name) VALUES (?,?)",
             [$data["email"], $data["name"]])
    # max 3 attempts, process now (delay_sec=0)
    jobs::push("welcome_email", ["to" => $data["email"], "name" => $data["name"]], 3, 0)
    # reminder email after 10 minutes
    jobs::push("reminder_email", ["to" => $data["email"]], 3, 600)
    response::json(["ok" => true])
})
# worker.lk — separate process (runs as a systemd service)
jobs::recover("welcome_email")  # crash recovery

jobs::worker("welcome_email", function($job) {
    $r = mail::send($job["payload"]["to"], "Welcome!",
                    "Hi " . $job["payload"]["name"] . ", your account is ready.")
    return $r["ok"]  # false → retry (max 3), then dead-letter
})
jobs::run(5000)  # poll every 5 seconds
blog.lk — Struct + template engine (server-side render)template::
use template
# Struct — Go style, no methods, data only
struct Post {
    id
    title
    content
    date
    likes: 0
    published: true
}

$tpl = env("VIEWS_DIR", "/app/views")
route("GET", "/blog", function() use ($conn, $tpl) {
    $rows = db::query($conn, "SELECT * FROM posts WHERE published=1", [])
    print(template::render($tpl . "/blog/index", [
        "title"  => "Blog",
        "posts" => $rows
    ]))
})

# views/blog/index.html — server-side, no JS
# {#extends "views/layout/base"}
# {#block "content"}
# {#each $posts as $p}
#   <article>
#     <h2>{$p.title}</h2>
#     <p>{$p.date} · {$p.likes} likes</p>
#     <div>{!$p.content}</div>
#   </article>
# {/each}
# {#empty}<p>No posts yet.</p>{/each}
# {/block}
api.lk — protect the API with JWT, install packages via look installpkg/jwt
# Terminal: look install github.com/Codlook/look-packages
use "pkg/jwt/jwt.lk"
use auth
use crypto
$conn = db::connect(env("DB_DSN", "mysql://root:@127.0.0.1/app"))
# Login — issue a token
route("POST", "/api/login", function() use ($conn) {
    $email = request::post("email") ?? ""
    $password = request::post("password") ?? ""
    $rows = db::query($conn, "SELECT * FROM users WHERE email=?", [$email])
    if (count($rows) == 0 || !auth::verify($password, $rows[0]["password_hash"])) {
        response::error(401, "Invalid credentials")
        return
    }

    $token = jwt_sign(
        ["user_id" => $rows[0]["id"], "role" => $rows[0]["role"]],
        env("JWT_SECRET", ""),
        ["exp" => 86400]   # 24 hours
    )
    response::json(["ok" => true, "token" => $token])
})
# Protected endpoint — Bearer token required
function auth_required() {
    $header = request::header("Authorization") ?? ""
    $token  = string::substr($header, 7)  # strip "Bearer "
    $p = jwt_verify($token, env("JWT_SECRET", ""))
    if ($p == null) {
        response::json(["error" => "Invalid token"], 401)
        return null
    }
    return $p
}

route("GET", "/api/profile", function() use ($conn) {
    $user = auth_required()
    if ($user == null) { return }

    $rows = db::query($conn, "SELECT id,name,email FROM users WHERE id=?", [$user["user_id"]])
    response::json(["ok" => true, "data" => $rows[0]])
})
# Webhook verification — HMAC-SHA256 timing-safe
route("POST", "/webhook/payment", function() {
    $payload  = request::body()
    $given  = request::header("X-Signature") ?? ""
    $expected = "sha256=" . crypto::hmac_sha256($payload, env("WEBHOOK_SECRET", ""))
    if (!crypto::constant_compare($given, $expected)) {
        response::status(401) return
    }
    # Signature valid — process the payment...
    response::json(["ok" => true])
})
Try it in the browser COMING SOON

Write and run LOOK code in the browser — no install. It's in the works — once ready, a live editor will sit right here.

playground.lk — runnable soon
route("GET", "/", fn() => response::json(["ok" => true, "message" => "Hello LOOK!"]))

# ▶ The Run button will appear here once it's ready

For now: download and run it in seconds with lk --mode http.

A single LOOK file

Login and data fetching — one file, no framework, no surprises.

LOOK index.lk — login + data fetching
use auth;

$conn = db::connect("mysql://root:@localhost/blog")
# Opened once — stays alive across every request

route("POST", "/login", function() use ($conn) {
    session::start()
    $d = request::json()

    $rows = db::query($conn,
        "SELECT * FROM users WHERE email=?", [$d["email"]])
    # ? parameterised — no SQL injection, no XSS sanitising

    if (count($rows) == 0 ||
        !auth::verify($d["password"], $rows[0]["password_hash"])) {
        response::json(["ok" => false], 401)
        return
    }
    session::set("user_id", $rows[0]["id"])
    response::redirect("/panel")
})

route("GET", "/posts", function() use ($conn) {
    $rows = db::query($conn, "SELECT * FROM posts", [])
    response::json($rows)
})
request:: — zero globals Prepared stmt automatic DB connection persistent auth:: PBKDF2 built in
Topic How LOOK does it
DB connection Warm start — opened once, stays alive across every request
Request data No globals — request:: / session:: namespaces
Routing Built into the language — route("GET", "/path/{id}", fn)
SQL injection protection db::query auto-parameterised (? placeholder)
Concurrency parallel() + channel() inside the language
WebSocket / SSE route("WS", ...) — part of the core
Performance (router) Bytecode VM — 10,000+ RPS on a direct port, single core
Three rules, never broken

Every new feature must serve these three principles — otherwise it doesn't make it into the language.

🗺️
Drop it, run it
Drop the file on the server, let the server route it — done. Nothing to install, nothing to compile. Shared hosting, VPS, Plesk — the same workflow.
# GitHub Releases → download the binary cp look-fcgi /opt/look/ && chmod +x /opt/look/look-fcgi # → Apache .htaccess, systemd service
Explicit scope
Clean scope, explicit dependencies, concurrency built into the language. use() declares exactly what comes in. No hidden globals, no surprises.
route("GET", "/product/{id}", function($id) use ($conn) { response::json($row) })
🔒
No framework to set up
Routing, DB, auth, cache, jobs, WebSocket, SSE, templates — all inside the language. No package manager to open, no dependency hell.
use jobs # SQLite job queue use cache # in-memory TTL cache use mail # Mailgun/SendGrid # No install. It's all inside the binary.
How deep does it go?

Every component of LOOK is written in C++23 with zero dependencies.

Parser

Lexer + Parser + AST

Precedence-aware parser, full operator set, string interpolation, error positions via SourceLocation.

VM

Bytecode VM

AST → 3-address register bytecode → switch dispatch. 7.8× faster on compute workloads. Fallback: tree-walk interpreter.

Concurrency

ThreadPool + ConnPool

--workers N, per-request interpreter copy, per-DSN connection pool. Hot reload: zero downtime.

Network

HTTP/1.1 + WebSocket + SSE

epoll (Linux), IOCP (Windows). RFC 6455 WS, SSE frame codec. Unlimited concurrent connections.

DB

MySQL · SQLite · PostgreSQL

Wire protocol from scratch. No driver, no ORM. Same db:: API, different DSN.

Deployment

CGI + FastCGI + HTTP

Apache/nginx FastCGI, standalone HTTP, CGI fallback. One binary, three modes.

Running in 5 minutes

Choose your platform:

  1. Download and extract the package

    One package for Ubuntu/Debian + AlmaLinux/Rocky/RHEL — the same portable binary. From GitHub Releases:

    unzip look-lang-linux-1.0.0.zip -d look && cd look
  2. Install (one command)

    Installs the binary to /usr/local/bin, creates a sample app + systemd service (port 9000), starts it and verifies.

    sudo bash install.sh
  3. Verify

    curl http://127.0.0.1:9000/ · systemctl status look

    Prefer to manage it with dnf on AlmaLinux/RHEL: dnf install look-lang-1.0.0-1.el8.x86_64.rpm (then dnf update look-lang).

  1. Install XAMPP

    If XAMPP is already installed, skip this step.

  2. Extract and install the package (Administrator PowerShell)

    Extract look-lang-xampp-1.0.0.zip; it copies lk-cgi.exe to cgi-bin, patches httpd.conf for .lk, and creates a sample test.lk.

    Expand-Archive look-lang-xampp-1.0.0.zip look; cd look; .\install.ps1

    Custom path: .\install.ps1 -XamppDir "D:\xampp" · Uninstall: .\uninstall.ps1

  3. Restart XAMPP Apache

    XAMPP Control Panel → Apache → Restart. Open http://localhost/test.lk — it's running.

  1. Open the Plesk Terminal

    Plesk Panel → Server Management → Terminal

    If you prefer SSH: ssh root@server_ip

  2. Download and install the ZIP

    plesk bin extension --uninstall look-lang 2>/dev/null; wget -O /tmp/look-lang-plesk-1.0.0.zip "https://github.com/codlook/look/releases/download/v1.0/look-lang-plesk-1.0.0.zip" && plesk bin extension --install /tmp/look-lang-plesk-1.0.0.zip

    You'll see "The extension was successfully installed."

  3. Open the LOOK Language panel

    Plesk Panel → ExtensionsMy ExtensionsLOOK Language → Open

  4. Add a domain

    Applications → Add New Domain → pick a domain (script path and port fill in automatically) → Add & Start

    After a few seconds it shows Running in the list. The systemd service + Apache proxy are created automatically.

  5. Write your code

    Use Edit Code to edit index.lk in the browser → Save & Redeploy — the file is written and the service restarts.

Full workspace: live system dashboard, in-browser code editor, per-domain monitor (CPU/RAM/PID/connections), journalctl log viewer.
On a fresh server, one root command is needed to grant sudo access (see docs).

  1. Build the image

    Docker is used to build the LOOK binaries. Separate Dockerfiles are provided for Ubuntu 24.04 and AlmaLinux 8.

    docker build -f Dockerfile.linux-build -t look-builder .
  2. Compile the binary

    docker run --rm -v "${PWD}:/src" look-builder sh -c "cd /src/build-linux && cmake .. -DCMAKE_BUILD_TYPE=Release && make -j4"
  3. Output binary

    cpp/build-linux/look-fcgi # ready binary

    For AlmaLinux 8 (Plesk) use Dockerfile.almalinux8-build — because of the glibc difference, the Ubuntu binary won't run on AlmaLinux.

Download
Built for web developers

v1.0.0 · Apache 2.0 · Stable release for every platform.

🐧 Linux — Ubuntu + AlmaLinux

One self-contained package. sudo bash install.sh → binary + systemd service. The same portable binary runs on Ubuntu/Debian and AlmaLinux/Rocky/RHEL.

⇓ look-lang-linux-1.0.0.zip → Install Guide

📦 AlmaLinux / RHEL — RPM

dnf-managed install. dnf install look-lang-…rpm → then update with dnf update look-lang. The binary links to the system OpenSSL (OS security patches).

⇓ look-lang-1.0.0-1.el8.x86_64.rpm → Plesk Alternative

🪟 Windows — XAMPP

One command for XAMPP. .\install.ps1 → lk-cgi.exe + httpd.conf patch. Unlimited connections via IOCP.

⇓ look-lang-xampp-1.0.0.zip → XAMPP Install Guide

🖥️ Plesk Extension

A full hosting panel for Plesk 18+ — code editor, live monitor, log viewer. Add a domain, write your code, go live.

⇓ look-lang-plesk-1.0.0.zip → GitHub →

🧩 VS Code Extension

IntelliSense (all built-ins + your own code), live error checking (syntax + undefined functions), outline, 20+ snippets, Run/Serve/REPL shortcuts. v2.0

⇓ Install from Marketplace → ⇓ look-lang-2.0.0.vsix (GitHub) →

📦 Source Code

C++23, CMake 3.20+. Apache 2.0 license. For contributing or building it yourself.

GitHub → codlook/look All Releases