From d8530410ec04e67a75bb7c45f30b2021964d1c09 Mon Sep 17 00:00:00 2001
From: kawre <kenterq@gmail.com>
Date: Wed, 30 Jul 2025 02:13:01 +0200
Subject: [PATCH 1/4] refactor: move to `m.hooks`

---
 lua/leetcode.lua                              |   8 +-
 lua/leetcode/api/statistics.lua               |   4 +-
 lua/leetcode/command/init.lua                 |  11 +-
 lua/leetcode/ui/menu/button/back.lua          |  13 +
 lua/leetcode/ui/menu/button/init.lua          |  43 +++
 lua/leetcode/ui/menu/buttons.lua              |   9 +
 lua/leetcode/ui/menu/footer.lua               |  71 +++++
 lua/leetcode/ui/menu/header.lua               | 217 +++++++++++++++
 lua/leetcode/ui/menu/init.lua                 |  63 +++++
 lua/leetcode/ui/menu/nav/init.lua             |  19 ++
 lua/leetcode/ui/menu/nav/loading.lua          |   5 +
 lua/leetcode/ui/menu/nav/menu/cache/init.lua  |  18 ++
 lua/leetcode/ui/menu/nav/menu/cookie/init.lua |  24 ++
 lua/leetcode/ui/menu/nav/menu/init.lua        |  46 ++++
 .../ui/menu/nav/menu/problems/init.lua        |  34 +++
 .../ui/menu/nav/menu/statistics/init.lua      |  32 +++
 lua/leetcode/ui/menu/nav/signin.lua           |   5 +
 lua/leetcode/ui/menu/title.lua                |  25 ++
 .../ui/problem/description/content/init.lua   |  17 ++
 .../ui/problem/description/header.lua         |  77 ++++++
 lua/leetcode/ui/problem/description/init.lua  |  45 ++++
 lua/leetcode/ui/problem/info/init.lua         | 121 +++++++++
 lua/leetcode/ui/problem/init.lua              | 251 ++++++++++++++++++
 lua/leetcode/ui/problem/window.lua            |  97 +++++++
 24 files changed, 1243 insertions(+), 12 deletions(-)
 create mode 100644 lua/leetcode/ui/menu/button/back.lua
 create mode 100644 lua/leetcode/ui/menu/button/init.lua
 create mode 100644 lua/leetcode/ui/menu/buttons.lua
 create mode 100644 lua/leetcode/ui/menu/footer.lua
 create mode 100644 lua/leetcode/ui/menu/header.lua
 create mode 100644 lua/leetcode/ui/menu/init.lua
 create mode 100644 lua/leetcode/ui/menu/nav/init.lua
 create mode 100644 lua/leetcode/ui/menu/nav/loading.lua
 create mode 100644 lua/leetcode/ui/menu/nav/menu/cache/init.lua
 create mode 100644 lua/leetcode/ui/menu/nav/menu/cookie/init.lua
 create mode 100644 lua/leetcode/ui/menu/nav/menu/init.lua
 create mode 100644 lua/leetcode/ui/menu/nav/menu/problems/init.lua
 create mode 100644 lua/leetcode/ui/menu/nav/menu/statistics/init.lua
 create mode 100644 lua/leetcode/ui/menu/nav/signin.lua
 create mode 100644 lua/leetcode/ui/menu/title.lua
 create mode 100644 lua/leetcode/ui/problem/description/content/init.lua
 create mode 100644 lua/leetcode/ui/problem/description/header.lua
 create mode 100644 lua/leetcode/ui/problem/description/init.lua
 create mode 100644 lua/leetcode/ui/problem/info/init.lua
 create mode 100644 lua/leetcode/ui/problem/init.lua
 create mode 100644 lua/leetcode/ui/problem/window.lua

diff --git a/lua/leetcode.lua b/lua/leetcode.lua
index 4d55f27e..0afb7224 100644
--- a/lua/leetcode.lua
+++ b/lua/leetcode.lua
@@ -3,6 +3,8 @@ local config = require("leetcode.config")
 ---@class lc.LeetCode
 local leetcode = {}
 
+_G.Leet = { ctx = {} }
+
 ---@private
 local function log_failed_to_init()
     local log = require("leetcode.logger")
@@ -104,8 +106,10 @@ function leetcode.start(on_vimenter)
         vim.cmd.enew()
     end
 
-    local Menu = require("leetcode-ui.renderer.menu")
-    Menu():mount()
+    local menu = require("leetcode.ui.menu")
+    menu.win.buf = vim.api.nvim_get_current_buf()
+    menu.win:show()
+    menu:render()
 
     local utils = require("leetcode.utils")
     utils.exec_hooks("enter")
diff --git a/lua/leetcode/api/statistics.lua b/lua/leetcode/api/statistics.lua
index 6ce24db8..130399ab 100644
--- a/lua/leetcode/api/statistics.lua
+++ b/lua/leetcode/api/statistics.lua
@@ -27,9 +27,7 @@ function statistics.calendar(cb)
             calendar.submission_calendar =
                 select(2, pcall(utils.decode, calendar.submission_calendar))
 
-            cb({
-                calendar = calendar,
-            }, nil)
+            cb({ calendar = calendar }, nil)
         end,
     })
 end
diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua
index 11824456..b683a3d9 100644
--- a/lua/leetcode/command/init.lua
+++ b/lua/leetcode/command/init.lua
@@ -176,15 +176,12 @@ function cmd.start_with_cmd()
 end
 
 function cmd.menu()
-    local winid, bufnr = _Lc_state.menu.winid, _Lc_state.menu.bufnr
-    local ok, tabp = pcall(api.nvim_win_get_tabpage, winid)
-    local ui = require("leetcode-ui.utils")
-
-    if ok then
+    if Leet.menu.win:valid() then
+        local _, tabp = pcall(api.nvim_win_get_tabpage, Leet.menu.win.win)
         api.nvim_set_current_tabpage(tabp)
-        ui.win_set_buf(winid, bufnr)
+        Leet.menu.win:focus()
     else
-        _Lc_state.menu:remount()
+        Leet.menu.win:show()
     end
 end
 
diff --git a/lua/leetcode/ui/menu/button/back.lua b/lua/leetcode/ui/menu/button/back.lua
new file mode 100644
index 00000000..0ed261ec
--- /dev/null
+++ b/lua/leetcode/ui/menu/button/back.lua
@@ -0,0 +1,13 @@
+local Button = require("leetcode.ui.menu.button")
+local markup = require("markup")
+
+return markup.Component(function(props)
+    local page = props.page
+
+    return Button({
+        icon = "",
+        title = "Back",
+        key = "q",
+        page = page,
+    })
+end)
diff --git a/lua/leetcode/ui/menu/button/init.lua b/lua/leetcode/ui/menu/button/init.lua
new file mode 100644
index 00000000..441c906c
--- /dev/null
+++ b/lua/leetcode/ui/menu/button/init.lua
@@ -0,0 +1,43 @@
+local m = require("markup")
+
+---@class MenuButtonProps
+---@field title string
+---@field icon string
+---@field key string
+---@field page string
+---@field nested boolean
+---@field on_submit function
+
+---@param p MenuButtonProps
+return m.component(function(p)
+    local menu = m.hooks.use(Leet.ctx.menu)
+    local win = m.hooks.window()
+
+    local function change_page()
+        if not p.on_submit and p.page then
+            menu.set_page(p.page)
+        else
+            p.on_submit()
+        end
+    end
+
+    m.hooks.effect(function()
+        return win:map("n", p.key, change_page, {
+            noremap = false,
+            silent = true,
+            nowait = true,
+        })
+    end, {})
+
+    return m.flex({
+        -- on_confirm = on_submit,
+        justify = "between",
+        m.flex({
+            spacing = 1,
+            m.button(p.icon, "Character"),
+            m.inline(p.title, "Tag"),
+            p.nested and m.inline("", "leetcode_menu_button_nested"),
+        }),
+        m.block(p.key, "Type"),
+    })
+end)
diff --git a/lua/leetcode/ui/menu/buttons.lua b/lua/leetcode/ui/menu/buttons.lua
new file mode 100644
index 00000000..edc18c42
--- /dev/null
+++ b/lua/leetcode/ui/menu/buttons.lua
@@ -0,0 +1,9 @@
+local markup = require("markup")
+
+return markup.component(function(props)
+    return markup.vflex({
+        spacing = 1,
+        width = 50,
+        children = props.children,
+    })
+end)
diff --git a/lua/leetcode/ui/menu/footer.lua b/lua/leetcode/ui/menu/footer.lua
new file mode 100644
index 00000000..41d924f7
--- /dev/null
+++ b/lua/leetcode/ui/menu/footer.lua
@@ -0,0 +1,71 @@
+local m = require("markup")
+local config = require("leetcode.config")
+local api = require("leetcode.api.statistics")
+
+return m.component(function()
+    local streak, set_streak = m.hooks.variable({})
+    local win = m.hooks.window()
+    local progress, set_progress = m.hooks.variable({})
+
+    m.hooks.effect(function()
+        api.session_progress(set_progress)
+    end, {})
+
+    m.hooks.effect(function()
+        api.streak(function(data)
+            set_streak(data)
+        end)
+
+        win:map("n", "9", function()
+            set_streak(function(v)
+                return {
+                    streakCount = (v.streakCount or 0) + 1,
+                    todayCompleted = v.todayCompleted,
+                }
+            end)
+        end)
+    end, {})
+
+    local function stats()
+        local res = {}
+
+        local hl = {
+            ["Easy"] = "DiagnosticVirtualTextHint",
+            ["Medium"] = "DiagnosticVirtualTextInfo",
+            ["Hard"] = "DiagnosticVirtualTextError",
+        }
+
+        for _, p in ipairs(progress or {}) do
+            if p.difficulty ~= "All" then
+                table.insert(
+                    res,
+                    m.block({
+                        m.inline("  ", hl[p.difficulty]),
+                        m.inline(" " .. p.count),
+                    })
+                )
+            end
+        end
+
+        return m.flex({ children = res, spacing = 1 })
+    end
+
+    return m.vflex({
+        stats(),
+        spacing = 1,
+        align = "center",
+        m.flex({
+            spacing = 1,
+            m.block({
+                m.inline("  ", "DiagnosticVirtualTextError"),
+                " ",
+                m.inline(streak.streakCount or "󰨕"),
+            }),
+            m.block({
+                m.inline("  ", "DiagnosticVirtualTextInfo"),
+                " ",
+                m.inline(config.auth.name or "no user", "Statement"),
+            }),
+        }),
+    })
+end)
diff --git a/lua/leetcode/ui/menu/header.lua b/lua/leetcode/ui/menu/header.lua
new file mode 100644
index 00000000..3b9a66b0
--- /dev/null
+++ b/lua/leetcode/ui/menu/header.lua
@@ -0,0 +1,217 @@
+local m = require("markup")
+local api = require("leetcode.api.statistics")
+
+local ascii = vim.tbl_map(m.block, {
+    [[ /$$                          /$$     /$$$$$$                /$$         ]],
+    [[| $$                         | $$    /$$__  $$              | $$         ]],
+    [[| $$       /$$$$$$  /$$$$$$ /$$$$$$ | $$  \__/ /$$$$$$  /$$$$$$$ /$$$$$$ ]],
+    [[| $$      /$$__  $$/$$__  $|_  $$_/ | $$      /$$__  $$/$$__  $$/$$__  $$]],
+    [[| $$     | $$$$$$$| $$$$$$$$ | $$   | $$     | $$  \ $| $$  | $| $$$$$$$$]],
+    [[| $$     | $$_____| $$_____/ | $$ /$| $$    $| $$  | $| $$  | $| $$_____/]],
+    [[| $$$$$$$|  $$$$$$|  $$$$$$$ |  $$$$|  $$$$$$|  $$$$$$|  $$$$$$|  $$$$$$$]],
+    [[|________/\_______/\_______/  \___/  \______/ \______/ \_______/\_______/]],
+})
+
+local function is_same_day(date1, date2)
+    return date1.year == date2.year and date1.month == date2.month and date1.day == date2.day
+end
+
+local month_names = {
+    "Jan",
+    "Feb",
+    "Mar",
+    "Apr",
+    "May",
+    "Jun",
+    "Jul",
+    "Aug",
+    "Sep",
+    "Oct",
+    "Nov",
+    "Dec",
+}
+
+local function get_days_in_month(month, year)
+    local days_in_month = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
+    if (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0) then
+        days_in_month[2] = 29
+    end
+    return days_in_month[month]
+end
+
+return m.component(function()
+    local calendar, set_calendar = m.hooks.variable({})
+
+    m.hooks.effect(function()
+        api.calendar(function(data)
+            local time = os.date("*t")
+            local curr = os.time(vim.tbl_extend("force", time, {
+                year = time.year - 1,
+                month = time.month,
+                day = time.day,
+                hour = 1,
+                isdst = false,
+            }))
+
+            local threshold = os.time({
+                year = time.year,
+                month = time.month,
+                day = time.day,
+                hour = 1,
+                isdst = false,
+            })
+            if threshold <= os.time(time) then
+                threshold = threshold + 24 * 60 * 60
+            end
+
+            local last_year_sub_count = 0
+            local month_lens = {}
+            local max_sub_count = 0
+            local submissions = {}
+
+            for ts, count in pairs(data.calendar.submission_calendar) do
+                local osdate = os.date("*t", tonumber(ts))
+                max_sub_count = math.max(max_sub_count, count)
+                table.insert(submissions, { osdate = osdate, count = count })
+                last_year_sub_count = last_year_sub_count + count
+            end
+
+            local months = {}
+            local ordered_keys = {} -- ✅ Declare here
+
+            local t = os.date("*t")
+            for i = 11, 0, -1 do
+                local past = os.date(
+                    "*t",
+                    os.time({
+                        year = t.year,
+                        month = t.month - i,
+                        day = 1,
+                    })
+                )
+                local key = string.format("%04d-%02d", past.year, past.month)
+                if not months[key] then
+                    months[key] = {}
+                end
+                table.insert(ordered_keys, key)
+            end
+
+            for _, sub in ipairs(submissions) do
+                local osdate = sub.osdate
+                local count = sub.count
+
+                local month_key = string.format("%04d-%02d", osdate.year, osdate.month)
+                months[month_key] = months[month_key] or {}
+
+                local first_day_of_month = os.time({
+                    year = osdate.year,
+                    month = osdate.month,
+                    day = 1,
+                    hour = 0,
+                })
+                local first_wday = os.date("*t", first_day_of_month).wday
+
+                local week_num = math.floor((osdate.day + first_wday - 2) / 7) + 1
+
+                months[month_key][week_num] = months[month_key][week_num] or {}
+                months[month_key][week_num][osdate.day] = count
+            end
+
+            set_calendar({
+                calendar = data.calendar,
+                curr_time = curr,
+                months = months,
+                threshold = threshold,
+                last_year_sub_count = last_year_sub_count,
+                month_lens = month_lens,
+                max_sub_count = max_sub_count,
+                submissions = submissions,
+                ordered_keys = ordered_keys, -- ✅ Pass it along
+            })
+        end)
+    end, {})
+
+    if vim.tbl_isempty(calendar or {}) then
+        return "loading..."
+    end
+
+    -- Example: render grouped structure
+    return m.vflex({
+        align = "center",
+        margin_top = 3,
+        height = 8,
+        m.inline("  leetcode.nvim ", "DiagnosticVirtualTextWarn"),
+        m.flex({
+            spacing = 1,
+            margin = 1,
+            align = "end",
+            vim.tbl_map(function(month_key)
+                local year, month_idx = month_key:match("^(%d+)-(%d+)$")
+                year = tonumber(year)
+                month_idx = tonumber(month_idx)
+
+                local month = calendar.months[month_key]
+
+                local days_in_month = get_days_in_month(month_idx, year)
+                local first_day = os.time({
+                    year = year,
+                    month = month_idx,
+                    day = 1,
+                    hour = 0,
+                })
+                local first_wday = os.date("*t", first_day).wday
+
+                -- Fill cells
+                local day_cells = {}
+
+                -- Leading blanks
+                for _ = 1, first_wday - 1 do
+                    table.insert(day_cells, { day = nil })
+                end
+
+                -- Days
+                for day = 1, days_in_month do
+                    local count = 0
+                    for _, week_days in pairs(month) do
+                        if week_days[day] then
+                            count = week_days[day]
+                            break
+                        end
+                    end
+                    table.insert(day_cells, { day = day, count = count })
+                end
+
+                -- Pad to full weeks
+                local rem = #day_cells % 7
+                if rem ~= 0 then
+                    for _ = 1, 7 - rem do
+                        table.insert(day_cells, { day = nil })
+                    end
+                end
+
+                -- Split into week blocks (each is a row of 7 days)
+                local week_blocks = {}
+                for i = 1, #day_cells, 7 do
+                    local week_days = {}
+                    for j = 0, 6 do
+                        local cell = day_cells[i + j]
+                        table.insert(
+                            week_days,
+                            m.block(
+                                cell.day and "󰨕" or " ",
+                                cell.count and cell.count > 0 and "DiagnosticOk" or nil
+                            )
+                        )
+                    end
+                    table.insert(week_blocks, m.block({ week_days }))
+                end
+
+                -- Month block: title + grid of weeks
+                return m.block({
+                    m.block(month_names[month_idx], "DiagnosticVirtualTextInfo"),
+                    m.flex(week_blocks, "SignColumn"),
+                })
+            end, calendar.ordered_keys),
+        }),
+    })
+end)
diff --git a/lua/leetcode/ui/menu/init.lua b/lua/leetcode/ui/menu/init.lua
new file mode 100644
index 00000000..5d086d3f
--- /dev/null
+++ b/lua/leetcode/ui/menu/init.lua
@@ -0,0 +1,63 @@
+local m = require("markup")
+local cookie = require("leetcode.cache.cookie")
+local Header = require("leetcode.ui.menu.header")
+local Footer = require("leetcode.ui.menu.footer")
+local Nav = require("leetcode.ui.menu.nav")
+local config = require("leetcode.config")
+
+local menu_ctx = m.hooks.context()
+Leet.ctx.menu = menu_ctx
+
+local renderer = Markup.renderer({
+    position = "current",
+    hijack = true,
+    show = false,
+})
+Leet.menu = renderer
+
+local App = m.component(function()
+    local menu = m.hooks.use(Leet.ctx.menu)
+
+    m.hooks.effect(function()
+        if cookie.get() then
+            menu.set_page("loading")
+
+            local auth_api = require("leetcode.api.auth")
+            auth_api.user(function(_, err)
+                if err then
+                    menu.set_page("signin")
+                    Markup.log.error(err)
+                else
+                    menu.set_page("menu")
+                end
+            end)
+        else
+            menu.set_page("signin")
+        end
+    end, {})
+
+    return m.vflex({
+        align = "center",
+        spacing = 3,
+        -- padding = { 1, 2 },
+        config.auth.is_signed_in and Header(),
+        Nav(),
+        Footer(),
+    })
+end)
+
+local Root = m.component(function()
+    local page, set_page = m.hooks.variable("loading")
+
+    return Leet.ctx.menu:provider({
+        value = {
+            page = page,
+            set_page = set_page,
+        },
+        App(),
+    })
+end)
+
+renderer:bind(Root)
+
+return renderer
diff --git a/lua/leetcode/ui/menu/nav/init.lua b/lua/leetcode/ui/menu/nav/init.lua
new file mode 100644
index 00000000..4774b403
--- /dev/null
+++ b/lua/leetcode/ui/menu/nav/init.lua
@@ -0,0 +1,19 @@
+local m = require("markup")
+local log = require("leetcode.logger")
+local Title = require("leetcode.ui.menu.title")
+
+local Nav = m.component(function()
+    local menu = m.hooks.use(Leet.ctx.menu)
+    local Items = require("leetcode.ui.menu.nav." .. menu.page)
+
+    return m.vflex({
+        spacing = 1,
+        align = "center",
+        children = {
+            Title(),
+            Items(),
+        },
+    })
+end)
+
+return Nav
diff --git a/lua/leetcode/ui/menu/nav/loading.lua b/lua/leetcode/ui/menu/nav/loading.lua
new file mode 100644
index 00000000..c287949a
--- /dev/null
+++ b/lua/leetcode/ui/menu/nav/loading.lua
@@ -0,0 +1,5 @@
+local m = require("markup")
+
+return m.component(function()
+    return "123"
+end)
diff --git a/lua/leetcode/ui/menu/nav/menu/cache/init.lua b/lua/leetcode/ui/menu/nav/menu/cache/init.lua
new file mode 100644
index 00000000..2969b081
--- /dev/null
+++ b/lua/leetcode/ui/menu/nav/menu/cache/init.lua
@@ -0,0 +1,18 @@
+local markup = require("markup")
+local Buttons = require("leetcode.ui.menu.buttons")
+local Button = require("leetcode.ui.menu.button")
+local BackButton = require("leetcode.ui.menu.button.back")
+
+return markup.Component(function()
+    local cmd = require("leetcode.command")
+
+    return Buttons({
+        Button({
+            icon = "󱘴",
+            title = "Update",
+            key = "u",
+            on_submit = cmd.cache_update,
+        }),
+        BackButton({ page = "menu" }),
+    })
+end)
diff --git a/lua/leetcode/ui/menu/nav/menu/cookie/init.lua b/lua/leetcode/ui/menu/nav/menu/cookie/init.lua
new file mode 100644
index 00000000..267dfb06
--- /dev/null
+++ b/lua/leetcode/ui/menu/nav/menu/cookie/init.lua
@@ -0,0 +1,24 @@
+local markup = require("markup")
+local Buttons = require("leetcode.ui.menu.buttons")
+local Button = require("leetcode.ui.menu.button")
+local BackButton = require("leetcode.ui.menu.button.back")
+
+return markup.Component(function()
+    local cmd = require("leetcode.command")
+
+    return Buttons({
+        Button({
+            icon = "󱛬",
+            key = "u",
+            title = "Update",
+            on_press = cmd.cookie_prompt,
+        }),
+        Button({
+            icon = "󱛪",
+            key = "d",
+            title = "Delete / Sign out",
+            on_submit = cmd.sign_out,
+        }),
+        BackButton({ page = "menu" }),
+    })
+end)
diff --git a/lua/leetcode/ui/menu/nav/menu/init.lua b/lua/leetcode/ui/menu/nav/menu/init.lua
new file mode 100644
index 00000000..b3aad9a9
--- /dev/null
+++ b/lua/leetcode/ui/menu/nav/menu/init.lua
@@ -0,0 +1,46 @@
+local m = require("markup")
+local log = require("leetcode.logger")
+
+local Buttons = require("leetcode.ui.menu.buttons")
+local Button = require("leetcode.ui.menu.button")
+
+return m.component(function()
+    return Buttons({
+        Button({
+            icon = "",
+            title = "Problems",
+            key = "p",
+            nested = true,
+            page = "menu.problems",
+        }),
+        Button({
+            icon = "",
+            title = "Statistics",
+            key = "s",
+            nested = true,
+            page = "menu.statistics",
+        }),
+        Button({
+            icon = "󰆘",
+            title = "Cookie",
+            key = "i",
+            nested = true,
+            page = "menu.cookie",
+        }),
+        Button({
+            icon = "",
+            title = "Cache",
+            key = "c",
+            nested = true,
+            page = "menu.cache",
+        }),
+        Button({
+            icon = "󰩈",
+            title = "Exit",
+            key = "qa",
+            on_submit = function()
+                require("leetcode").stop()
+            end,
+        }),
+    })
+end)
diff --git a/lua/leetcode/ui/menu/nav/menu/problems/init.lua b/lua/leetcode/ui/menu/nav/menu/problems/init.lua
new file mode 100644
index 00000000..28de2e0e
--- /dev/null
+++ b/lua/leetcode/ui/menu/nav/menu/problems/init.lua
@@ -0,0 +1,34 @@
+local m = require("markup")
+
+local Buttons = require("leetcode.ui.menu.buttons")
+local Button = require("leetcode.ui.menu.button")
+local BackButton = require("leetcode.ui.menu.button.back")
+
+return m.component(function()
+    local cmd = require("leetcode.command")
+
+    return Buttons({
+        Button({
+            icon = "",
+            title = "List",
+            key = "p",
+            nested = true,
+            on_submit = cmd.problems,
+        }),
+        Button({
+            icon = "",
+            title = "Random",
+            key = "s",
+            nested = true,
+            on_submit = cmd.random_question,
+        }),
+        Button({
+            icon = "󰃭",
+            title = "Daily",
+            key = "i",
+            nested = true,
+            on_submit = cmd.qot,
+        }),
+        BackButton({ page = "menu" }),
+    })
+end)
diff --git a/lua/leetcode/ui/menu/nav/menu/statistics/init.lua b/lua/leetcode/ui/menu/nav/menu/statistics/init.lua
new file mode 100644
index 00000000..4fac2881
--- /dev/null
+++ b/lua/leetcode/ui/menu/nav/menu/statistics/init.lua
@@ -0,0 +1,32 @@
+local markup = require("markup")
+local Buttons = require("leetcode.ui.menu.buttons")
+local Button = require("leetcode.ui.menu.button")
+local BackButton = require("leetcode.ui.menu.button.back")
+local config = require("leetcode.config")
+local log = require("leetcode.logger")
+
+local cmd = require("leetcode.command")
+
+return markup.Component(function()
+    return Buttons({
+        not config.is_cn and Button({
+            title = "Skills",
+            icon = "",
+            key = "s",
+            on_submit = cmd.ui_skills,
+        }),
+        Button({
+            icon = "",
+            title = "Languages",
+            key = "l",
+            on_submit = cmd.ui_languages,
+        }),
+        Button({
+            icon = "",
+            title = "Update",
+            key = "u",
+            on_submit = function() end,
+        }),
+        BackButton({ page = "menu" }),
+    })
+end)
diff --git a/lua/leetcode/ui/menu/nav/signin.lua b/lua/leetcode/ui/menu/nav/signin.lua
new file mode 100644
index 00000000..012a50a8
--- /dev/null
+++ b/lua/leetcode/ui/menu/nav/signin.lua
@@ -0,0 +1,5 @@
+local m = require("markup")
+
+return m.component(function()
+    return m.inline("please sign in")
+end)
diff --git a/lua/leetcode/ui/menu/title.lua b/lua/leetcode/ui/menu/title.lua
new file mode 100644
index 00000000..ac098b5d
--- /dev/null
+++ b/lua/leetcode/ui/menu/title.lua
@@ -0,0 +1,25 @@
+local m = require("markup")
+
+return m.component(function()
+    local menu = m.hooks.use(Leet.ctx.menu)
+    local page = menu.page
+
+    local titles = {}
+    local parts = vim.split(page, ".", { plain = true })
+
+    for i, title in ipairs(parts) do
+        title = title:gsub("^%l", string.upper)
+        local hl = i == #parts and "leetcode_menu_title" or "leetcode_menu_title_inactive"
+        table.insert(titles, m.Inline(title, hl))
+
+        if i ~= #parts then
+            table.insert(titles, m.Inline("  "))
+        end
+    end
+
+    return m.flex({
+        -- width = 50,
+        justify = "center",
+        children = titles,
+    })
+end)
diff --git a/lua/leetcode/ui/problem/description/content/init.lua b/lua/leetcode/ui/problem/description/content/init.lua
new file mode 100644
index 00000000..b56c3caa
--- /dev/null
+++ b/lua/leetcode/ui/problem/description/content/init.lua
@@ -0,0 +1,17 @@
+local markup = require("markup")
+local log = require("leetcode.logger")
+
+return markup.Component(function(self)
+    local content = self.props
+    local html_parser = require("markup.parser.html")
+
+    local parts = {}
+    for part in vim.gsplit(content, "<p>&nbsp;</p>") do
+        table.insert(parts, html_parser(part, "html"):parse())
+    end
+
+    return markup.Flex({
+        spacing = 2,
+        children = parts,
+    })
+end)
diff --git a/lua/leetcode/ui/problem/description/header.lua b/lua/leetcode/ui/problem/description/header.lua
new file mode 100644
index 00000000..d03c01af
--- /dev/null
+++ b/lua/leetcode/ui/problem/description/header.lua
@@ -0,0 +1,77 @@
+local markup = require("markup")
+local utils = require("leetcode.utils")
+local config = require("leetcode.config")
+local t = require("leetcode.translator")
+local icons = config.icons
+
+local function hl(str)
+    local prefix = "leetcode_description_header"
+    if not str then
+        return prefix
+    end
+    return table.concat({ prefix, str }, "_")
+end
+
+local Title = markup.Component(function(self)
+    ---@type lc.QuestionResponse
+    local info = self.props
+    local title = utils.translate(info.title, info.translated_title)
+
+    return markup.HFlex({
+        spacing = 1,
+        children = {
+            markup.Inline(info.frontend_id .. ".", "leetcode_normal"),
+            markup.Inline(title, hl("title")),
+            markup.If(info.is_paid_only, markup.Inline(t("Premium"), "leetcode_medium")),
+        },
+    })
+end)
+
+local Stats = markup.Component(function(self)
+    ---@type lc.QuestionResponse, lc.cache.Question
+    local info, cache = self.props.info, self.props.cache
+
+    local diff = info.difficulty
+    local status = cache.status
+    local dislikes = info.dislikes
+    local likes = info.likes
+
+    local ac_rate = info.stats.acRate
+    local total_sub = info.stats.totalSubmission
+    local hints = info.hints
+
+    return markup.HFlex({
+        spacing = 1,
+        children = {
+            markup.Inline(diff, "leetcode_" .. diff:lower()),
+            markup.Inline(icons.bar, hl("deli")),
+            markup.Inline(likes, hl("stat")),
+            markup.Inline(icons.like),
+            markup.Inline(dislikes, hl("stat")),
+            markup.Inline(icons.dislike),
+            markup.Inline(icons.bar, hl("deli")),
+            markup.Inline(ac_rate, hl("stat")),
+            markup.Inline("of"),
+            markup.Inline(total_sub),
+            markup.If(not vim.tbl_isempty(hints), {
+                markup.Inline(icons.bar, hl("deli")),
+                markup.Inline("󰛨 " .. t("Hints"), hl("hint")),
+            }),
+        },
+    })
+end)
+
+return markup.Component(function(self)
+    local cache, info = self.props.cache, self.props.info
+
+    return markup.Flex({
+        align = "center",
+        spacing = 1,
+        margin = { 1, 0, 2 },
+        style = { link = hl() },
+        children = {
+            Title(info),
+            Stats({ cache = cache, info = info }),
+        },
+    })
+end)
diff --git a/lua/leetcode/ui/problem/description/init.lua b/lua/leetcode/ui/problem/description/init.lua
new file mode 100644
index 00000000..3a1045c6
--- /dev/null
+++ b/lua/leetcode/ui/problem/description/init.lua
@@ -0,0 +1,45 @@
+local Header = require("leetcode.ui.problem.description.header")
+local Content = require("leetcode.ui.problem.description.content")
+local utils = require("leetcode.utils")
+
+local markup = require("markup")
+
+---@class leet.ui.Problem.Description : markup.Renderer
+---@field protected super markup.Renderer
+local Description = markup.Renderer:extend("leet.problem.description") ---@diagnostic disable-line: undefined-field
+
+---@param problem leet.ui.Problem
+function Description:init(problem)
+    Description.super.init(self, {
+        minimal = true,
+        position = "left",
+        config = {
+            vertical = true,
+            width = 0.4,
+        },
+        win_opts = {
+            wrap = true,
+            winhighlight = "Normal:MarkupNormalFloat,FloatBorder:MarkupFloatBorder",
+        },
+    })
+
+    self.problem = problem
+end
+
+function Description:render()
+    Description.super.render(self, {
+        Header({
+            cache = self.problem.cache,
+            info = self.problem.q,
+        }),
+        Content(utils.translate(self.problem.q.content, self.problem.q.translated_content)),
+    })
+end
+
+-- function Description:
+
+-- function Description:buf_open()
+--     return markup.Inline("Description")
+-- end
+
+return Description
diff --git a/lua/leetcode/ui/problem/info/init.lua b/lua/leetcode/ui/problem/info/init.lua
new file mode 100644
index 00000000..021370cc
--- /dev/null
+++ b/lua/leetcode/ui/problem/info/init.lua
@@ -0,0 +1,121 @@
+local markup = require("markup")
+local u = require("leetcode.utils")
+local log = require("leetcode.logger")
+local t = require("leetcode.translator")
+local problemlist = require("leetcode.cache.problemlist")
+local config = require("leetcode.config")
+local icons = config.icons
+
+---@class leet.ui.Problem.Info : markup.Renderer
+---@field protected super markup.Renderer
+---@field problem leet.ui.Problem
+local ProblemInfo = markup.Renderer:extend("markup.problem.info") ---@diagnostic disable-line: undefined-field
+
+---@param problem leet.ui.Problem
+function ProblemInfo:init(problem)
+    ProblemInfo.super.init(self, {
+        position = "float",
+        config = {
+            width = 0.5,
+            height = 0.5,
+            border = "rounded",
+            title = " Problem Info ",
+            title_pos = "center",
+        },
+        win_opts = {
+            wrap = true,
+        },
+        show = false,
+        enter = true,
+    })
+    self.problem = problem
+end
+
+function ProblemInfo:build_hints()
+    local hints = self.problem.q.hints
+
+    local children = {}
+    local total = #hints
+
+    for i, hint in ipairs(hints) do
+        local tree = markup.Tree({
+            title = ("%d/%d"):format(i, total),
+            children = { markup.Block(hint) },
+        })
+        table.insert(children, tree)
+    end
+
+    return markup.Tree({
+        title = t("Hints"),
+        children = children,
+    })
+end
+
+function ProblemInfo:build_tags()
+    local tags = self.problem.q.topic_tags
+
+    local children = {}
+
+    for _, topic in ipairs(tags) do
+        table.insert(children, markup.Block(topic.name, "leetcode_tag"))
+    end
+
+    return markup.Tree({
+        title = t("Topics"),
+        children = children,
+    })
+end
+
+function ProblemInfo:build_similar_questions()
+    local similar = self.problem.q.similar
+
+    local children = {}
+
+    for _, problem in ipairs(similar) do
+        local ok, cache = pcall(problemlist.get_by_title_slug, problem.title_slug)
+
+        if ok then
+            local elem = markup.HFlex({
+                spacing = 1,
+                on_submit = function()
+                    local Problem = require("leetcode.ui.problem")
+                    Problem(cache):mount()
+                end,
+                children = {
+                    markup.Inline(icons.square, u.diff_to_hl(cache.difficulty)),
+                    markup.Inline(u.translate(cache.title, cache.title_cn)),
+                    markup.If(
+                        cache.paid_only,
+                        markup.Inline({
+                            config.auth.is_premium and { icons.unlock }
+                                or { icons.lock, " ", t("Premium") },
+                        }, "leetcode_medium")
+                    ),
+                },
+            })
+
+            table.insert(children, elem)
+        end
+    end
+
+    return markup.Tree({
+        title = t("Similar Questions"),
+        children = children,
+    })
+end
+
+function ProblemInfo:render()
+    ProblemInfo.super.render(
+        self,
+        markup.Flex({
+            spacing = 2,
+            children = {
+                self:build_hints(),
+                self:build_tags(),
+                self:build_similar_questions(),
+            },
+        })
+    )
+end
+
+return ProblemInfo
diff --git a/lua/leetcode/ui/problem/init.lua b/lua/leetcode/ui/problem/init.lua
new file mode 100644
index 00000000..9c85552f
--- /dev/null
+++ b/lua/leetcode/ui/problem/init.lua
@@ -0,0 +1,251 @@
+local Description = require("leetcode.ui.problem.description")
+local Info = require("leetcode.ui.problem.info")
+local Console = require("leetcode-ui.layout.console")
+local Window = require("leetcode.ui.problem.window")
+local markup = require("markup")
+
+local api_question = require("leetcode.api.question")
+local utils = require("leetcode.utils")
+local ui_utils = require("leetcode-ui.utils")
+local config = require("leetcode.config")
+local log = require("leetcode.logger")
+
+---@class leet.ui.Problem : markup.TabPage
+---@field protected super markup.TabPage
+---@field file Path
+---@field q lc.question_res
+---@field description lc.ui.Description
+---@field bufnr integer
+---@field console lc.ui.Console
+---@field lang string
+---@field cache lc.cache.Question
+---@field reset boolean
+---@field win leet.ui.Problem.Window
+local Problem = markup.TabPage:extend("leet.question")
+
+---@param raw? boolean
+function Problem:snippet(raw)
+    local snippets = self.q.code_snippets ~= vim.NIL and self.q.code_snippets or {}
+    local snip = vim.tbl_filter(function(snip)
+        return snip.lang_slug == self.lang
+    end, snippets)[1]
+    if not snip then
+        return
+    end
+
+    local code = snip.code:gsub("\r\n", "\n")
+    return raw and code or self:injector(code)
+end
+
+---@return string path, boolean existed
+function Problem:path()
+    local lang = utils.get_lang(self.lang)
+    local alt = lang.alt and ("." .. lang.alt) or ""
+
+    -- handle legacy file names first
+    local fn_legacy = --
+        ("%s.%s-%s.%s"):format(self.q.frontend_id, self.q.title_slug, lang.slug, lang.ft)
+    self.file = config.storage.home:joinpath(fn_legacy)
+
+    if self.file:exists() then
+        return self.file:absolute(), true
+    end
+
+    local fn = ("%s.%s%s.%s"):format(self.q.frontend_id, self.q.title_slug, alt, lang.ft)
+    self.file = config.storage.home:joinpath(fn)
+    local existed = self.file:exists()
+
+    if not existed then
+        self.file:write(self:snippet(), "w")
+    end
+
+    return self.file:absolute(), existed
+end
+
+---@param before boolean
+function Problem:inject(before)
+    local inject = config.user.injector[self.lang] or {}
+    local inj = before and inject.before or inject.after
+
+    local res
+
+    if type(inj) == "boolean" and inj == true and before then
+        inj = config.imports[self.lang]
+    end
+
+    if type(inj) == "table" then
+        res = table.concat(inj, "\n")
+    elseif type(inj) == "string" then
+        res = inj
+    end
+
+    if res and res ~= "" then
+        return before and (res .. "\n") or ("\n" .. res)
+    else
+        return nil
+    end
+end
+
+---@param code string
+function Problem:injector(code)
+    local lang = utils.get_lang(self.lang)
+
+    local parts = {
+        ("%s @leet start"):format(lang.comment),
+        code,
+        ("%s @leet end"):format(lang.comment),
+    }
+
+    local before = self:inject(true)
+    if before then
+        table.insert(parts, 1, before)
+    end
+
+    local after = self:inject(false)
+    if after then
+        table.insert(parts, after)
+    end
+
+    return table.concat(parts, "\n")
+end
+
+function Problem:_unmount()
+    if vim.v.dying ~= 0 then
+        return
+    end
+
+    vim.schedule(function()
+        self.info:unmount()
+        self.console:unmount()
+        self.description:unmount()
+
+        if self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr) then
+            vim.api.nvim_buf_delete(self.bufnr, { force = true, unload = false })
+        end
+
+        _Lc_state.questions = vim.tbl_filter(function(q)
+            return q.bufnr ~= self.bufnr
+        end, _Lc_state.questions)
+
+        self = nil
+    end)
+end
+
+function Problem:unmount()
+    if self.winid and vim.api.nvim_win_is_valid(self.winid) then
+        vim.api.nvim_win_close(self.winid, true)
+    end
+end
+
+local group = vim.api.nvim_create_augroup("leetcode_questions", { clear = true })
+function Problem:autocmds()
+    vim.api.nvim_create_autocmd("WinClosed", {
+        group = group,
+        pattern = tostring(self.winid),
+        callback = function()
+            self:_unmount()
+        end,
+    })
+end
+
+function Problem:handle_mount()
+    local path, existed = self:path()
+
+    self:set_file(path)
+    Problem.super.mount(self)
+
+    self.win = Window({
+        problem = self,
+        minimal = false,
+        bufnr = 0,
+        winid = 0,
+        buf_opts = {
+            buflisted = true,
+        },
+    })
+
+    -- self:open_buffer(existed)
+
+    self.description = Description(self)
+    -- self.console = Console(self)
+    self.info = Info(self)
+
+    table.insert(_Lc_state.questions, self)
+
+    -- self:autocmds()
+    -- utils.exec_hook("question_enter", self)
+
+    return self
+end
+
+function Problem:mount()
+    local tabp = utils.detect_duplicate_question(self.cache.title_slug, config.lang)
+    if tabp then
+        return pcall(vim.api.nvim_set_current_tabpage, tabp)
+    end
+
+    local q = api_question.by_title_slug(self.cache.title_slug)
+    if not q or q.is_paid_only and not config.auth.is_premium then
+        return log.warn("Question is for premium users only")
+    end
+    self.q = q
+
+    if self:snippet() then
+        self:handle_mount()
+    else
+        local msg = ("Snippet for `%s` not found. Select a different language"):format(self.lang)
+        log.warn(msg)
+
+        require("leetcode.pickers.language").pick_lang(self, function(snippet)
+            self.lang = snippet.t.slug
+            self:handle_mount()
+        end)
+    end
+
+    return self
+end
+
+---@param self lc.ui.Question
+---@param lang lc.lang
+Problem.change_lang = vim.schedule_wrap(function(self, lang)
+    local old_lang, old_bufnr = self.lang, self.bufnr
+
+    local ok, err = pcall(function()
+        self.lang = lang
+        local path, existed = self:path()
+
+        self.bufnr = vim.fn.bufadd(path)
+        assert(self.bufnr ~= 0, "Failed to create buffer " .. path)
+
+        local loaded = vim.api.nvim_buf_is_loaded(self.bufnr)
+        vim.fn.bufload(self.bufnr)
+
+        vim.api.nvim_set_option_value("buflisted", false, { buf = old_bufnr })
+        self:open_buffer(existed)
+
+        if not loaded then
+            utils.exec_hook("question_enter", self)
+        end
+    end)
+
+    if not ok then
+        log.error("Failed to change language\n" .. err)
+        self.lang = old_lang
+        self.bufnr = old_bufnr
+    end
+end)
+
+---@param problem lc.cache.Question
+function Problem:init(problem)
+    self.cache = problem
+    self.lang = config.lang
+
+    Problem.super.init(self, {
+        mount = true,
+    })
+end
+
+---@type fun(question: lc.cache.Question): leet.ui.Problem
+local LeetProblem = Problem
+
+return LeetProblem
diff --git a/lua/leetcode/ui/problem/window.lua b/lua/leetcode/ui/problem/window.lua
new file mode 100644
index 00000000..03e4471e
--- /dev/null
+++ b/lua/leetcode/ui/problem/window.lua
@@ -0,0 +1,97 @@
+local markup = require("markup")
+local log = require("leetcode.logger")
+
+---@class leet.ui.Problem.Window : markup.Window
+---@field protected super markup.Window
+---@field problem leet.ui.Problem
+local ProblemWindow = markup.Window:extend("leet.problem.window")
+
+function ProblemWindow:init(...)
+    ProblemWindow.super.init(self, ...)
+    self.problem = self._.problem
+end
+
+---@param inclusive? boolean
+---@return integer, integer, string[]
+function ProblemWindow:code_range(inclusive)
+    local lines = self:buf_get_lines(0, -1, false)
+    local start_i, end_i
+
+    for i, line in ipairs(lines) do
+        if line:match("@leet start") then
+            start_i = i + (inclusive and 0 or 1)
+        elseif line:match("@leet end") then
+            end_i = i - (inclusive and 0 or 1)
+        end
+    end
+
+    return start_i, end_i, lines
+end
+
+---@param submit boolean
+function ProblemWindow:code_lines(submit)
+    local start_i, end_i, lines = self:code_range()
+
+    start_i = start_i or 1
+    end_i = end_i or #lines
+
+    local prefix = not submit and ("\n"):rep(start_i - 1) or ""
+    return prefix .. table.concat(lines, "\n", start_i, end_i)
+end
+
+function ProblemWindow:fold_range()
+    local start_i, _, lines = self:code_range(true)
+    if start_i == nil or start_i <= 1 then
+        return
+    end
+
+    local i = start_i - 1
+    while lines[i] == "" do
+        i = i - 1
+    end
+
+    if 1 < i then
+        return i
+    end
+end
+
+---@param code? string
+function ProblemWindow:set_code_lines(code)
+    if not (self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr)) then
+        return
+    end
+
+    pcall(vim.cmd.undojoin)
+    local s_i, e_i, lines = self:code_range()
+    s_i = s_i or 1
+    e_i = e_i or #lines
+    code = code and code or (self.problem:snippet(true) or "")
+    vim.api.nvim_buf_set_lines(self.bufnr, s_i - 1, e_i, false, vim.split(code, "\n"))
+end
+
+function ProblemWindow:reset_code_lines()
+    local new_lines = self.problem:snippet(true) or ""
+
+    -- vim.schedule(function()
+    --     log.info("Previous code found and reset\nTo undo, simply press `u`")
+    -- end)
+
+    self:set_code_lines(new_lines)
+end
+
+function ProblemWindow:buf_open()
+    ProblemWindow.super.buf_open(self)
+
+    local i = self:fold_range()
+    if i then
+        vim.api.nvim_buf_call(self.bufnr, function()
+            vim.cmd(("%d,%dfold"):format(1, i)) ---@diagnostic disable-line: param-type-mismatch
+        end)
+    end
+
+    -- if existed and self.cache.status == "ac" then
+    -- self:reset_code_lines()
+    -- end
+end
+
+return ProblemWindow

From f91524f107b87a397f26a61a2aeb383be1214746 Mon Sep 17 00:00:00 2001
From: kawre <kenterq@gmail.com>
Date: Fri, 1 Aug 2025 12:52:21 +0200
Subject: [PATCH 2/4] refactor: problem layout

---
 .neoconf.json                                 |  47 +--
 lua/leetcode-plugins/cn/api.lua               |   2 +-
 .../non_standalone/leetcode.lua               |   4 +-
 lua/leetcode-ui/group/case.lua                |   2 +-
 lua/leetcode-ui/group/pre/stdout.lua          |   2 +-
 lua/leetcode-ui/group/similar-questions.lua   |   6 +-
 lua/leetcode-ui/layout/console.lua            |   2 +-
 lua/leetcode-ui/lines/calendar.lua            |   2 +-
 lua/leetcode-ui/lines/solved.lua              |   2 +-
 lua/leetcode-ui/popup/console/result.lua      |   2 +-
 lua/leetcode-ui/popup/languages.lua           |   4 +-
 lua/leetcode-ui/popup/skills.lua              |   2 +-
 lua/leetcode-ui/question.lua                  |   2 +-
 lua/leetcode-ui/renderer/menu.lua             |   2 +-
 lua/leetcode-ui/renderer/result.lua           |   2 +-
 lua/leetcode-ui/split/description.lua         |   2 +-
 lua/leetcode-ui/utils.lua                     |   4 +-
 lua/leetcode/api/auth.lua                     |  48 +--
 lua/leetcode/api/headers.lua                  |  23 --
 lua/leetcode/api/init.lua                     | 201 +++++++++++
 lua/leetcode/api/interpreter.lua              |   7 +-
 lua/leetcode/api/problems.lua                 | 155 ++++++++-
 lua/leetcode/api/queries.lua                  | 314 +++++++++---------
 lua/leetcode/api/question.lua                 |  89 -----
 .../api/{statistics.lua => stats.lua}         |  46 ++-
 lua/leetcode/api/urls.lua                     |   2 +-
 lua/leetcode/api/utils.lua                    | 251 --------------
 lua/leetcode/cache/cookie.lua                 |  24 +-
 lua/leetcode/cache/init.lua                   |  17 +-
 .../cache/{problemlist.lua => problems.lua}   |  52 +--
 lua/leetcode/command/init.lua                 |  60 ++--
 lua/leetcode/config/init.lua                  |   2 +-
 lua/leetcode/config/stats.lua                 |   4 +-
 lua/leetcode/guard.lua                        |  30 ++
 lua/{leetcode.lua => leetcode/init.lua}       |  30 +-
 lua/leetcode/picker/init.lua                  |   2 +-
 lua/leetcode/picker/language/init.lua         |   2 +-
 lua/leetcode/picker/question/fzf.lua          |   4 +-
 lua/leetcode/picker/question/init.lua         |   6 +-
 lua/leetcode/picker/tabs/init.lua             |   2 +-
 lua/leetcode/problem/console/init.lua         |  37 +++
 lua/leetcode/problem/console/result.lua       |  13 +
 lua/leetcode/problem/console/testcase.lua     |  13 +
 lua/leetcode/problem/description.lua          |  16 +
 lua/leetcode/problem/init.lua                 | 204 ++++++++++++
 .../{runner/init.lua => problem/runner.lua}   |   4 +-
 lua/leetcode/runner/utils.lua                 |   8 -
 lua/leetcode/ui/menu/button/back.lua          |   6 +-
 lua/leetcode/ui/menu/button/init.lua          |   6 +-
 lua/leetcode/ui/menu/ctx.lua                  |   7 +
 lua/leetcode/ui/menu/footer.lua               |  27 +-
 lua/leetcode/ui/menu/header.lua               |  21 +-
 lua/leetcode/ui/menu/init.lua                 |  25 +-
 lua/leetcode/ui/menu/nav/menu/cache/init.lua  |   6 +-
 lua/leetcode/ui/menu/nav/menu/cookie/init.lua |   6 +-
 lua/leetcode/ui/menu/nav/menu/init.lua        |  10 +-
 .../ui/menu/nav/menu/problems/init.lua        |   6 +-
 .../ui/menu/nav/menu/statistics/init.lua      |  10 +-
 lua/leetcode/ui/menu/title.lua                |   6 +-
 .../ui/problem/description/content/init.lua   |  17 -
 .../ui/problem/description/header.lua         |  77 -----
 lua/leetcode/ui/problem/description/init.lua  |  45 ---
 lua/leetcode/ui/problem/info/init.lua         | 121 -------
 lua/leetcode/ui/problem/init.lua              | 251 --------------
 lua/leetcode/ui/problem/window.lua            |  97 ------
 lua/leetcode/{utils.lua => util.lua}          |  44 +--
 66 files changed, 1131 insertions(+), 1412 deletions(-)
 delete mode 100644 lua/leetcode/api/headers.lua
 create mode 100644 lua/leetcode/api/init.lua
 delete mode 100644 lua/leetcode/api/question.lua
 rename lua/leetcode/api/{statistics.lua => stats.lua} (83%)
 delete mode 100644 lua/leetcode/api/utils.lua
 rename lua/leetcode/cache/{problemlist.lua => problems.lua} (74%)
 create mode 100644 lua/leetcode/guard.lua
 rename lua/{leetcode.lua => leetcode/init.lua} (87%)
 create mode 100644 lua/leetcode/problem/console/init.lua
 create mode 100644 lua/leetcode/problem/console/result.lua
 create mode 100644 lua/leetcode/problem/console/testcase.lua
 create mode 100644 lua/leetcode/problem/description.lua
 create mode 100644 lua/leetcode/problem/init.lua
 rename lua/leetcode/{runner/init.lua => problem/runner.lua} (96%)
 delete mode 100644 lua/leetcode/runner/utils.lua
 create mode 100644 lua/leetcode/ui/menu/ctx.lua
 delete mode 100644 lua/leetcode/ui/problem/description/content/init.lua
 delete mode 100644 lua/leetcode/ui/problem/description/header.lua
 delete mode 100644 lua/leetcode/ui/problem/description/init.lua
 delete mode 100644 lua/leetcode/ui/problem/info/init.lua
 delete mode 100644 lua/leetcode/ui/problem/init.lua
 delete mode 100644 lua/leetcode/ui/problem/window.lua
 rename lua/leetcode/{utils.lua => util.lua} (80%)

diff --git a/.neoconf.json b/.neoconf.json
index b6608e1c..f25436c6 100644
--- a/.neoconf.json
+++ b/.neoconf.json
@@ -1,25 +1,26 @@
 {
-  "neodev": {
-    "library": {
-      "enabled": true,
-      "plugins": [
-        "telescope.nvim",
-        "plenary.nvim",
-        "nui.nvim",
-        "nvim-web-devicons",
-        "image.nvim",
-        "nvim-treesitter"
-      ]
-    }
-  },
-  "neoconf": {
-    "plugins": {
-      "lua_ls": {
-        "enabled": true
-      }
-    }
-  },
-  "lspconfig": {
-    "sumneko_lua": {}
-  }
+	"neodev": {
+		"library": {
+			"enabled": true,
+			"plugins": [
+				"telescope.nvim",
+				"plenary.nvim",
+				"nui.nvim",
+				"nvim-web-devicons",
+				"image.nvim",
+				"nvim-treesitter",
+				"markup.nvim"
+			]
+		}
+	},
+	"neoconf": {
+		"plugins": {
+			"lua_ls": {
+				"enabled": true
+			}
+		}
+	},
+	"lspconfig": {
+		"sumneko_lua": {}
+	}
 }
diff --git a/lua/leetcode-plugins/cn/api.lua b/lua/leetcode-plugins/cn/api.lua
index ee93f294..deb55e2c 100644
--- a/lua/leetcode-plugins/cn/api.lua
+++ b/lua/leetcode-plugins/cn/api.lua
@@ -1,6 +1,6 @@
 local log = require("leetcode.logger")
 local config = require("leetcode.config")
-local statistics = require("leetcode.api.statistics")
+local statistics = require("leetcode.api.stats")
 local queries = require("leetcode.api.queries")
 local urls = require("leetcode.api.urls")
 local utils = require("leetcode.api.utils")
diff --git a/lua/leetcode-plugins/non_standalone/leetcode.lua b/lua/leetcode-plugins/non_standalone/leetcode.lua
index 9cb95d61..195bb3db 100644
--- a/lua/leetcode-plugins/non_standalone/leetcode.lua
+++ b/lua/leetcode-plugins/non_standalone/leetcode.lua
@@ -36,7 +36,7 @@ leetcode.start = function(on_vimenter)
     local Menu = require("leetcode-ui.renderer.menu")
     Menu():mount()
 
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     utils.exec_hooks("enter")
 
     return true
@@ -55,7 +55,7 @@ leetcode.stop = vim.schedule_wrap(function()
         desc = "Open leetcode.nvim",
     })
 
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     utils.exec_hooks("leave")
 
     if prev_cwd then
diff --git a/lua/leetcode-ui/group/case.lua b/lua/leetcode-ui/group/case.lua
index 8da0169b..374bfdd2 100644
--- a/lua/leetcode-ui/group/case.lua
+++ b/lua/leetcode-ui/group/case.lua
@@ -1,4 +1,4 @@
-local utils = require("leetcode.utils")
+local utils = require("leetcode.util")
 
 local Pre = require("leetcode-ui.group.pre")
 local Input = require("leetcode-ui.group.pre.input")
diff --git a/lua/leetcode-ui/group/pre/stdout.lua b/lua/leetcode-ui/group/pre/stdout.lua
index 2f1f1ea2..9cc98b0a 100644
--- a/lua/leetcode-ui/group/pre/stdout.lua
+++ b/lua/leetcode-ui/group/pre/stdout.lua
@@ -3,7 +3,7 @@ local t = require("leetcode.translator")
 local Line = require("leetcode-ui.line")
 local Lines = require("leetcode-ui.lines")
 
-local utils = require("leetcode.utils")
+local utils = require("leetcode.util")
 local log = require("leetcode.logger")
 
 ---@class lc.ui.Stdout : lc.ui.Pre
diff --git a/lua/leetcode-ui/group/similar-questions.lua b/lua/leetcode-ui/group/similar-questions.lua
index 9bcf45f7..9c59afa0 100644
--- a/lua/leetcode-ui/group/similar-questions.lua
+++ b/lua/leetcode-ui/group/similar-questions.lua
@@ -2,9 +2,9 @@ local Button = require("leetcode-ui.lines.button")
 local Group = require("leetcode-ui.group")
 local NuiTree = require("nui.tree")
 
-local problemlist = require("leetcode.cache.problemlist")
+local problemlist = require("leetcode.cache.problems")
 local config = require("leetcode.config")
-local utils = require("leetcode.utils")
+local utils = require("leetcode.util")
 local ui_utils = require("leetcode-ui.utils")
 
 local t = require("leetcode.translator")
@@ -19,7 +19,7 @@ function SimilarQuestions:init(questions)
     SimilarQuestions.super.init(self)
 
     for _, q in ipairs(questions) do
-        local ok, p = pcall(problemlist.get_by_title_slug, q.title_slug)
+        local ok, p = pcall(problemlist.by_slug, q.title_slug)
 
         if ok then
             local button = Button({}, {
diff --git a/lua/leetcode-ui/layout/console.lua b/lua/leetcode-ui/layout/console.lua
index b7e88c01..0ec6c466 100644
--- a/lua/leetcode-ui/layout/console.lua
+++ b/lua/leetcode-ui/layout/console.lua
@@ -6,7 +6,7 @@ local Popup = require("leetcode-ui.popup")
 
 local config = require("leetcode.config")
 local keys = config.user.keys
-local Runner = require("leetcode.runner")
+local Runner = require("leetcode.problem.runner")
 local log = require("leetcode.logger")
 
 ---@class lc.ui.Console : lc.ui.Layout
diff --git a/lua/leetcode-ui/lines/calendar.lua b/lua/leetcode-ui/lines/calendar.lua
index 99e34473..d96d29e7 100644
--- a/lua/leetcode-ui/lines/calendar.lua
+++ b/lua/leetcode-ui/lines/calendar.lua
@@ -1,7 +1,7 @@
 local config = require("leetcode.config")
 local Spinner = require("leetcode.logger.spinner")
 local Lines = require("leetcode-ui.lines")
-local statistics = require("leetcode.api.statistics")
+local statistics = require("leetcode.api.stats")
 local t = require("leetcode.translator")
 
 local Line = require("leetcode-ui.line")
diff --git a/lua/leetcode-ui/lines/solved.lua b/lua/leetcode-ui/lines/solved.lua
index 7e324d3f..3583845e 100644
--- a/lua/leetcode-ui/lines/solved.lua
+++ b/lua/leetcode-ui/lines/solved.lua
@@ -2,7 +2,7 @@ local Lines = require("leetcode-ui.lines")
 local Line = require("leetcode-ui.line")
 local utils = require("leetcode-ui.utils")
 local Spinner = require("leetcode.logger.spinner")
-local statistics = require("leetcode.api.statistics")
+local statistics = require("leetcode.api.stats")
 local t = require("leetcode.translator")
 
 local log = require("leetcode.logger")
diff --git a/lua/leetcode-ui/popup/console/result.lua b/lua/leetcode-ui/popup/console/result.lua
index 520113d2..c62ca47e 100644
--- a/lua/leetcode-ui/popup/console/result.lua
+++ b/lua/leetcode-ui/popup/console/result.lua
@@ -2,7 +2,7 @@ local ConsolePopup = require("leetcode-ui.popup.console")
 local ResultLayout = require("leetcode-ui.renderer.result")
 local t = require("leetcode.translator")
 
-local problemlist = require("leetcode.cache.problemlist")
+local problemlist = require("leetcode.cache.problems")
 local log = require("leetcode.logger")
 local config = require("leetcode.config")
 
diff --git a/lua/leetcode-ui/popup/languages.lua b/lua/leetcode-ui/popup/languages.lua
index 6ca9e3f9..19d29660 100644
--- a/lua/leetcode-ui/popup/languages.lua
+++ b/lua/leetcode-ui/popup/languages.lua
@@ -5,9 +5,9 @@ local Lines = require("leetcode-ui.lines")
 local log = require("leetcode.logger")
 local t = require("leetcode.translator")
 
-local utils = require("leetcode.utils")
+local utils = require("leetcode.util")
 local config = require("leetcode.config")
-local stats_api = require("leetcode.api.statistics")
+local stats_api = require("leetcode.api.stats")
 local Spinner = require("leetcode.logger.spinner")
 
 ---@class lc.ui.LanguagesPopup : lc.ui.SkillsPopup
diff --git a/lua/leetcode-ui/popup/skills.lua b/lua/leetcode-ui/popup/skills.lua
index 2dcf24d7..e7619f6e 100644
--- a/lua/leetcode-ui/popup/skills.lua
+++ b/lua/leetcode-ui/popup/skills.lua
@@ -2,7 +2,7 @@ local Popup = require("leetcode-ui.popup")
 local Group = require("leetcode-ui.group")
 local Lines = require("leetcode-ui.lines")
 
-local stats_api = require("leetcode.api.statistics")
+local stats_api = require("leetcode.api.stats")
 local config = require("leetcode.config")
 local Spinner = require("leetcode.logger.spinner")
 local utils = require("leetcode-ui.utils")
diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua
index a72ccafb..b9a9516c 100644
--- a/lua/leetcode-ui/question.lua
+++ b/lua/leetcode-ui/question.lua
@@ -4,7 +4,7 @@ local Info = require("leetcode-ui.popup.info")
 local Object = require("nui.object")
 
 local api_question = require("leetcode.api.question")
-local utils = require("leetcode.utils")
+local utils = require("leetcode.util")
 local ui_utils = require("leetcode-ui.utils")
 local config = require("leetcode.config")
 local log = require("leetcode.logger")
diff --git a/lua/leetcode-ui/renderer/menu.lua b/lua/leetcode-ui/renderer/menu.lua
index 4b0d9eac..914b1842 100644
--- a/lua/leetcode-ui/renderer/menu.lua
+++ b/lua/leetcode-ui/renderer/menu.lua
@@ -2,7 +2,7 @@ local log = require("leetcode.logger")
 local cookie = require("leetcode.cache.cookie")
 local config = require("leetcode.config")
 local ui_utils = require("leetcode-ui.utils")
-local utils = require("leetcode.utils")
+local utils = require("leetcode.util")
 local Renderer = require("leetcode-ui.renderer")
 local api = vim.api
 
diff --git a/lua/leetcode-ui/renderer/result.lua b/lua/leetcode-ui/renderer/result.lua
index 23110ad3..1221e0ea 100644
--- a/lua/leetcode-ui/renderer/result.lua
+++ b/lua/leetcode-ui/renderer/result.lua
@@ -1,6 +1,6 @@
 local Cases = require("leetcode-ui.group.cases")
 local Header = require("leetcode-ui.lines.header")
-local utils = require("leetcode.utils")
+local utils = require("leetcode.util")
 local SimilarQuestions = require("leetcode-ui.group.similar-questions")
 
 local Renderer = require("leetcode-ui.renderer")
diff --git a/lua/leetcode-ui/split/description.lua b/lua/leetcode-ui/split/description.lua
index a5c16c05..977768fd 100644
--- a/lua/leetcode-ui/split/description.lua
+++ b/lua/leetcode-ui/split/description.lua
@@ -8,7 +8,7 @@ local Split = require("leetcode-ui.split")
 local Parser = require("leetcode.parser")
 
 local log = require("leetcode.logger")
-local utils = require("leetcode.utils")
+local utils = require("leetcode.util")
 
 local t = require("leetcode.translator")
 
diff --git a/lua/leetcode-ui/utils.lua b/lua/leetcode-ui/utils.lua
index b6d2aa4e..253e3cc1 100644
--- a/lua/leetcode-ui/utils.lua
+++ b/lua/leetcode-ui/utils.lua
@@ -132,7 +132,7 @@ end
 
 ---@param winid number
 function utils.win_set_winfixbuf(winid)
-    local u = require("leetcode.utils")
+    local u = require("leetcode.util")
     u.with_version("0.10.0", function()
         utils.win_set_opts(winid, { winfixbuf = true })
     end)
@@ -142,7 +142,7 @@ end
 ---@param bufnr number
 ---@param force? boolean
 function utils.win_set_buf(winid, bufnr, force)
-    local u = require("leetcode.utils")
+    local u = require("leetcode.util")
 
     u.with_version("0.10.0", function()
         local wfb = vim.api.nvim_win_get_option(winid, "winfixbuf")
diff --git a/lua/leetcode/api/auth.lua b/lua/leetcode/api/auth.lua
index cef2f941..12dfd497 100644
--- a/lua/leetcode/api/auth.lua
+++ b/lua/leetcode/api/auth.lua
@@ -1,33 +1,14 @@
-local utils = require("leetcode.api.utils")
 local config = require("leetcode.config")
 local log = require("leetcode.logger")
 local queries = require("leetcode.api.queries")
 local urls = require("leetcode.api.urls")
 
----@class lc.AuthApi
-local Auth = {}
-
----@return lc.UserStatus, lc.err
-function Auth.user(cb)
-    local query = queries.auth
-
-    if cb then
-        utils.query(query, {}, {
-            callback = function(res, err)
-                cb(Auth.handle(res, err))
-            end,
-            endpoint = urls.auth,
-        })
-    else
-        return Auth.handle(utils.query(query, {}, {
-            endpoint = urls.auth,
-        }))
-    end
-end
+---@class leet.api.auth
+local M = {}
 
 ---@private
 ---@return lc.UserStatus, lc.err
-function Auth.handle(res, err)
+local function handle(res, err)
     if err then
         return res, err
     end
@@ -49,7 +30,28 @@ function Auth.handle(res, err)
     end
 
     config.auth = log.debug(auth) ---@diagnostic disable-line
+    Leet.auth:set(function()
+        return { auth = Markup.list(auth) }
+    end)
     return auth
 end
 
-return Auth
+---@return lc.UserStatus, lc.err
+function M.user(cb)
+    local query = queries.auth
+
+    if cb then
+        Leet.api.query(query, {}, {
+            callback = function(res, err)
+                cb(handle(res, err))
+            end,
+            endpoint = urls.auth,
+        })
+    else
+        return handle(Leet.api.query(query, {}, {
+            endpoint = urls.auth,
+        }))
+    end
+end
+
+return M
diff --git a/lua/leetcode/api/headers.lua b/lua/leetcode/api/headers.lua
deleted file mode 100644
index a476a12f..00000000
--- a/lua/leetcode/api/headers.lua
+++ /dev/null
@@ -1,23 +0,0 @@
-local Cookie = require("leetcode.cache.cookie")
-local config = require("leetcode.config")
-
-local headers = {}
-
-function headers.get()
-    local cookie = Cookie.get()
-
-    return vim.tbl_extend("force", {
-        ["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
-        ["Referer"] = ("https://leetcode.%s"):format(config.domain),
-        ["Origin"] = ("https://leetcode.%s/"):format(config.domain),
-        ["Content-Type"] = "application/json",
-        ["Accept"] = "application/json",
-        ["Host"] = ("leetcode.%s"):format(config.domain),
-        -- ["X-Requested-With"] = "XMLHttpRequest",
-    }, cookie and {
-        ["Cookie"] = cookie.str,
-        ["x-csrftoken"] = cookie.csrftoken,
-    } or {})
-end
-
-return headers
diff --git a/lua/leetcode/api/init.lua b/lua/leetcode/api/init.lua
new file mode 100644
index 00000000..556af9ab
--- /dev/null
+++ b/lua/leetcode/api/init.lua
@@ -0,0 +1,201 @@
+local Cookie = require("leetcode.cache.cookie")
+local config = require("leetcode.config")
+local urls = require("leetcode.api.urls")
+local curl = require("plenary.curl")
+
+---@alias leet.api.method
+---| fun(endpoint: string, opts?: table)
+
+---@class leet.api
+---@field problems leet.api.problems
+---@field queries leet.api.queries
+---@field interpreter leet.api.interpreter
+---@field post leet.api.method
+---@field get leet.api.method
+---@field put leet.api.method
+---@overload fun(method: string, endpoint: string, opts?: table): any
+local M = setmetatable({}, {
+    __index = function(t, k)
+        if k == "post" or k == "get" or k == "put" then
+            t[k] = function(endpoint, opts)
+                opts = vim.tbl_extend("keep", opts or {}, { endpoint = endpoint })
+                return t(k, endpoint, opts)
+            end
+        else
+            t[k] = require("leetcode.api." .. k)
+        end
+        return rawget(t, k)
+    end,
+    __call = function(t, ...)
+        return t.curl(...)
+    end,
+})
+
+local function headers()
+    local cookie = Cookie.get()
+
+    return vim.tbl_extend("force", {
+        ["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
+        ["Referer"] = ("https://leetcode.%s"):format(config.domain),
+        ["Origin"] = ("https://leetcode.%s/"):format(config.domain),
+        ["Content-Type"] = "application/json",
+        ["Accept"] = "application/json",
+        ["Host"] = ("leetcode.%s"):format(config.domain),
+        -- ["X-Requested-With"] = "XMLHttpRequest",
+    }, cookie and {
+        ["Cookie"] = cookie.str,
+        ["x-csrftoken"] = cookie.csrftoken,
+    } or {})
+end
+
+---@param err lc.err
+local function check_err(err)
+    if not err then
+        return
+    end
+
+    if err.status then
+        if err.status == 401 or err.status == 403 then
+            err.msg =
+                "Your cookie may have expired, or LeetCode has temporarily restricted API access"
+        end
+    end
+
+    return err
+end
+
+---@private
+---@return table, lc.err
+local function handle_res(out)
+    local res, err
+    Markup.log.debug(out)
+
+    if out.exit ~= 0 then
+        err = {
+            code = out.exit,
+            msg = "curl failed",
+        }
+    elseif out.status >= 300 then
+        local ok, msg = pcall(function()
+            local dec = vim.json.decode(out.body)
+
+            if dec.error then
+                return dec.error
+            end
+
+            local tbl = {}
+            for _, e in ipairs(dec.errors) do
+                table.insert(tbl, e.message)
+            end
+
+            return table.concat(tbl, "\n")
+        end)
+
+        res = out.body
+        err = {
+            code = 0,
+            status = out.status,
+            response = out,
+            msg = "http error " .. out.status .. (ok and ("\n\n" .. msg) or ""),
+            out = out.body,
+        }
+    else
+        res = vim.json.decode(out.body)
+    end
+
+    return res, check_err(err)
+end
+
+---@private
+---@param method string
+---@param params table
+function M.curl(method, params)
+    local params_cpy = vim.deepcopy(params)
+
+    params = vim.tbl_deep_extend("force", {
+        headers = headers(),
+        compressed = false,
+        retry = 5,
+        endpoint = urls.base,
+    }, params or {})
+    local url = ("https://leetcode.%s%s"):format(config.domain, params.endpoint)
+
+    if type(params.body) == "table" then
+        params.body = vim.json.encode(params.body)
+    end
+
+    local tries = params.retry
+    local function should_retry(err)
+        return err and err.status >= 500 and tries > 0
+    end
+
+    if params.callback then
+        local cb = vim.schedule_wrap(params.callback)
+        params.callback = function(out, _)
+            local res, err = handle_res(out)
+
+            if should_retry(err) then
+                Markup.log.debug("retry " .. tries)
+                params_cpy.retry = tries - 1
+                M.curl(method, params_cpy)
+            else
+                cb(res, err)
+            end
+        end
+
+        curl[method](url, params)
+    else
+        local out = curl[method](url, params)
+        local res, err = handle_res(out)
+
+        if should_retry(err) then
+            Markup.log.debug("retry " .. tries)
+            params_cpy.retry = tries - 1
+            M.curl(method, params_cpy)
+        else
+            return res, err
+        end
+    end
+end
+
+---@param query string
+---@param variables? table optional
+---@param opts? { callback?: function, endpoint?: string }
+function M.query(query, variables, opts)
+    opts = vim.tbl_deep_extend("force", {
+        body = { query = query, variables = variables },
+    }, opts or {})
+
+    return M.curl("post", opts)
+end
+
+function M.normalize_similar_cn(s)
+    return vim.tbl_map(function(sq)
+        return {
+            title = sq.title,
+            translated_title = sq.translatedTitle,
+            paid_only = sq.isPaidOnly,
+            title_slug = sq.titleSlug,
+            difficulty = sq.difficulty,
+        }
+    end, vim.json.decode(s))
+end
+
+---@param problems lc.cache.Question[]
+---@param titles { questionId: integer, title: string }[]
+function M.translate_titles(problems, titles)
+    local map = {}
+    for _, title in ipairs(titles) do
+        map[title.questionId] = title.title
+    end
+
+    return vim.tbl_map(function(p)
+        local title = map[tostring(p.id)]
+        if title then
+            p.title_cn = title
+        end
+        return p
+    end, problems)
+end
+
+return M
diff --git a/lua/leetcode/api/interpreter.lua b/lua/leetcode/api/interpreter.lua
index a90438d7..5606550e 100644
--- a/lua/leetcode/api/interpreter.lua
+++ b/lua/leetcode/api/interpreter.lua
@@ -2,11 +2,10 @@ local log = require("leetcode.logger")
 local urls = require("leetcode.api.urls")
 
 local config = require("leetcode.config")
-local utils = require("leetcode.api.utils")
 
 local t = require("leetcode.translator")
 
----@class lc.Interpreter
+---@class leet.api.interpreter
 local interpreter = {}
 
 ---@param item lc.interpreter_response
@@ -98,11 +97,11 @@ end
 ---@return lc.Interpreter.Response
 function interpreter.check(id, cb)
     local url = urls.check:format(id)
-    utils.get(url, { callback = cb })
+    Leet.api.get(url, { callback = cb })
 end
 
 function interpreter.fetch(url, opts)
-    utils.post(url, {
+    Leet.api.post(url, {
         body = opts.body,
         callback = opts.callback,
     })
diff --git a/lua/leetcode/api/problems.lua b/lua/leetcode/api/problems.lua
index 75a1969a..c959a631 100644
--- a/lua/leetcode/api/problems.lua
+++ b/lua/leetcode/api/problems.lua
@@ -1,17 +1,16 @@
-local utils = require("leetcode.api.utils")
 local queries = require("leetcode.api.queries")
 local config = require("leetcode.config")
 local urls = require("leetcode.api.urls")
 local Spinner = require("leetcode.logger.spinner")
 
----@class lc.ProblemsApi
-local Problems = {}
+---@class leet.api.problems
+local M = {}
 
 ---@param cb? function
 ---@param noti? boolean
 ---
 ---@return lc.cache.Question[], lc.err
-function Problems.all(cb, noti)
+function M.all(cb, noti)
     local endpoint = urls.problems:format("algorithms")
 
     local spinner
@@ -20,7 +19,7 @@ function Problems.all(cb, noti)
     end
 
     if cb then
-        utils.get(endpoint, {
+        Leet.api.get(endpoint, {
             callback = function(res, err)
                 if err then
                     if spinner then
@@ -29,13 +28,13 @@ function Problems.all(cb, noti)
                     return cb(nil, err)
                 end
 
-                local problems = utils.normalize_problems(res.stat_status_pairs)
+                local problems = Leet.api.problems.normalize(res.stat_status_pairs)
 
                 if config.is_cn then
                     if spinner then
                         spinner:update("fetching title translations")
                     end
-                    Problems.translated_titles(function(titles, terr)
+                    M.translated_titles(function(titles, terr)
                         if terr then
                             if spinner then
                                 spinner:error(terr.msg)
@@ -43,7 +42,7 @@ function Problems.all(cb, noti)
                             return cb(nil, terr)
                         end
 
-                        problems = utils.translate_titles(problems, titles)
+                        problems = Leet.api.translate_titles(problems, titles)
                         if spinner then
                             spinner:success("cache updated")
                         end
@@ -60,7 +59,7 @@ function Problems.all(cb, noti)
             end,
         })
     else
-        local res, err = utils.get(endpoint)
+        local res, err = Leet.api.get(endpoint)
         if err then
             if spinner then
                 spinner:error(err.msg)
@@ -68,10 +67,10 @@ function Problems.all(cb, noti)
             return nil, err
         end
 
-        local problems = utils.normalize_problems(res.stat_status_pairs)
+        local problems = Leet.api.problems.normalize(res.stat_status_pairs)
 
         if config.is_cn then
-            local titles, terr = Problems.translated_titles()
+            local titles, terr = M.translated_titles()
             if terr then
                 if spinner then
                     spinner:error(terr.msg)
@@ -82,7 +81,7 @@ function Problems.all(cb, noti)
             if spinner then
                 spinner:success("problems cache updated")
             end
-            return utils.translate_titles(problems, titles)
+            return Leet.api.translate_titles(problems, titles)
         else
             if spinner then
                 spinner:success("problems cache updated")
@@ -92,10 +91,10 @@ function Problems.all(cb, noti)
     end
 end
 
-function Problems.question_of_today(cb)
+function M.question_of_today(cb)
     local query = queries.qot
 
-    utils.query(query, {}, {
+    Leet.api.query(query, {}, {
         callback = function(res, err)
             if err then
                 return cb(nil, err)
@@ -108,11 +107,11 @@ function Problems.question_of_today(cb)
     })
 end
 
-function Problems.translated_titles(cb)
+function M.translated_titles(cb)
     local query = queries.translations
 
     if cb then
-        utils.query(query, {}, {
+        Leet.api.query(query, {}, {
             callback = function(res, err)
                 if err then
                     return cb(nil, err)
@@ -129,4 +128,126 @@ function Problems.translated_titles(cb)
     end
 end
 
-return Problems
+---@param filters? table
+function M.random(filters)
+    local variables = {
+        categorySlug = "algorithms",
+        filters = filters or vim.empty_dict(),
+    }
+
+    local query = queries.random_question
+
+    local res, err = Leet.api.query(query, variables)
+    if err then
+        return nil, err
+    end
+
+    local q = res.data.randomQuestion
+
+    if q == vim.NIL then
+        local msg = "Random question fetch responded with `null`"
+
+        if filters then
+            msg = msg .. ".\n\nMaybe invalid filters?\n" .. vim.inspect(filters)
+        end
+
+        return nil, { msg = msg, lvl = vim.log.levels.ERROR }
+    end
+
+    if config.is_cn then
+        q = {
+            title_slug = q,
+            paid_only = Leet.cache.problems.by_slug(q).paid_only,
+        }
+    end
+
+    if not config.auth.is_premium and q.paid_only then
+        err = err or {}
+        err.msg = "Drawn question is for premium users only. Please try again"
+        err.lvl = vim.log.levels.WARN
+        return nil, err
+    end
+
+    return q
+end
+
+---@param qid integer
+---@param lang lc.lang
+---@param cb function
+function M.latest_submission(qid, lang, cb)
+    local url = urls.latest_submission:format(qid, lang)
+    Leet.api.get(url, { callback = cb })
+end
+
+---@param title_slug string
+---
+---@return lc.question_res|nil
+function M.by_title_slug(title_slug)
+    local variables = {
+        titleSlug = title_slug,
+    }
+
+    local query = queries.question
+
+    local res, err = Leet.api.query(query, variables)
+    if not res or err then
+        return log.err(err)
+    end
+
+    local q = res.data.question
+    q.meta_data = vim.json.decode(q.meta_data)
+    q.stats = vim.json.decode(q.stats)
+    if type(q.similar) == "string" then
+        q.similar = Leet.api.normalize_similar_cn(q.similar)
+    end
+
+    return q
+end
+
+---@return lc.cache.Question[]
+function M.normalize(problems)
+    problems = vim.tbl_filter(function(p)
+        return not p.stat.question__hide
+    end, problems)
+
+    local comp = function(a, b)
+        local a_fid = a.stat.frontend_question_id
+        local b_fid = b.stat.frontend_question_id
+
+        local is_num_a = tonumber(a_fid)
+        local is_num_b = tonumber(b_fid)
+
+        if is_num_a and is_num_b then
+            return tonumber(a_fid) < tonumber(b_fid)
+        elseif is_num_a then
+            return true
+        elseif is_num_b then
+            return false
+        else
+            return a_fid < b_fid
+        end
+    end
+    table.sort(problems, comp)
+
+    return vim.tbl_map(function(p)
+        return {
+            status = p.status == vim.NIL and "todo" or p.status, -- api returns nil for todo
+            id = p.stat.question_id,
+            frontend_id = p.stat.frontend_question_id,
+            title = p.stat.question__title,
+            title_cn = "",
+            title_slug = p.stat.question__title_slug,
+            link = ("https://leetcode.%s/problems/%s/"):format(
+                config.domain,
+                p.stat.question__title_slug
+            ),
+            paid_only = p.paid_only,
+            ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1),
+            difficulty = ({ "Easy", "Medium", "Hard" })(p.difficulty.level),
+            starred = p.is_favor,
+            topic_tags = {},
+        }
+    end, problems)
+end
+
+return M
diff --git a/lua/leetcode/api/queries.lua b/lua/leetcode/api/queries.lua
index b8356208..1bc0cc88 100644
--- a/lua/leetcode/api/queries.lua
+++ b/lua/leetcode/api/queries.lua
@@ -1,182 +1,182 @@
----@class lc.Api.Queries
+---@class leet.api.queries
 local queries = {}
 
 queries.auth = [[
-        query globalData {
-            userStatus {
-                id: userId
-                name: username
-                is_signed_in: isSignedIn
-                is_premium: isPremium
-                is_verified: isVerified
-                session_id: activeSessionId
-            }
-        }
-    ]]
+	query globalData {
+		userStatus {
+			id: userId
+			name: username
+			is_signed_in: isSignedIn
+			is_premium: isPremium
+			is_verified: isVerified
+			session_id: activeSessionId
+		}
+	}
+]]
 
 queries.qot = [[
-        query questionOfToday {
-            todayRecord: activeDailyCodingChallengeQuestion {
-                question {
-                    title_slug: titleSlug
-                }
-            }
-        }
-    ]]
+	query questionOfToday {
+		todayRecord: activeDailyCodingChallengeQuestion {
+			question {
+				title_slug: titleSlug
+			}
+		}
+	}
+]]
 
 queries.question = [[
-        query ($titleSlug: String!) {
-            question(titleSlug: $titleSlug) {
-                id: questionId
-                frontend_id: questionFrontendId
-                title
-                title_slug: titleSlug
-                is_paid_only: isPaidOnly
-                difficulty
-                likes
-                dislikes
-                category_title: categoryTitle
-                content
-                mysql_schemas: mysqlSchemas
-                data_schemas: dataSchemas
-                code_snippets: codeSnippets {
-                    lang
-                    lang_slug: langSlug
-                    code
-                }
-                testcase_list: exampleTestcaseList
-                meta_data: metaData
-                ac_rate: acRate
-                stats
-                hints
-                topic_tags: topicTags {
-                    name
-                    slug
-                }
-                similar: similarQuestionList {
-                    difficulty
-                    title_slug: titleSlug
-                    title
-                    paid_only: isPaidOnly
-                }
-            }
-        }
-    ]]
+	query ($titleSlug: String!) {
+		question(titleSlug: $titleSlug) {
+			id: questionId
+			frontend_id: questionFrontendId
+			title
+			title_slug: titleSlug
+			is_paid_only: isPaidOnly
+			difficulty
+			likes
+			dislikes
+			category_title: categoryTitle
+			content
+			mysql_schemas: mysqlSchemas
+			data_schemas: dataSchemas
+			code_snippets: codeSnippets {
+				lang
+				lang_slug: langSlug
+				code
+			}
+			testcase_list: exampleTestcaseList
+			meta_data: metaData
+			ac_rate: acRate
+			stats
+			hints
+			topic_tags: topicTags {
+				name
+				slug
+			}
+			similar: similarQuestionList {
+				difficulty
+				title_slug: titleSlug
+				title
+				paid_only: isPaidOnly
+			}
+		}
+	}
+]]
 
 queries.random_question = [[
-        query randomQuestion($categorySlug: String, $filters: QuestionListFilterInput) {
-            randomQuestion(categorySlug: $categorySlug, filters: $filters) {
-                title_slug: titleSlug
-                paid_only: isPaidOnly
-            }
-        }
-    ]]
+	query randomQuestion($categorySlug: String, $filters: QuestionListFilterInput) {
+		randomQuestion(categorySlug: $categorySlug, filters: $filters) {
+			title_slug: titleSlug
+			paid_only: isPaidOnly
+		}
+	}
+]]
 
 queries.translations = nil
 
 queries.solved = [[
-        query userCalendar($username: String!) {
-            allQuestionsCount {
-                difficulty
-                count
-            }
-            matchedUser(username: $username) {
-                submit_stats: submitStatsGlobal {
-                    acSubmissionNum {
-                        difficulty
-                        count
-                    }
-                }
-            }
-        }
-    ]]
+	query userCalendar($username: String!) {
+		allQuestionsCount {
+			difficulty
+			count
+		}
+		matchedUser(username: $username) {
+			submit_stats: submitStatsGlobal {
+				acSubmissionNum {
+					difficulty
+					count
+				}
+			}
+		}
+	}
+]]
 
 queries.calendar = [[
-        query userCalendar($username: String!, $year: Int) {
-            matchedUser(username: $username) {
-                calendar: userCalendar(year: $year) {
-                    active_years: activeYears
-                    streak
-                    total_active_days: totalActiveDays
-                    dcc_badges: dccBadges {
-                        timestamp
-                        badge {
-                            name
-                            icon
-                        }
-                    }
-                    submission_calendar: submissionCalendar
-                }
-                solved_beats: problemsSolvedBeatsStats {
-                    difficulty
-                    percentage
-                }
-                submit_stats: submitStatsGlobal {
-                    acSubmissionNum {
-                        difficulty
-                        count
-                    }
-                }
-            }
-        }
-    ]]
+	query userCalendar($username: String!, $year: Int) {
+		matchedUser(username: $username) {
+			calendar: userCalendar(year: $year) {
+				active_years: activeYears
+				streak
+				total_active_days: totalActiveDays
+				dcc_badges: dccBadges {
+					timestamp
+					badge {
+						name
+						icon
+					}
+				}
+				submission_calendar: submissionCalendar
+			}
+			solved_beats: problemsSolvedBeatsStats {
+				difficulty
+				percentage
+			}
+			submit_stats: submitStatsGlobal {
+				acSubmissionNum {
+					difficulty
+					count
+				}
+			}
+		}
+	}
+]]
 
 queries.skills = [[
-        query skillStats($username: String!) {
-            matchedUser(username: $username) {
-                tag_problems_counts: tagProblemCounts {
-                    advanced {
-                        tag: tagName
-                        slug: tagSlug
-                        problems_solved: problemsSolved
-                    }
-                    intermediate {
-                        tag: tagName
-                        slug: tagSlug
-                        problems_solved: problemsSolved
-                    }
-                    fundamental {
-                        tag: tagName
-                        slug: tagSlug
-                        problems_solved: problemsSolved
-                    }
-                }
-            }
-        }
-    ]]
+	query skillStats($username: String!) {
+		matchedUser(username: $username) {
+			tag_problems_counts: tagProblemCounts {
+				advanced {
+					tag: tagName
+					slug: tagSlug
+					problems_solved: problemsSolved
+				}
+				intermediate {
+					tag: tagName
+					slug: tagSlug
+					problems_solved: problemsSolved
+				}
+				fundamental {
+					tag: tagName
+					slug: tagSlug
+					problems_solved: problemsSolved
+				}
+			}
+		}
+	}
+]]
 
 queries.languages = [[
-        query languageStats($username: String!) {
-            matchedUser(username: $username) {
-                languageProblemCount {
-                    lang: languageName
-                    problems_solved: problemsSolved
-                }
-            }
-        }
-    ]]
+	query languageStats($username: String!) {
+		matchedUser(username: $username) {
+			languageProblemCount {
+				lang: languageName
+				problems_solved: problemsSolved
+			}
+		}
+	}
+]]
 
 queries.streak = [[
-        query getStreakCounter {
-            streakCounter {
-                streakCount
-                daysSkipped
-                todayCompleted: currentDayCompleted
-            }
-        }
-    ]]
+	query getStreakCounter {
+		streakCounter {
+			streakCount
+			daysSkipped
+			todayCompleted: currentDayCompleted
+		}
+	}
+]]
 
 queries.session_progress = [[
-        query userSessionProgress($username: String!) {
-            matchedUser(username: $username) {
-                submitStats {
-                    acSubmissionNum {
-                        difficulty
-                        count
-                    }
-                }
-            }
-        }
-    ]]
+	query userSessionProgress($username: String!) {
+		matchedUser(username: $username) {
+			submitStats {
+				acSubmissionNum {
+					difficulty
+					count
+				}
+			}
+		}
+	}
+]]
 
 return queries
diff --git a/lua/leetcode/api/question.lua b/lua/leetcode/api/question.lua
deleted file mode 100644
index f76f5611..00000000
--- a/lua/leetcode/api/question.lua
+++ /dev/null
@@ -1,89 +0,0 @@
-local utils = require("leetcode.api.utils")
-local log = require("leetcode.logger")
-local queries = require("leetcode.api.queries")
-local problemlist = require("leetcode.cache.problemlist")
-local urls = require("leetcode.api.urls")
-
-local question = {}
-
----@class lc.Question.Content
----@field content string
-
----@param title_slug string
----
----@return lc.question_res|nil
-function question.by_title_slug(title_slug)
-    local variables = {
-        titleSlug = title_slug,
-    }
-
-    local query = queries.question
-
-    local res, err = utils.query(query, variables)
-    if not res or err then
-        return log.err(err)
-    end
-
-    local q = res.data.question
-    q.meta_data = select(2, pcall(utils.decode, q.meta_data))
-    q.stats = select(2, pcall(utils.decode, q.stats))
-    if type(q.similar) == "string" then
-        q.similar = utils.normalize_similar_cn(q.similar)
-    end
-
-    return q
-end
-
----@param filters? table
-function question.random(filters)
-    local variables = {
-        categorySlug = "algorithms",
-        filters = filters or vim.empty_dict(),
-    }
-
-    local query = queries.random_question
-
-    local config = require("leetcode.config")
-    local res, err = utils.query(query, variables)
-    if err then
-        return nil, err
-    end
-
-    local q = res.data.randomQuestion
-
-    if q == vim.NIL then
-        local msg = "Random question fetch responded with `null`"
-
-        if filters then
-            msg = msg .. ".\n\nMaybe invalid filters?\n" .. vim.inspect(filters)
-        end
-
-        return nil, { msg = msg, lvl = vim.log.levels.ERROR }
-    end
-
-    if config.is_cn then
-        q = {
-            title_slug = q,
-            paid_only = problemlist.get_by_title_slug(q).paid_only,
-        }
-    end
-
-    if not config.auth.is_premium and q.paid_only then
-        err = err or {}
-        err.msg = "Drawn question is for premium users only. Please try again"
-        err.lvl = vim.log.levels.WARN
-        return nil, err
-    end
-
-    return q
-end
-
----@param qid integer
----@param lang lc.lang
----@param cb function
-function question.latest_submission(qid, lang, cb)
-    local url = urls.latest_submission:format(qid, lang)
-    utils.get(url, { callback = cb })
-end
-
-return question
diff --git a/lua/leetcode/api/statistics.lua b/lua/leetcode/api/stats.lua
similarity index 83%
rename from lua/leetcode/api/statistics.lua
rename to lua/leetcode/api/stats.lua
index 130399ab..1ccf4435 100644
--- a/lua/leetcode/api/statistics.lua
+++ b/lua/leetcode/api/stats.lua
@@ -1,20 +1,19 @@
-local utils = require("leetcode.api.utils")
 local config = require("leetcode.config")
 local log = require("leetcode.logger")
 local urls = require("leetcode.api.urls")
 local queries = require("leetcode.api.queries")
 
----@class lc.api.statistics
-local statistics = {}
+---@class leet.api.stats
+local M = {}
 
-function statistics.calendar(cb)
+function M.calendar(cb)
     local variables = {
         username = config.auth.name,
     }
 
     local query = queries.calendar
 
-    utils.query(query, variables, {
+    Leet.api.query(query, variables, {
         endpoint = urls.calendar,
         callback = function(res, err)
             if err then
@@ -24,8 +23,7 @@ function statistics.calendar(cb)
             local data = res.data
             local calendar = data["matchedUser"]["calendar"]
 
-            calendar.submission_calendar =
-                select(2, pcall(utils.decode, calendar.submission_calendar))
+            calendar.submission_calendar = vim.json.decode(calendar.submission_calendar)
 
             cb({ calendar = calendar }, nil)
         end,
@@ -33,14 +31,14 @@ function statistics.calendar(cb)
 end
 
 ---@param cb fun(res: lc.Stats.QuestionCount[], err: lc.err)
-function statistics.session_progress(cb)
+function M.session_progress(cb)
     local variables = {
         username = config.auth.name,
     }
 
     local query = queries.session_progress
 
-    utils.query(query, variables, {
+    Leet.api.query(query, variables, {
         callback = function(res, err)
             if err then
                 return cb(nil, err)
@@ -54,14 +52,14 @@ function statistics.session_progress(cb)
 end
 
 ---@param cb fun(res: lc.Stats.Res, err: lc.err)
-function statistics.solved(cb)
+function M.solved(cb)
     local variables = {
         username = config.auth.name,
     }
 
     local query = queries.solved
 
-    utils.query(query, variables, {
+    Leet.api.query(query, variables, {
         endpoint = urls.solved,
         callback = function(res, err)
             if err then
@@ -81,14 +79,14 @@ function statistics.solved(cb)
 end
 
 ---@param cb fun(res: lc.Skills.Res, err: lc.err)
-function statistics.skills(cb)
+function M.skills(cb)
     local variables = {
         username = config.auth.name,
     }
 
     local query = queries.skills
 
-    utils.query(query, variables, {
+    Leet.api.query(query, variables, {
         endpoint = urls.skills,
         callback = function(res, err)
             if err then
@@ -102,14 +100,14 @@ function statistics.skills(cb)
 end
 
 ---@param cb fun(res: lc.Languages.Res, err: lc.err)
-function statistics.languages(cb)
+function M.languages(cb)
     local variables = {
         username = config.auth.name,
     }
 
     local query = queries.languages
 
-    utils.query(query, variables, {
+    Leet.api.query(query, variables, {
         endpoint = urls.languages,
         callback = function(res, err)
             if err then
@@ -122,12 +120,12 @@ function statistics.languages(cb)
     })
 end
 
-function statistics.streak(cb)
+function M.streak(cb)
     local variables = vim.empty_dict()
 
     local query = queries.streak
 
-    utils.query(query, variables, {
+    Leet.api.query(query, variables, {
         endpoint = urls.streak_counter,
         callback = function(res, err)
             if err then
@@ -148,10 +146,10 @@ function statistics.streak(cb)
 end
 
 ---@param cb fun(res: lc.res.session[], err: lc.err)
-function statistics.sessions(cb)
+function M.sessions(cb)
     local url = urls.session
 
-    utils.post(url, {
+    Leet.api.post(url, {
         body = vim.empty_dict(),
         callback = function(res, err)
             if err then
@@ -163,7 +161,7 @@ function statistics.sessions(cb)
     })
 end
 
-function statistics.change_session(id, cb)
+function M.change_session(id, cb)
     local body = {
         func = "activate",
         target = id,
@@ -171,7 +169,7 @@ function statistics.change_session(id, cb)
 
     local url = urls.session
 
-    utils.put(url, {
+    Leet.api.put(url, {
         body = body,
         callback = function(res, err)
             if err then
@@ -183,7 +181,7 @@ function statistics.change_session(id, cb)
     })
 end
 
-function statistics.create_session(name, cb)
+function M.create_session(name, cb)
     local body = {
         func = "create",
         name = name,
@@ -191,7 +189,7 @@ function statistics.create_session(name, cb)
 
     local url = urls.session
 
-    utils.put(url, {
+    Leet.api.put(url, {
         body = body,
         callback = function(res, err)
             if err then
@@ -203,4 +201,4 @@ function statistics.create_session(name, cb)
     })
 end
 
-return statistics
+return M
diff --git a/lua/leetcode/api/urls.lua b/lua/leetcode/api/urls.lua
index ea7de617..76a74465 100644
--- a/lua/leetcode/api/urls.lua
+++ b/lua/leetcode/api/urls.lua
@@ -1,4 +1,4 @@
----@class lc.api.urls
+---@class leet.api.urls
 local urls = {}
 
 urls.base = "/graphql/"
diff --git a/lua/leetcode/api/utils.lua b/lua/leetcode/api/utils.lua
deleted file mode 100644
index 74c9d5b1..00000000
--- a/lua/leetcode/api/utils.lua
+++ /dev/null
@@ -1,251 +0,0 @@
-local curl = require("plenary.curl")
-local log = require("leetcode.logger")
-local config = require("leetcode.config")
-local headers = require("leetcode.api.headers")
-local urls = require("leetcode.api.urls")
-
----@class lc.Api.Utils
-local utils = {}
-
----@param endpoint string
-function utils.put(endpoint, opts)
-    local options = vim.tbl_deep_extend("force", {
-        endpoint = endpoint,
-    }, opts or {})
-
-    return utils.curl("put", options)
-end
-
----@param endpoint string
-function utils.post(endpoint, opts)
-    local options = vim.tbl_deep_extend("force", {
-        endpoint = endpoint,
-    }, opts or {})
-
-    return utils.curl("post", options)
-end
-
----@param endpoint string
----@param opts? table
-function utils.get(endpoint, opts)
-    local options = vim.tbl_deep_extend("force", {
-        endpoint = endpoint,
-    }, opts or {})
-
-    return utils.curl("get", options)
-end
-
----@param query string
----@param variables? table optional
----@param opts? { callback?: function, endpoint?: string }
-function utils.query(query, variables, opts)
-    opts = vim.tbl_deep_extend("force", {
-        body = {
-            query = query,
-            variables = variables,
-        },
-    }, opts or {})
-
-    return utils.curl("post", opts)
-end
-
----@private
----@param method string
----@param params table
-function utils.curl(method, params)
-    local params_cpy = vim.deepcopy(params)
-
-    params = vim.tbl_deep_extend("force", {
-        headers = headers.get(),
-        compressed = false,
-        retry = 5,
-        endpoint = urls.base,
-    }, params or {})
-    local url = ("https://leetcode.%s%s"):format(config.domain, params.endpoint)
-
-    if type(params.body) == "table" then
-        params.body = vim.json.encode(params.body)
-    end
-
-    local tries = params.retry
-    local function should_retry(err)
-        return err and err.status >= 500 and tries > 0
-    end
-
-    if params.callback then
-        local cb = vim.schedule_wrap(params.callback)
-        params.callback = function(out, _)
-            local res, err = utils.handle_res(out)
-
-            if should_retry(err) then
-                log.debug("retry " .. tries)
-                params_cpy.retry = tries - 1
-                utils.curl(method, params_cpy)
-            else
-                cb(res, err)
-            end
-        end
-
-        curl[method](url, params)
-    else
-        local out = curl[method](url, params)
-        local res, err = utils.handle_res(out)
-
-        if should_retry(err) then
-            log.debug("retry " .. tries)
-            params_cpy.retry = tries - 1
-            utils.curl(method, params_cpy)
-        else
-            return res, err
-        end
-    end
-end
-
----@private
----@return table, lc.err
-function utils.handle_res(out)
-    local res, err
-    log.debug(out)
-
-    if out.exit ~= 0 then
-        err = {
-            code = out.exit,
-            msg = "curl failed",
-        }
-    elseif out.status >= 300 then
-        local ok, msg = pcall(function()
-            local dec = utils.decode(out.body)
-
-            if dec.error then
-                return dec.error
-            end
-
-            local tbl = {}
-            for _, e in ipairs(dec.errors) do
-                table.insert(tbl, e.message)
-            end
-
-            return table.concat(tbl, "\n")
-        end)
-
-        res = out.body
-        err = {
-            code = 0,
-            status = out.status,
-            response = out,
-            msg = "http error " .. out.status .. (ok and ("\n\n" .. msg) or ""),
-            out = out.body,
-        }
-    else
-        res = utils.decode(out.body)
-    end
-
-    return res, utils.check_err(err)
-end
-
----@param err lc.err
-function utils.check_err(err)
-    if not err then
-        return
-    end
-
-    if err.status then
-        if err.status == 401 or err.status == 403 then
-            -- require("leetcode.command").expire()
-            err.msg =
-                "Your cookie may have expired, or LeetCode has temporarily restricted API access"
-        end
-    end
-
-    return err
-end
-
-function utils.decode(str)
-    return vim.json.decode(str)
-    -- local ok, res = pcall(vim.json.decode, str)
-    -- assert(ok, str)
-    -- return res
-end
-
-function utils.normalize_similar_cn(s)
-    s = select(2, pcall(utils.decode, s))
-
-    return vim.tbl_map(function(sq)
-        return {
-            title = sq.title,
-            translated_title = sq.translatedTitle,
-            paid_only = sq.isPaidOnly,
-            title_slug = sq.titleSlug,
-            difficulty = sq.difficulty,
-        }
-    end, s)
-end
-
-function utils.lvl_to_name(lvl)
-    return ({ "Easy", "Medium", "Hard" })[lvl]
-end
-
----@return lc.cache.Question[]
-function utils.normalize_problems(problems)
-    problems = vim.tbl_filter(function(p)
-        return not p.stat.question__hide
-    end, problems)
-
-    local comp = function(a, b)
-        local a_fid = a.stat.frontend_question_id
-        local b_fid = b.stat.frontend_question_id
-
-        local is_num_a = tonumber(a_fid)
-        local is_num_b = tonumber(b_fid)
-
-        if is_num_a and is_num_b then
-            return tonumber(a_fid) < tonumber(b_fid)
-        elseif is_num_a then
-            return true
-        elseif is_num_b then
-            return false
-        else
-            return a_fid < b_fid
-        end
-    end
-    table.sort(problems, comp)
-
-    return vim.tbl_map(function(p)
-        return {
-            status = p.status == vim.NIL and "todo" or p.status, -- api returns nil for todo
-            id = p.stat.question_id,
-            frontend_id = p.stat.frontend_question_id,
-            title = p.stat.question__title,
-            title_cn = "",
-            title_slug = p.stat.question__title_slug,
-            link = ("https://leetcode.%s/problems/%s/"):format(
-                config.domain,
-                p.stat.question__title_slug
-            ),
-            paid_only = p.paid_only,
-            ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1),
-            difficulty = utils.lvl_to_name(p.difficulty.level),
-            starred = p.is_favor,
-            topic_tags = {},
-        }
-    end, problems)
-end
-
----@param problems lc.cache.Question[]
----@param titles { questionId: integer, title: string }[]
-function utils.translate_titles(problems, titles)
-    local map = {}
-    for _, title in ipairs(titles) do
-        map[title.questionId] = title.title
-    end
-
-    return vim.tbl_map(function(p)
-        local title = map[tostring(p.id)]
-        if title then
-            p.title_cn = title
-        end
-        return p
-    end, problems)
-end
-
-return utils
diff --git a/lua/leetcode/cache/cookie.lua b/lua/leetcode/cache/cookie.lua
index 4962dfaa..97838526 100644
--- a/lua/leetcode/cache/cookie.lua
+++ b/lua/leetcode/cache/cookie.lua
@@ -1,5 +1,4 @@
 local path = require("plenary.path")
-local log = require("leetcode.logger")
 
 local config = require("leetcode.config")
 ---@type Path
@@ -12,14 +11,13 @@ local hist = {}
 ---@field leetcode_session string
 ---@field str string
 
----@class lc.Cookie
-local Cookie = {}
+---@class leet.cache.cookie
+local M = {}
 
 ---@param str string
----
 ---@return string|nil
-function Cookie.set(str)
-    local _, cerr = Cookie.parse(str)
+function M.set(str)
+    local _, cerr = M.parse(str)
     if cerr then
         return cerr
     end
@@ -34,7 +32,7 @@ function Cookie.set(str)
 end
 
 ---@return boolean
-function Cookie.delete()
+function M.delete()
     if not file:exists() then
         return false
     end
@@ -42,7 +40,7 @@ function Cookie.delete()
 end
 
 ---@return string|nil
-function Cookie.read()
+function M.read()
     local contents = file:read()
 
     if not contents or type(contents) ~= "string" then
@@ -53,7 +51,7 @@ function Cookie.read()
 end
 
 ---@return lc.cache.Cookie | nil
-function Cookie.get()
+function M.get()
     if not file:exists() then
         return
     end
@@ -66,13 +64,13 @@ function Cookie.get()
         return hcookie
     end
 
-    local contents = Cookie.read()
+    local contents = M.read()
     if not contents then
         require("leetcode.command").delete_cookie()
         return
     end
 
-    local cookie = Cookie.parse(contents)
+    local cookie = M.parse(contents)
     if not cookie then
         require("leetcode.command").delete_cookie()
         return
@@ -85,7 +83,7 @@ end
 ---@param str string
 ---
 ---@return lc.cache.Cookie|nil, string|nil
-function Cookie.parse(str)
+function M.parse(str)
     local csrf = str:match("csrftoken=([^;]+)")
     if not csrf or csrf == "" then
         return nil, "Bad csrf token format"
@@ -99,4 +97,4 @@ function Cookie.parse(str)
     return { csrftoken = csrf, leetcode_session = ls, str = str }
 end
 
-return Cookie
+return M
diff --git a/lua/leetcode/cache/init.lua b/lua/leetcode/cache/init.lua
index b7052a10..1d1c2c40 100644
--- a/lua/leetcode/cache/init.lua
+++ b/lua/leetcode/cache/init.lua
@@ -1,10 +1,17 @@
-local Problemlist = require("leetcode.cache.problemlist")
+local Problemlist = require("leetcode.cache.problems")
 
----@class lc.Cache
-local cache = {}
+---@class leet.cache
+---@field cookie leet.cache.cookie
+---@field problems leet.cache.problems
+local M = setmetatable({}, {
+    __index = function(t, k)
+        t[k] = require("leetcode.cache." .. k)
+        return rawget(t, k)
+    end,
+})
 
-function cache.update()
+function M.update()
     Problemlist.update()
 end
 
-return cache
+return M
diff --git a/lua/leetcode/cache/problemlist.lua b/lua/leetcode/cache/problems.lua
similarity index 74%
rename from lua/leetcode/cache/problemlist.lua
rename to lua/leetcode/cache/problems.lua
index 13cbf0d1..61c784ac 100644
--- a/lua/leetcode/cache/problemlist.lua
+++ b/lua/leetcode/cache/problems.lua
@@ -24,18 +24,18 @@ local hist = nil
 ---@field difficulty "Easy" | "Medium" | "Hard"
 ---@field topic_tags { name:string, slug: string }
 
----@class lc.cache.Problemlist
-local Problemlist = {}
+---@class leet.cache.problems
+local M = {}
 
 ---@return lc.cache.Question[]
-function Problemlist.get()
-    return Problemlist.read().data
+function M.get()
+    return M.read().data
 end
 
 ---@return lc.cache.payload
-function Problemlist.read()
+function M.read()
     if not file:exists() then
-        return Problemlist.populate()
+        return M.populate()
     end
 
     local time = os.time()
@@ -45,25 +45,25 @@ function Problemlist.read()
 
     local contents = file:read()
     if not contents or type(contents) ~= "string" then
-        return Problemlist.populate()
+        return M.populate()
     end
 
-    local cached = Problemlist.parse(contents)
+    local cached = M.parse(contents)
 
     if not cached or (cached.version ~= config.version or cached.username ~= config.auth.name) then
-        return Problemlist.populate()
+        return M.populate()
     end
 
     hist = { at = time, payload = cached }
     if (time - cached.updated_at) > interval then
-        Problemlist.update()
+        M.update()
     end
 
     return cached
 end
 
 ---@return lc.cache.payload
-function Problemlist.populate()
+function M.populate()
     local res, err = problems_api.all(nil, true)
 
     if not res or err then
@@ -71,32 +71,32 @@ function Problemlist.populate()
         error(msg)
     end
 
-    Problemlist.write({ data = res })
+    M.write({ data = res })
     return hist.payload
 end
 
-function Problemlist.update()
+function M.update()
     problems_api.all(function(res, err)
         if not err then
-            Problemlist.write({ data = res })
+            M.write({ data = res })
         end
     end, true)
 end
 
 ---@return lc.cache.Question
-function Problemlist.get_by_title_slug(title_slug)
-    local problems = Problemlist.get()
+function M.by_slug(slug)
+    local problems = M.get()
 
     local problem = vim.tbl_filter(function(e)
-        return e.title_slug == title_slug
+        return e.title_slug == slug
     end, problems)[1]
 
-    assert(problem, ("Problem `%s` not found. Try updating cache?"):format(title_slug))
+    assert(problem, ("Problem `%s` not found. Try updating cache?"):format(slug))
     return problem
 end
 
 ---@param payload? lc.cache.payload
-function Problemlist.write(payload)
+function M.write(payload)
     payload = vim.tbl_deep_extend("force", {
         version = config.version,
         updated_at = os.time(),
@@ -104,7 +104,7 @@ function Problemlist.write(payload)
     }, payload)
 
     if not payload.data then
-        payload.data = Problemlist.get()
+        payload.data = M.get()
     end
 
     file:write(vim.json.encode(payload), "w")
@@ -116,14 +116,14 @@ end
 ---@param str string
 ---
 ---@return lc.cache.payload
-function Problemlist.parse(str)
+function M.parse(str)
     return vim.json.decode(str)
 end
 
 ---@param title_slug string
 ---@param status "ac" | "notac"
-Problemlist.change_status = vim.schedule_wrap(function(title_slug, status)
-    local cached = Problemlist.read()
+M.change_status = vim.schedule_wrap(function(title_slug, status)
+    local cached = M.read()
 
     cached.data = vim.tbl_map(function(p)
         if p.title_slug == title_slug then
@@ -132,14 +132,14 @@ Problemlist.change_status = vim.schedule_wrap(function(title_slug, status)
         return p
     end, cached.data)
 
-    Problemlist.write(cached)
+    M.write(cached)
 end)
 
-function Problemlist.delete()
+function M.delete()
     if not file:exists() then
         return false
     end
     return pcall(path.rm, file)
 end
 
-return Problemlist
+return M
diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua
index b683a3d9..35129036 100644
--- a/lua/leetcode/command/init.lua
+++ b/lua/leetcode/command/init.lua
@@ -16,16 +16,16 @@ function cmd.deprecate(old_name, new_name)
 end
 
 function cmd.cache_update()
-    require("leetcode.utils").auth_guard()
+    require("leetcode.util").auth_guard()
 
     require("leetcode.cache").update()
 end
 
 ---@param options table<string, string[]>
 function cmd.problems(options)
-    require("leetcode.utils").auth_guard()
+    require("leetcode.util").auth_guard()
 
-    local p = require("leetcode.cache.problemlist").get()
+    local p = require("leetcode.cache.problems").get()
     local picker = require("leetcode.picker")
     picker.question(p, options)
 end
@@ -97,7 +97,7 @@ function cmd.delete_cookie()
 end
 
 cmd.q_close_all = function()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     local qs = utils.question_tabs()
 
     for _, tabp in ipairs(qs) do
@@ -127,7 +127,7 @@ cmd.expire = vim.schedule_wrap(function()
 end)
 
 function cmd.qot()
-    require("leetcode.utils").auth_guard()
+    require("leetcode.util").auth_guard()
 
     local problems = require("leetcode.api.problems")
     local Question = require("leetcode-ui.question")
@@ -136,15 +136,15 @@ function cmd.qot()
         if err then
             return log.err(err)
         end
-        local problemlist = require("leetcode.cache.problemlist")
-        Question(problemlist.get_by_title_slug(qot.title_slug)):mount()
+        local problemlist = require("leetcode.cache.problems")
+        Question(problemlist.by_slug(qot.title_slug)):mount()
     end)
 end
 
 function cmd.random_question(opts)
-    require("leetcode.utils").auth_guard()
+    require("leetcode.util").auth_guard()
 
-    local problems = require("leetcode.cache.problemlist")
+    local problems = require("leetcode.cache.problems")
     local question = require("leetcode.api.question")
 
     if opts and opts.difficulty then
@@ -163,7 +163,7 @@ function cmd.random_question(opts)
         return log.err(err)
     end
 
-    local item = problems.get_by_title_slug(q.title_slug) or {}
+    local item = problems.by_slug(q.title_slug) or {}
     local Question = require("leetcode-ui.question")
     Question(item):mount()
 end
@@ -186,7 +186,7 @@ function cmd.menu()
 end
 
 function cmd.yank()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     local q = utils.curr_question()
     if not q then
         return
@@ -211,7 +211,7 @@ function cmd.question_tabs()
 end
 
 function cmd.change_lang()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     local q = utils.curr_question()
     if q then
         local picker = require("leetcode.picker")
@@ -220,7 +220,7 @@ function cmd.change_lang()
 end
 
 function cmd.desc_toggle()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     local q = utils.curr_question()
     if q then
         q.description:toggle()
@@ -228,7 +228,7 @@ function cmd.desc_toggle()
 end
 
 function cmd.desc_toggle_stats()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     local q = utils.curr_question()
     if q then
         q.description:toggle_stats()
@@ -236,7 +236,7 @@ function cmd.desc_toggle_stats()
 end
 
 function cmd.console()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     local q = utils.curr_question()
     if q then
         q.console:toggle()
@@ -244,7 +244,7 @@ function cmd.console()
 end
 
 function cmd.info()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     local q = utils.curr_question()
     if q then
         q.info:toggle()
@@ -257,7 +257,7 @@ function cmd.hints()
 end
 
 function cmd.q_run()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     utils.auth_guard()
     local q = utils.curr_question()
     if q then
@@ -266,7 +266,7 @@ function cmd.q_run()
 end
 
 function cmd.q_submit()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     utils.auth_guard()
     local q = utils.curr_question()
     if q then
@@ -288,7 +288,7 @@ function cmd.ui_languages()
 end
 
 function cmd.open()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     utils.auth_guard()
     local q = utils.curr_question()
 
@@ -314,7 +314,7 @@ function cmd.open()
 end
 
 function cmd.reset()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     utils.auth_guard()
     local q = utils.curr_question()
     if not q then
@@ -325,7 +325,7 @@ function cmd.reset()
 end
 
 function cmd.last_submit()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     utils.auth_guard()
     local q = utils.curr_question()
     if not q then
@@ -355,7 +355,7 @@ function cmd.last_submit()
 end
 
 function cmd.restore()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     local ui = require("leetcode-ui.utils")
     local q = utils.curr_question()
     if not q then
@@ -381,7 +381,7 @@ function cmd.restore()
 end
 
 function cmd.inject()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     local q = utils.curr_question()
     if not q then
         return
@@ -402,7 +402,7 @@ function cmd.inject()
 end
 
 function cmd.fold()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     local q = utils.curr_question()
     if not q then
         return
@@ -431,7 +431,7 @@ function cmd.get_session_by_name(name)
 end
 
 function cmd.change_session(opts)
-    require("leetcode.utils").auth_guard()
+    require("leetcode.util").auth_guard()
 
     local name = opts.name[1] or config.sessions.default
 
@@ -440,7 +440,7 @@ function cmd.change_session(opts)
         return log.error("Session not found")
     end
 
-    local stats_api = require("leetcode.api.statistics")
+    local stats_api = require("leetcode.api.stats")
     stats_api.change_session(session.id, function(_, err)
         if err then
             return log.err(err)
@@ -451,14 +451,14 @@ function cmd.change_session(opts)
 end
 
 function cmd.create_session(opts)
-    require("leetcode.utils").auth_guard()
+    require("leetcode.util").auth_guard()
 
     local name = opts.name[1]
     if not name then
         return log.error("Session name not provided")
     end
 
-    local stats_api = require("leetcode.api.statistics")
+    local stats_api = require("leetcode.api.stats")
     stats_api.create_session(name, function(_, err)
         if err then
             return log.err(err)
@@ -468,14 +468,14 @@ function cmd.create_session(opts)
 end
 
 function cmd.update_sessions()
-    require("leetcode.utils").auth_guard()
+    require("leetcode.util").auth_guard()
 
     config.stats.update_sessions()
 end
 
 function cmd.fix()
     require("leetcode.cache.cookie").delete()
-    require("leetcode.cache.problemlist").delete()
+    require("leetcode.cache.problems").delete()
     vim.cmd("qa!")
 end
 
diff --git a/lua/leetcode/config/init.lua b/lua/leetcode/config/init.lua
index 0dfad2a6..adce46f4 100644
--- a/lua/leetcode/config/init.lua
+++ b/lua/leetcode/config/init.lua
@@ -71,7 +71,7 @@ function config.setup()
 end
 
 function config.validate()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
 
     assert(vim.fn.has("nvim-0.9.0") == 1, "Neovim >= 0.9.0 required")
 
diff --git a/lua/leetcode/config/stats.lua b/lua/leetcode/config/stats.lua
index ba968579..45c3f66d 100644
--- a/lua/leetcode/config/stats.lua
+++ b/lua/leetcode/config/stats.lua
@@ -5,7 +5,7 @@ Stats.daily = {} ---@type { streak?: number, today_completed?: boolean }
 Stats.progress = {} ---@type table<string, lc.Stats.QuestionCount>
 
 function Stats.update_streak()
-    local stats_api = require("leetcode.api.statistics")
+    local stats_api = require("leetcode.api.stats")
     local log = require("leetcode.logger")
 
     stats_api.streak(function(res, err)
@@ -21,7 +21,7 @@ function Stats.update_streak()
 end
 
 function Stats.update_sessions()
-    local stats_api = require("leetcode.api.statistics")
+    local stats_api = require("leetcode.api.stats")
     local log = require("leetcode.logger")
 
     Stats.progress = {}
diff --git a/lua/leetcode/guard.lua b/lua/leetcode/guard.lua
new file mode 100644
index 00000000..d9d48418
--- /dev/null
+++ b/lua/leetcode/guard.lua
@@ -0,0 +1,30 @@
+local M = {}
+
+function M.auth()
+    local auth_api = require("leetcode.api.auth")
+    local _, aerr = auth_api.user()
+
+    if aerr then
+        return aerr.msg
+    end
+
+    return true
+end
+
+-- ---@param problem leet.res.problem
+-- function M.premium(problem)
+--     local config = require("leetcode.config")
+--     if not config.auth.is_premium then
+--         return "You need to be a premium user to access this feature."
+--     end
+-- 	if
+--         not problem --
+--         or (problem.is_paid_only and not config.auth.is_premium)
+-- 	then
+--
+-- 	end
+--
+--     return true
+-- end
+
+return M
diff --git a/lua/leetcode.lua b/lua/leetcode/init.lua
similarity index 87%
rename from lua/leetcode.lua
rename to lua/leetcode/init.lua
index 0afb7224..54293f3b 100644
--- a/lua/leetcode.lua
+++ b/lua/leetcode/init.lua
@@ -1,11 +1,10 @@
 local config = require("leetcode.config")
 
----@class lc.LeetCode
-local leetcode = {}
+---@class leet
+local M = {}
 
 _G.Leet = { ctx = {} }
 
----@private
 local function log_failed_to_init()
     local log = require("leetcode.logger")
     log.warn("Failed to initialize: `neovim` contains listed buffers")
@@ -28,7 +27,7 @@ end
 ---@param on_vimenter boolean
 ---
 ---@return boolean, boolean? (skip, standalone)
-function leetcode.should_skip(on_vimenter)
+function M.should_skip(on_vimenter)
     if on_vimenter then
         if vim.fn.argc(-1) ~= 1 then
             return true
@@ -82,22 +81,27 @@ function leetcode.should_skip(on_vimenter)
     end
 end
 
-function leetcode.setup_cmds()
+function M.setup_cmds()
     require("leetcode.command").setup()
 end
 
 ---@param on_vimenter boolean
-function leetcode.start(on_vimenter)
-    local skip = leetcode.should_skip(on_vimenter)
+function M.start(on_vimenter)
+    local skip = M.should_skip(on_vimenter)
     if skip then
         return false
     end
 
     config.setup()
 
+    _G.Leet.util = require("leetcode.util")
+    _G.Leet.api = require("leetcode.api")
+    _G.Leet.cache = require("leetcode.cache")
+    _G.Leet.guard = require("leetcode.guard")
+
     vim.api.nvim_set_current_dir(config.storage.home:absolute())
 
-    leetcode.setup_cmds()
+    M.setup_cmds()
 
     local theme = require("leetcode.theme")
     theme.setup()
@@ -111,18 +115,18 @@ function leetcode.start(on_vimenter)
     menu.win:show()
     menu:render()
 
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     utils.exec_hooks("enter")
 
     return true
 end
 
-function leetcode.stop()
+function M.stop()
     vim.cmd("qa!")
 end
 
 ---@param cfg? lc.UserConfig
-function leetcode.setup(cfg)
+function M.setup(cfg)
     config.apply(cfg or {})
 
     vim.api.nvim_create_user_command("Leet", require("leetcode.command").start_with_cmd, {
@@ -137,9 +141,9 @@ function leetcode.setup(cfg)
         pattern = "*",
         nested = true,
         callback = function()
-            leetcode.start(true)
+            M.start(true)
         end,
     })
 end
 
-return leetcode
+return M
diff --git a/lua/leetcode/picker/init.lua b/lua/leetcode/picker/init.lua
index 65547ba9..f9d873dd 100644
--- a/lua/leetcode/picker/init.lua
+++ b/lua/leetcode/picker/init.lua
@@ -115,7 +115,7 @@ function P.question(...)
 end
 
 function P.tabs()
-    local utils = require("leetcode.utils")
+    local utils = require("leetcode.util")
     local tabs = utils.question_tabs()
 
     if vim.tbl_isempty(tabs) then
diff --git a/lua/leetcode/picker/language/init.lua b/lua/leetcode/picker/language/init.lua
index d7b7615b..ab7e3f9f 100644
--- a/lua/leetcode/picker/language/init.lua
+++ b/lua/leetcode/picker/language/init.lua
@@ -1,7 +1,7 @@
 local log = require("leetcode.logger")
 local t = require("leetcode.translator")
 local config = require("leetcode.config")
-local utils = require("leetcode.utils")
+local utils = require("leetcode.util")
 
 local L = {}
 
diff --git a/lua/leetcode/picker/question/fzf.lua b/lua/leetcode/picker/question/fzf.lua
index 3b23e5b4..d0b14e14 100644
--- a/lua/leetcode/picker/question/fzf.lua
+++ b/lua/leetcode/picker/question/fzf.lua
@@ -1,5 +1,5 @@
 local fzf = require("fzf-lua")
-local problemlist = require("leetcode.cache.problemlist")
+local problemlist = require("leetcode.cache.problems")
 local t = require("leetcode.translator")
 local question_picker = require("leetcode.picker.question")
 local Picker = require("leetcode.picker")
@@ -28,7 +28,7 @@ return function(questions, opts)
         actions = {
             ["default"] = function(selected)
                 local slug = Picker.hidden_field(selected[1], deli)
-                local question = problemlist.get_by_title_slug(slug)
+                local question = problemlist.by_slug(slug)
                 question_picker.select(question)
             end,
         },
diff --git a/lua/leetcode/picker/question/init.lua b/lua/leetcode/picker/question/init.lua
index 6967b583..aa24400b 100644
--- a/lua/leetcode/picker/question/init.lua
+++ b/lua/leetcode/picker/question/init.lua
@@ -1,8 +1,8 @@
 local config = require("leetcode.config")
 local log = require("leetcode.logger")
 local ui_utils = require("leetcode-ui.utils")
-local utils = require("leetcode.utils")
-local Question = require("leetcode-ui.question")
+local utils = require("leetcode.util")
+local Problem = require("leetcode.problem")
 local Picker = require("leetcode.picker")
 
 ---@class leet.Picker.Question: leet.Picker
@@ -96,7 +96,7 @@ function P.select(selection, close)
     if close then
         close()
     end
-    Question(selection):mount()
+    Problem(selection):show()
 end
 
 return P
diff --git a/lua/leetcode/picker/tabs/init.lua b/lua/leetcode/picker/tabs/init.lua
index 8ea70497..02406df9 100644
--- a/lua/leetcode/picker/tabs/init.lua
+++ b/lua/leetcode/picker/tabs/init.lua
@@ -1,5 +1,5 @@
 local log = require("leetcode.logger")
-local utils = require("leetcode.utils")
+local utils = require("leetcode.util")
 local ui_utils = require("leetcode-ui.utils")
 local t = require("leetcode.translator")
 
diff --git a/lua/leetcode/problem/console/init.lua b/lua/leetcode/problem/console/init.lua
new file mode 100644
index 00000000..196cb9ab
--- /dev/null
+++ b/lua/leetcode/problem/console/init.lua
@@ -0,0 +1,37 @@
+local Result = require("leetcode.problem.console.result")
+local Testcase = require("leetcode.problem.console.testcase")
+
+---@class leet.problem.console
+local M = Markup.object()
+
+---@param problem leet.problem
+function M:new(problem)
+    self.question = problem
+
+    self.result = Result()
+    self.testcase = Testcase()
+
+    self.layout = Markup.layout({
+        wins = {
+            ["result"] = self.result.win,
+            ["testcase"] = self.testcase.win,
+        },
+        layout = {
+            box = "horizontal",
+            focusable = true,
+            position = "bottom",
+            height = 0.3,
+            { win = "testcase", width = 0.4 },
+            { win = "result" },
+        },
+    })
+    -- self.renderer = Markup.renderer({
+    --     relative = "editor",
+    --     position = "bottom",
+    --     height = 0.3,
+    --     enter = false,
+    --     focusable = true,
+    -- })
+end
+
+return M
diff --git a/lua/leetcode/problem/console/result.lua b/lua/leetcode/problem/console/result.lua
new file mode 100644
index 00000000..c6c2ca94
--- /dev/null
+++ b/lua/leetcode/problem/console/result.lua
@@ -0,0 +1,13 @@
+---@class leet.problem.console.result
+local M = Markup.object()
+
+function M:new()
+    self.win = Markup.window({
+        box = "horizontal",
+        title = "Results",
+        border = "rounded",
+        show = false,
+    })
+end
+
+return M
diff --git a/lua/leetcode/problem/console/testcase.lua b/lua/leetcode/problem/console/testcase.lua
new file mode 100644
index 00000000..c207f73a
--- /dev/null
+++ b/lua/leetcode/problem/console/testcase.lua
@@ -0,0 +1,13 @@
+---@class leet.problem.console.testcase
+local M = Markup.object()
+
+function M:new()
+    self.win = Markup.window({
+        box = "horizontal",
+        border = "rounded",
+        title = "Testcase",
+        show = false,
+    })
+end
+
+return M
diff --git a/lua/leetcode/problem/description.lua b/lua/leetcode/problem/description.lua
new file mode 100644
index 00000000..ba7ecd87
--- /dev/null
+++ b/lua/leetcode/problem/description.lua
@@ -0,0 +1,16 @@
+---@param leet.problem.description
+local M = Markup.object()
+
+---@param problem leet.problem
+function M:new(problem)
+    self.problem = problem
+    self.renderer = Markup.renderer({
+        relative = "win",
+        position = "left",
+        width = 0.4,
+        enter = false,
+        focusable = true,
+    })
+end
+
+return M
diff --git a/lua/leetcode/problem/init.lua b/lua/leetcode/problem/init.lua
new file mode 100644
index 00000000..d87f8c31
--- /dev/null
+++ b/lua/leetcode/problem/init.lua
@@ -0,0 +1,204 @@
+---@module 'markup'
+
+local config = require("leetcode.config")
+local Description = require("leetcode.problem.description")
+local Console = require("leetcode.problem.console")
+
+---@class leet.problem
+---@field cached leet.cache.problem
+local M = Markup.object()
+
+---@param problem leet.cache.problem
+function M:new(problem)
+    self.lang = config.lang
+    self.cached = problem
+end
+
+function M:show()
+    local tabp = Leet.util.detect_duplicate_question(self.cached.title_slug, config.lang)
+    if tabp then
+        return pcall(vim.api.nvim_set_current_tabpage, tabp)
+    end
+
+    local problem = Leet.api.problems.by_title_slug(self.cached.title_slug)
+    if
+        not problem --
+        or (problem.is_paid_only and not config.auth.is_premium)
+    then
+        return Markup.log.warn("Question is for premium users only")
+    end
+
+    self.problem = problem
+
+    vim.cmd.tabe()
+    local path = self:path()
+    vim.cmd("$tabe " .. path)
+
+    self.desc = Description(self)
+    self.console = Console(self)
+
+    -- self.win = Markup.window({
+    --     position = "current",
+    --     hijack = true,
+    --     -- fixed = true,
+    --     -- file = path,
+    --     minimal = false,
+    -- })
+    -- if self:snippet() then
+    --     self:handle_mount()
+    -- else
+    --     local msg = ("Snippet for `%s` not found. Select a different language"):format(self.lang)
+    --     Markup.log.warn(msg)
+    --
+    --     require("leetcode.pickers.language").pick_lang(self, function(snippet)
+    --         self.lang = snippet.t.slug
+    --         self:handle_mount()
+    --     end)
+    -- end
+end
+
+---@return string path, boolean existed
+function M:path()
+    local lang = Leet.util.get_lang(self.lang)
+    local alt = lang.alt and ("." .. lang.alt) or ""
+
+    local fn = ("%s.%s%s.%s"):format(
+        self.problem.frontend_id,
+        self.problem.title_slug,
+        alt,
+        lang.ft
+    )
+    self.file = config.storage.home:joinpath(fn)
+    local existed = self.file:exists()
+
+    if not existed then
+        self.file:write(self:snippet(), "w")
+    end
+
+    return self.file:absolute(), existed
+end
+
+---@param raw? boolean
+function M:snippet(raw)
+    local snippets = self.problem.code_snippets ~= vim.NIL and self.problem.code_snippets or {}
+    local snip = vim.tbl_filter(function(snip)
+        return snip.lang_slug == self.lang
+    end, snippets)[1]
+    if not snip then
+        return
+    end
+
+    local code = snip.code:gsub("\r\n", "\n")
+    return raw and code or self:injector(code)
+end
+
+---@param code string
+function M:injector(code)
+    local inject = config.user.injector[self.lang] or {}
+
+    local parts = { self:editor_section(code, "code") }
+
+    local before = self:inject(true)
+    if before then
+        table.insert(parts, 1, before)
+    end
+
+    local imports = self:inject_imports()
+    if imports then
+        table.insert(parts, 1, self:editor_section(table.concat(imports, "\n"), "imports"))
+    end
+
+    local after = self:inject(false)
+    if after then
+        table.insert(parts, after)
+    end
+
+    local gap = (inject.gap or 1) + 1
+    return table.concat(parts, ("\n"):rep(gap))
+end
+
+---@param before boolean
+---@return string?
+function M:inject(before)
+    local inject = config.user.injector[self.lang] or {}
+    local inj = before and (inject.before or {}) or (inject.after or {})
+
+    local res
+    if type(inj) == "table" then
+        res = table.concat(inj, "\n")
+    elseif type(inj) == "string" then
+        res = inj
+    end
+
+    if res and res ~= "" then
+        return res
+    else
+        return nil
+    end
+end
+
+---@return string[]?
+function M:inject_imports()
+    local inject = config.user.injector[self.lang] or {}
+
+    local imports = inject.imports
+    local default_imports = config.imports[self.lang]
+
+    local function valid_imports(tbl)
+        if vim.tbl_isempty(tbl or {}) then
+            return false
+        end
+        if not vim.islist(tbl) then
+            Markup.log.error("Invalid imports format for language: " .. self.lang)
+            return false
+        end
+        return true
+    end
+
+    if not imports then
+        return default_imports
+    end
+
+    if type(imports) == "function" then
+        local overriden = imports(vim.deepcopy(default_imports or {}))
+        if not valid_imports(overriden) then
+            return default_imports
+        end
+        return overriden
+    end
+
+    local resolved = type(imports) == "string" and { imports } or imports
+    if not valid_imports(resolved) then
+        return
+    end
+
+    local merged, seen = {}, {}
+    local function add(list)
+        for _, v in ipairs(list) do
+            if not seen[v] then
+                table.insert(merged, v)
+                seen[v] = true
+            end
+        end
+    end
+
+    add(default_imports)
+    add(resolved)
+
+    return not vim.tbl_isempty(merged) and merged or nil
+end
+
+---@param lines string|string[]
+---@param name lc.editor.section
+---@return string
+function M:editor_section(lines, name)
+    local comment = Leet.util.get_lang(self.lang).comment
+
+    local start_tag = comment .. " " .. Leet.util.section_tag(name, true)
+    local end_tag = comment .. " " .. Leet.util.section_tag(name, false)
+
+    local str = type(lines) ~= "string" and table.concat(lines, "\n") or lines
+    return table.concat({ start_tag, str, end_tag }, "\n")
+end
+
+return M
diff --git a/lua/leetcode/runner/init.lua b/lua/leetcode/problem/runner.lua
similarity index 96%
rename from lua/leetcode/runner/init.lua
rename to lua/leetcode/problem/runner.lua
index 69fd5438..4e1dbc70 100644
--- a/lua/leetcode/runner/init.lua
+++ b/lua/leetcode/problem/runner.lua
@@ -9,14 +9,14 @@ local Judge = require("leetcode.logger.spinner.judge")
 local leetbody = config.storage.cache:joinpath("body")
 leetbody:touch()
 
----@class lc.Runner
+---@class leet.problem.runner
 ---@field question lc.ui.Question
 local Runner = {}
 Runner.__index = Runner
 
 Runner.running = false
 
----@param self lc.Runner
+---@param self leet.problem.runner
 ---@param submit boolean
 Runner.run = vim.schedule_wrap(function(self, submit)
     if Runner.running then
diff --git a/lua/leetcode/runner/utils.lua b/lua/leetcode/runner/utils.lua
deleted file mode 100644
index 262924f6..00000000
--- a/lua/leetcode/runner/utils.lua
+++ /dev/null
@@ -1,8 +0,0 @@
-local utils = {}
-
----@param lines string[]
-function utils.to_typed_code(lines)
-    return table.concat(lines, "\n")
-end
-
-return utils
diff --git a/lua/leetcode/ui/menu/button/back.lua b/lua/leetcode/ui/menu/button/back.lua
index 0ed261ec..76c6916e 100644
--- a/lua/leetcode/ui/menu/button/back.lua
+++ b/lua/leetcode/ui/menu/button/back.lua
@@ -1,13 +1,13 @@
 local Button = require("leetcode.ui.menu.button")
-local markup = require("markup")
+local m = require("markup")
 
-return markup.Component(function(props)
+return m.component(function(props)
     local page = props.page
 
     return Button({
         icon = "",
         title = "Back",
-        key = "q",
+        lhs = "q",
         page = page,
     })
 end)
diff --git a/lua/leetcode/ui/menu/button/init.lua b/lua/leetcode/ui/menu/button/init.lua
index 441c906c..ddeb8481 100644
--- a/lua/leetcode/ui/menu/button/init.lua
+++ b/lua/leetcode/ui/menu/button/init.lua
@@ -3,7 +3,7 @@ local m = require("markup")
 ---@class MenuButtonProps
 ---@field title string
 ---@field icon string
----@field key string
+---@field lhs string
 ---@field page string
 ---@field nested boolean
 ---@field on_submit function
@@ -22,7 +22,7 @@ return m.component(function(p)
     end
 
     m.hooks.effect(function()
-        return win:map("n", p.key, change_page, {
+        return win:map("n", p.lhs, change_page, {
             noremap = false,
             silent = true,
             nowait = true,
@@ -38,6 +38,6 @@ return m.component(function(p)
             m.inline(p.title, "Tag"),
             p.nested and m.inline("", "leetcode_menu_button_nested"),
         }),
-        m.block(p.key, "Type"),
+        m.block(p.lhs, "Type"),
     })
 end)
diff --git a/lua/leetcode/ui/menu/ctx.lua b/lua/leetcode/ui/menu/ctx.lua
new file mode 100644
index 00000000..bd915f2b
--- /dev/null
+++ b/lua/leetcode/ui/menu/ctx.lua
@@ -0,0 +1,7 @@
+local m = require("markup")
+
+local menu = m.hooks.context()
+local auth = m.hooks.context()
+
+Leet.ctx.menu = menu
+Leet.ctx.auth = auth
diff --git a/lua/leetcode/ui/menu/footer.lua b/lua/leetcode/ui/menu/footer.lua
index 41d924f7..28e9a26e 100644
--- a/lua/leetcode/ui/menu/footer.lua
+++ b/lua/leetcode/ui/menu/footer.lua
@@ -1,22 +1,35 @@
 local m = require("markup")
 local config = require("leetcode.config")
-local api = require("leetcode.api.statistics")
+local api = require("leetcode.api.stats")
 
 return m.component(function()
     local streak, set_streak = m.hooks.variable({})
     local win = m.hooks.window()
     local progress, set_progress = m.hooks.variable({})
+    local store = m.hooks.use(Leet.auth)
 
     m.hooks.effect(function()
-        api.session_progress(set_progress)
-    end, {})
+        if store.auth:isempty() then
+            return
+        end
 
-    m.hooks.effect(function()
-        api.streak(function(data)
+        api.session_progress(function(data, err)
+            if err then
+                Markup.log.error(err)
+                return
+            end
+            set_progress(data)
+        end)
+
+        api.streak(function(data, err)
+            if err then
+                Markup.log.error(err)
+                return
+            end
             set_streak(data)
         end)
 
-        win:map("n", "9", function()
+        return win:map("n", "9", function()
             set_streak(function(v)
                 return {
                     streakCount = (v.streakCount or 0) + 1,
@@ -24,7 +37,7 @@ return m.component(function()
                 }
             end)
         end)
-    end, {})
+    end, { store.auth })
 
     local function stats()
         local res = {}
diff --git a/lua/leetcode/ui/menu/header.lua b/lua/leetcode/ui/menu/header.lua
index 3b9a66b0..51f5c916 100644
--- a/lua/leetcode/ui/menu/header.lua
+++ b/lua/leetcode/ui/menu/header.lua
@@ -1,5 +1,5 @@
 local m = require("markup")
-local api = require("leetcode.api.statistics")
+local api = require("leetcode.api.stats")
 
 local ascii = vim.tbl_map(m.block, {
     [[ /$$                          /$$     /$$$$$$                /$$         ]],
@@ -40,10 +40,20 @@ local function get_days_in_month(month, year)
 end
 
 return m.component(function()
+    local store = m.hooks.use(Leet.auth)
     local calendar, set_calendar = m.hooks.variable({})
 
     m.hooks.effect(function()
-        api.calendar(function(data)
+        if store.auth:isempty() then
+            return
+        end
+
+        api.calendar(function(data, err)
+            if err then
+                Markup.log.error(err)
+                return
+            end
+
             local time = os.date("*t")
             local curr = os.time(vim.tbl_extend("force", time, {
                 year = time.year - 1,
@@ -129,10 +139,13 @@ return m.component(function()
                 ordered_keys = ordered_keys, -- ✅ Pass it along
             })
         end)
-    end, {})
+    end, { store.auth })
 
     if vim.tbl_isempty(calendar or {}) then
-        return "loading..."
+        return m.block({
+            margin_top = 3,
+            ascii,
+        }, "Tag")
     end
 
     -- Example: render grouped structure
diff --git a/lua/leetcode/ui/menu/init.lua b/lua/leetcode/ui/menu/init.lua
index 5d086d3f..5e5723db 100644
--- a/lua/leetcode/ui/menu/init.lua
+++ b/lua/leetcode/ui/menu/init.lua
@@ -8,12 +8,30 @@ local config = require("leetcode.config")
 local menu_ctx = m.hooks.context()
 Leet.ctx.menu = menu_ctx
 
+Leet.auth = m.hooks.store(function(set)
+    return {
+        auth = Markup.list(),
+        set_auth = function(new_auth)
+            set(function(state)
+                state.auth = new_auth
+            end)
+        end,
+        -- c = function(self, new_auth)
+        --     self:set(function(state)
+        --         state.auth = new_auth
+        --     end)
+        -- end,
+    }
+end)
+
 local renderer = Markup.renderer({
     position = "current",
     hijack = true,
     show = false,
 })
+
 Leet.menu = renderer
+Leet.auth:bind(renderer)
 
 local App = m.component(function()
     local menu = m.hooks.use(Leet.ctx.menu)
@@ -38,9 +56,9 @@ local App = m.component(function()
 
     return m.vflex({
         align = "center",
-        spacing = 3,
+        spacing = 2,
         -- padding = { 1, 2 },
-        config.auth.is_signed_in and Header(),
+        Header(),
         Nav(),
         Footer(),
     })
@@ -48,11 +66,14 @@ end)
 
 local Root = m.component(function()
     local page, set_page = m.hooks.variable("loading")
+    local user, set_user = m.hooks.variable({})
 
     return Leet.ctx.menu:provider({
         value = {
             page = page,
             set_page = set_page,
+            user = user,
+            set_user = set_user,
         },
         App(),
     })
diff --git a/lua/leetcode/ui/menu/nav/menu/cache/init.lua b/lua/leetcode/ui/menu/nav/menu/cache/init.lua
index 2969b081..2b18f1da 100644
--- a/lua/leetcode/ui/menu/nav/menu/cache/init.lua
+++ b/lua/leetcode/ui/menu/nav/menu/cache/init.lua
@@ -1,16 +1,16 @@
-local markup = require("markup")
+local m = require("markup")
 local Buttons = require("leetcode.ui.menu.buttons")
 local Button = require("leetcode.ui.menu.button")
 local BackButton = require("leetcode.ui.menu.button.back")
 
-return markup.Component(function()
+return m.component(function()
     local cmd = require("leetcode.command")
 
     return Buttons({
         Button({
             icon = "󱘴",
             title = "Update",
-            key = "u",
+            lhs = "u",
             on_submit = cmd.cache_update,
         }),
         BackButton({ page = "menu" }),
diff --git a/lua/leetcode/ui/menu/nav/menu/cookie/init.lua b/lua/leetcode/ui/menu/nav/menu/cookie/init.lua
index 267dfb06..bf5de5fa 100644
--- a/lua/leetcode/ui/menu/nav/menu/cookie/init.lua
+++ b/lua/leetcode/ui/menu/nav/menu/cookie/init.lua
@@ -3,19 +3,19 @@ local Buttons = require("leetcode.ui.menu.buttons")
 local Button = require("leetcode.ui.menu.button")
 local BackButton = require("leetcode.ui.menu.button.back")
 
-return markup.Component(function()
+return markup.component(function()
     local cmd = require("leetcode.command")
 
     return Buttons({
         Button({
             icon = "󱛬",
-            key = "u",
+            lhs = "u",
             title = "Update",
             on_press = cmd.cookie_prompt,
         }),
         Button({
             icon = "󱛪",
-            key = "d",
+            lhs = "d",
             title = "Delete / Sign out",
             on_submit = cmd.sign_out,
         }),
diff --git a/lua/leetcode/ui/menu/nav/menu/init.lua b/lua/leetcode/ui/menu/nav/menu/init.lua
index b3aad9a9..dcb09325 100644
--- a/lua/leetcode/ui/menu/nav/menu/init.lua
+++ b/lua/leetcode/ui/menu/nav/menu/init.lua
@@ -9,35 +9,35 @@ return m.component(function()
         Button({
             icon = "",
             title = "Problems",
-            key = "p",
+            lhs = "p",
             nested = true,
             page = "menu.problems",
         }),
         Button({
             icon = "",
             title = "Statistics",
-            key = "s",
+            lhs = "s",
             nested = true,
             page = "menu.statistics",
         }),
         Button({
             icon = "󰆘",
             title = "Cookie",
-            key = "i",
+            lhs = "i",
             nested = true,
             page = "menu.cookie",
         }),
         Button({
             icon = "",
             title = "Cache",
-            key = "c",
+            lhs = "c",
             nested = true,
             page = "menu.cache",
         }),
         Button({
             icon = "󰩈",
             title = "Exit",
-            key = "qa",
+            lhs = "qa",
             on_submit = function()
                 require("leetcode").stop()
             end,
diff --git a/lua/leetcode/ui/menu/nav/menu/problems/init.lua b/lua/leetcode/ui/menu/nav/menu/problems/init.lua
index 28de2e0e..c8280ac6 100644
--- a/lua/leetcode/ui/menu/nav/menu/problems/init.lua
+++ b/lua/leetcode/ui/menu/nav/menu/problems/init.lua
@@ -11,21 +11,21 @@ return m.component(function()
         Button({
             icon = "",
             title = "List",
-            key = "p",
+            lhs = "p",
             nested = true,
             on_submit = cmd.problems,
         }),
         Button({
             icon = "",
             title = "Random",
-            key = "s",
+            lhs = "s",
             nested = true,
             on_submit = cmd.random_question,
         }),
         Button({
             icon = "󰃭",
             title = "Daily",
-            key = "i",
+            lhs = "i",
             nested = true,
             on_submit = cmd.qot,
         }),
diff --git a/lua/leetcode/ui/menu/nav/menu/statistics/init.lua b/lua/leetcode/ui/menu/nav/menu/statistics/init.lua
index 4fac2881..84d7bd9e 100644
--- a/lua/leetcode/ui/menu/nav/menu/statistics/init.lua
+++ b/lua/leetcode/ui/menu/nav/menu/statistics/init.lua
@@ -1,4 +1,4 @@
-local markup = require("markup")
+local m = require("markup")
 local Buttons = require("leetcode.ui.menu.buttons")
 local Button = require("leetcode.ui.menu.button")
 local BackButton = require("leetcode.ui.menu.button.back")
@@ -7,24 +7,24 @@ local log = require("leetcode.logger")
 
 local cmd = require("leetcode.command")
 
-return markup.Component(function()
+return m.component(function()
     return Buttons({
         not config.is_cn and Button({
             title = "Skills",
             icon = "",
-            key = "s",
+            lhs = "s",
             on_submit = cmd.ui_skills,
         }),
         Button({
             icon = "",
             title = "Languages",
-            key = "l",
+            lhs = "l",
             on_submit = cmd.ui_languages,
         }),
         Button({
             icon = "",
             title = "Update",
-            key = "u",
+            lhs = "u",
             on_submit = function() end,
         }),
         BackButton({ page = "menu" }),
diff --git a/lua/leetcode/ui/menu/title.lua b/lua/leetcode/ui/menu/title.lua
index ac098b5d..914b7951 100644
--- a/lua/leetcode/ui/menu/title.lua
+++ b/lua/leetcode/ui/menu/title.lua
@@ -9,11 +9,11 @@ return m.component(function()
 
     for i, title in ipairs(parts) do
         title = title:gsub("^%l", string.upper)
-        local hl = i == #parts and "leetcode_menu_title" or "leetcode_menu_title_inactive"
-        table.insert(titles, m.Inline(title, hl))
+        local hl = i == #parts and "Label" or "Type"
+        table.insert(titles, m.inline(title, hl))
 
         if i ~= #parts then
-            table.insert(titles, m.Inline("  "))
+            table.insert(titles, m.inline("  "))
         end
     end
 
diff --git a/lua/leetcode/ui/problem/description/content/init.lua b/lua/leetcode/ui/problem/description/content/init.lua
deleted file mode 100644
index b56c3caa..00000000
--- a/lua/leetcode/ui/problem/description/content/init.lua
+++ /dev/null
@@ -1,17 +0,0 @@
-local markup = require("markup")
-local log = require("leetcode.logger")
-
-return markup.Component(function(self)
-    local content = self.props
-    local html_parser = require("markup.parser.html")
-
-    local parts = {}
-    for part in vim.gsplit(content, "<p>&nbsp;</p>") do
-        table.insert(parts, html_parser(part, "html"):parse())
-    end
-
-    return markup.Flex({
-        spacing = 2,
-        children = parts,
-    })
-end)
diff --git a/lua/leetcode/ui/problem/description/header.lua b/lua/leetcode/ui/problem/description/header.lua
deleted file mode 100644
index d03c01af..00000000
--- a/lua/leetcode/ui/problem/description/header.lua
+++ /dev/null
@@ -1,77 +0,0 @@
-local markup = require("markup")
-local utils = require("leetcode.utils")
-local config = require("leetcode.config")
-local t = require("leetcode.translator")
-local icons = config.icons
-
-local function hl(str)
-    local prefix = "leetcode_description_header"
-    if not str then
-        return prefix
-    end
-    return table.concat({ prefix, str }, "_")
-end
-
-local Title = markup.Component(function(self)
-    ---@type lc.QuestionResponse
-    local info = self.props
-    local title = utils.translate(info.title, info.translated_title)
-
-    return markup.HFlex({
-        spacing = 1,
-        children = {
-            markup.Inline(info.frontend_id .. ".", "leetcode_normal"),
-            markup.Inline(title, hl("title")),
-            markup.If(info.is_paid_only, markup.Inline(t("Premium"), "leetcode_medium")),
-        },
-    })
-end)
-
-local Stats = markup.Component(function(self)
-    ---@type lc.QuestionResponse, lc.cache.Question
-    local info, cache = self.props.info, self.props.cache
-
-    local diff = info.difficulty
-    local status = cache.status
-    local dislikes = info.dislikes
-    local likes = info.likes
-
-    local ac_rate = info.stats.acRate
-    local total_sub = info.stats.totalSubmission
-    local hints = info.hints
-
-    return markup.HFlex({
-        spacing = 1,
-        children = {
-            markup.Inline(diff, "leetcode_" .. diff:lower()),
-            markup.Inline(icons.bar, hl("deli")),
-            markup.Inline(likes, hl("stat")),
-            markup.Inline(icons.like),
-            markup.Inline(dislikes, hl("stat")),
-            markup.Inline(icons.dislike),
-            markup.Inline(icons.bar, hl("deli")),
-            markup.Inline(ac_rate, hl("stat")),
-            markup.Inline("of"),
-            markup.Inline(total_sub),
-            markup.If(not vim.tbl_isempty(hints), {
-                markup.Inline(icons.bar, hl("deli")),
-                markup.Inline("󰛨 " .. t("Hints"), hl("hint")),
-            }),
-        },
-    })
-end)
-
-return markup.Component(function(self)
-    local cache, info = self.props.cache, self.props.info
-
-    return markup.Flex({
-        align = "center",
-        spacing = 1,
-        margin = { 1, 0, 2 },
-        style = { link = hl() },
-        children = {
-            Title(info),
-            Stats({ cache = cache, info = info }),
-        },
-    })
-end)
diff --git a/lua/leetcode/ui/problem/description/init.lua b/lua/leetcode/ui/problem/description/init.lua
deleted file mode 100644
index 3a1045c6..00000000
--- a/lua/leetcode/ui/problem/description/init.lua
+++ /dev/null
@@ -1,45 +0,0 @@
-local Header = require("leetcode.ui.problem.description.header")
-local Content = require("leetcode.ui.problem.description.content")
-local utils = require("leetcode.utils")
-
-local markup = require("markup")
-
----@class leet.ui.Problem.Description : markup.Renderer
----@field protected super markup.Renderer
-local Description = markup.Renderer:extend("leet.problem.description") ---@diagnostic disable-line: undefined-field
-
----@param problem leet.ui.Problem
-function Description:init(problem)
-    Description.super.init(self, {
-        minimal = true,
-        position = "left",
-        config = {
-            vertical = true,
-            width = 0.4,
-        },
-        win_opts = {
-            wrap = true,
-            winhighlight = "Normal:MarkupNormalFloat,FloatBorder:MarkupFloatBorder",
-        },
-    })
-
-    self.problem = problem
-end
-
-function Description:render()
-    Description.super.render(self, {
-        Header({
-            cache = self.problem.cache,
-            info = self.problem.q,
-        }),
-        Content(utils.translate(self.problem.q.content, self.problem.q.translated_content)),
-    })
-end
-
--- function Description:
-
--- function Description:buf_open()
---     return markup.Inline("Description")
--- end
-
-return Description
diff --git a/lua/leetcode/ui/problem/info/init.lua b/lua/leetcode/ui/problem/info/init.lua
deleted file mode 100644
index 021370cc..00000000
--- a/lua/leetcode/ui/problem/info/init.lua
+++ /dev/null
@@ -1,121 +0,0 @@
-local markup = require("markup")
-local u = require("leetcode.utils")
-local log = require("leetcode.logger")
-local t = require("leetcode.translator")
-local problemlist = require("leetcode.cache.problemlist")
-local config = require("leetcode.config")
-local icons = config.icons
-
----@class leet.ui.Problem.Info : markup.Renderer
----@field protected super markup.Renderer
----@field problem leet.ui.Problem
-local ProblemInfo = markup.Renderer:extend("markup.problem.info") ---@diagnostic disable-line: undefined-field
-
----@param problem leet.ui.Problem
-function ProblemInfo:init(problem)
-    ProblemInfo.super.init(self, {
-        position = "float",
-        config = {
-            width = 0.5,
-            height = 0.5,
-            border = "rounded",
-            title = " Problem Info ",
-            title_pos = "center",
-        },
-        win_opts = {
-            wrap = true,
-        },
-        show = false,
-        enter = true,
-    })
-    self.problem = problem
-end
-
-function ProblemInfo:build_hints()
-    local hints = self.problem.q.hints
-
-    local children = {}
-    local total = #hints
-
-    for i, hint in ipairs(hints) do
-        local tree = markup.Tree({
-            title = ("%d/%d"):format(i, total),
-            children = { markup.Block(hint) },
-        })
-        table.insert(children, tree)
-    end
-
-    return markup.Tree({
-        title = t("Hints"),
-        children = children,
-    })
-end
-
-function ProblemInfo:build_tags()
-    local tags = self.problem.q.topic_tags
-
-    local children = {}
-
-    for _, topic in ipairs(tags) do
-        table.insert(children, markup.Block(topic.name, "leetcode_tag"))
-    end
-
-    return markup.Tree({
-        title = t("Topics"),
-        children = children,
-    })
-end
-
-function ProblemInfo:build_similar_questions()
-    local similar = self.problem.q.similar
-
-    local children = {}
-
-    for _, problem in ipairs(similar) do
-        local ok, cache = pcall(problemlist.get_by_title_slug, problem.title_slug)
-
-        if ok then
-            local elem = markup.HFlex({
-                spacing = 1,
-                on_submit = function()
-                    local Problem = require("leetcode.ui.problem")
-                    Problem(cache):mount()
-                end,
-                children = {
-                    markup.Inline(icons.square, u.diff_to_hl(cache.difficulty)),
-                    markup.Inline(u.translate(cache.title, cache.title_cn)),
-                    markup.If(
-                        cache.paid_only,
-                        markup.Inline({
-                            config.auth.is_premium and { icons.unlock }
-                                or { icons.lock, " ", t("Premium") },
-                        }, "leetcode_medium")
-                    ),
-                },
-            })
-
-            table.insert(children, elem)
-        end
-    end
-
-    return markup.Tree({
-        title = t("Similar Questions"),
-        children = children,
-    })
-end
-
-function ProblemInfo:render()
-    ProblemInfo.super.render(
-        self,
-        markup.Flex({
-            spacing = 2,
-            children = {
-                self:build_hints(),
-                self:build_tags(),
-                self:build_similar_questions(),
-            },
-        })
-    )
-end
-
-return ProblemInfo
diff --git a/lua/leetcode/ui/problem/init.lua b/lua/leetcode/ui/problem/init.lua
deleted file mode 100644
index 9c85552f..00000000
--- a/lua/leetcode/ui/problem/init.lua
+++ /dev/null
@@ -1,251 +0,0 @@
-local Description = require("leetcode.ui.problem.description")
-local Info = require("leetcode.ui.problem.info")
-local Console = require("leetcode-ui.layout.console")
-local Window = require("leetcode.ui.problem.window")
-local markup = require("markup")
-
-local api_question = require("leetcode.api.question")
-local utils = require("leetcode.utils")
-local ui_utils = require("leetcode-ui.utils")
-local config = require("leetcode.config")
-local log = require("leetcode.logger")
-
----@class leet.ui.Problem : markup.TabPage
----@field protected super markup.TabPage
----@field file Path
----@field q lc.question_res
----@field description lc.ui.Description
----@field bufnr integer
----@field console lc.ui.Console
----@field lang string
----@field cache lc.cache.Question
----@field reset boolean
----@field win leet.ui.Problem.Window
-local Problem = markup.TabPage:extend("leet.question")
-
----@param raw? boolean
-function Problem:snippet(raw)
-    local snippets = self.q.code_snippets ~= vim.NIL and self.q.code_snippets or {}
-    local snip = vim.tbl_filter(function(snip)
-        return snip.lang_slug == self.lang
-    end, snippets)[1]
-    if not snip then
-        return
-    end
-
-    local code = snip.code:gsub("\r\n", "\n")
-    return raw and code or self:injector(code)
-end
-
----@return string path, boolean existed
-function Problem:path()
-    local lang = utils.get_lang(self.lang)
-    local alt = lang.alt and ("." .. lang.alt) or ""
-
-    -- handle legacy file names first
-    local fn_legacy = --
-        ("%s.%s-%s.%s"):format(self.q.frontend_id, self.q.title_slug, lang.slug, lang.ft)
-    self.file = config.storage.home:joinpath(fn_legacy)
-
-    if self.file:exists() then
-        return self.file:absolute(), true
-    end
-
-    local fn = ("%s.%s%s.%s"):format(self.q.frontend_id, self.q.title_slug, alt, lang.ft)
-    self.file = config.storage.home:joinpath(fn)
-    local existed = self.file:exists()
-
-    if not existed then
-        self.file:write(self:snippet(), "w")
-    end
-
-    return self.file:absolute(), existed
-end
-
----@param before boolean
-function Problem:inject(before)
-    local inject = config.user.injector[self.lang] or {}
-    local inj = before and inject.before or inject.after
-
-    local res
-
-    if type(inj) == "boolean" and inj == true and before then
-        inj = config.imports[self.lang]
-    end
-
-    if type(inj) == "table" then
-        res = table.concat(inj, "\n")
-    elseif type(inj) == "string" then
-        res = inj
-    end
-
-    if res and res ~= "" then
-        return before and (res .. "\n") or ("\n" .. res)
-    else
-        return nil
-    end
-end
-
----@param code string
-function Problem:injector(code)
-    local lang = utils.get_lang(self.lang)
-
-    local parts = {
-        ("%s @leet start"):format(lang.comment),
-        code,
-        ("%s @leet end"):format(lang.comment),
-    }
-
-    local before = self:inject(true)
-    if before then
-        table.insert(parts, 1, before)
-    end
-
-    local after = self:inject(false)
-    if after then
-        table.insert(parts, after)
-    end
-
-    return table.concat(parts, "\n")
-end
-
-function Problem:_unmount()
-    if vim.v.dying ~= 0 then
-        return
-    end
-
-    vim.schedule(function()
-        self.info:unmount()
-        self.console:unmount()
-        self.description:unmount()
-
-        if self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr) then
-            vim.api.nvim_buf_delete(self.bufnr, { force = true, unload = false })
-        end
-
-        _Lc_state.questions = vim.tbl_filter(function(q)
-            return q.bufnr ~= self.bufnr
-        end, _Lc_state.questions)
-
-        self = nil
-    end)
-end
-
-function Problem:unmount()
-    if self.winid and vim.api.nvim_win_is_valid(self.winid) then
-        vim.api.nvim_win_close(self.winid, true)
-    end
-end
-
-local group = vim.api.nvim_create_augroup("leetcode_questions", { clear = true })
-function Problem:autocmds()
-    vim.api.nvim_create_autocmd("WinClosed", {
-        group = group,
-        pattern = tostring(self.winid),
-        callback = function()
-            self:_unmount()
-        end,
-    })
-end
-
-function Problem:handle_mount()
-    local path, existed = self:path()
-
-    self:set_file(path)
-    Problem.super.mount(self)
-
-    self.win = Window({
-        problem = self,
-        minimal = false,
-        bufnr = 0,
-        winid = 0,
-        buf_opts = {
-            buflisted = true,
-        },
-    })
-
-    -- self:open_buffer(existed)
-
-    self.description = Description(self)
-    -- self.console = Console(self)
-    self.info = Info(self)
-
-    table.insert(_Lc_state.questions, self)
-
-    -- self:autocmds()
-    -- utils.exec_hook("question_enter", self)
-
-    return self
-end
-
-function Problem:mount()
-    local tabp = utils.detect_duplicate_question(self.cache.title_slug, config.lang)
-    if tabp then
-        return pcall(vim.api.nvim_set_current_tabpage, tabp)
-    end
-
-    local q = api_question.by_title_slug(self.cache.title_slug)
-    if not q or q.is_paid_only and not config.auth.is_premium then
-        return log.warn("Question is for premium users only")
-    end
-    self.q = q
-
-    if self:snippet() then
-        self:handle_mount()
-    else
-        local msg = ("Snippet for `%s` not found. Select a different language"):format(self.lang)
-        log.warn(msg)
-
-        require("leetcode.pickers.language").pick_lang(self, function(snippet)
-            self.lang = snippet.t.slug
-            self:handle_mount()
-        end)
-    end
-
-    return self
-end
-
----@param self lc.ui.Question
----@param lang lc.lang
-Problem.change_lang = vim.schedule_wrap(function(self, lang)
-    local old_lang, old_bufnr = self.lang, self.bufnr
-
-    local ok, err = pcall(function()
-        self.lang = lang
-        local path, existed = self:path()
-
-        self.bufnr = vim.fn.bufadd(path)
-        assert(self.bufnr ~= 0, "Failed to create buffer " .. path)
-
-        local loaded = vim.api.nvim_buf_is_loaded(self.bufnr)
-        vim.fn.bufload(self.bufnr)
-
-        vim.api.nvim_set_option_value("buflisted", false, { buf = old_bufnr })
-        self:open_buffer(existed)
-
-        if not loaded then
-            utils.exec_hook("question_enter", self)
-        end
-    end)
-
-    if not ok then
-        log.error("Failed to change language\n" .. err)
-        self.lang = old_lang
-        self.bufnr = old_bufnr
-    end
-end)
-
----@param problem lc.cache.Question
-function Problem:init(problem)
-    self.cache = problem
-    self.lang = config.lang
-
-    Problem.super.init(self, {
-        mount = true,
-    })
-end
-
----@type fun(question: lc.cache.Question): leet.ui.Problem
-local LeetProblem = Problem
-
-return LeetProblem
diff --git a/lua/leetcode/ui/problem/window.lua b/lua/leetcode/ui/problem/window.lua
deleted file mode 100644
index 03e4471e..00000000
--- a/lua/leetcode/ui/problem/window.lua
+++ /dev/null
@@ -1,97 +0,0 @@
-local markup = require("markup")
-local log = require("leetcode.logger")
-
----@class leet.ui.Problem.Window : markup.Window
----@field protected super markup.Window
----@field problem leet.ui.Problem
-local ProblemWindow = markup.Window:extend("leet.problem.window")
-
-function ProblemWindow:init(...)
-    ProblemWindow.super.init(self, ...)
-    self.problem = self._.problem
-end
-
----@param inclusive? boolean
----@return integer, integer, string[]
-function ProblemWindow:code_range(inclusive)
-    local lines = self:buf_get_lines(0, -1, false)
-    local start_i, end_i
-
-    for i, line in ipairs(lines) do
-        if line:match("@leet start") then
-            start_i = i + (inclusive and 0 or 1)
-        elseif line:match("@leet end") then
-            end_i = i - (inclusive and 0 or 1)
-        end
-    end
-
-    return start_i, end_i, lines
-end
-
----@param submit boolean
-function ProblemWindow:code_lines(submit)
-    local start_i, end_i, lines = self:code_range()
-
-    start_i = start_i or 1
-    end_i = end_i or #lines
-
-    local prefix = not submit and ("\n"):rep(start_i - 1) or ""
-    return prefix .. table.concat(lines, "\n", start_i, end_i)
-end
-
-function ProblemWindow:fold_range()
-    local start_i, _, lines = self:code_range(true)
-    if start_i == nil or start_i <= 1 then
-        return
-    end
-
-    local i = start_i - 1
-    while lines[i] == "" do
-        i = i - 1
-    end
-
-    if 1 < i then
-        return i
-    end
-end
-
----@param code? string
-function ProblemWindow:set_code_lines(code)
-    if not (self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr)) then
-        return
-    end
-
-    pcall(vim.cmd.undojoin)
-    local s_i, e_i, lines = self:code_range()
-    s_i = s_i or 1
-    e_i = e_i or #lines
-    code = code and code or (self.problem:snippet(true) or "")
-    vim.api.nvim_buf_set_lines(self.bufnr, s_i - 1, e_i, false, vim.split(code, "\n"))
-end
-
-function ProblemWindow:reset_code_lines()
-    local new_lines = self.problem:snippet(true) or ""
-
-    -- vim.schedule(function()
-    --     log.info("Previous code found and reset\nTo undo, simply press `u`")
-    -- end)
-
-    self:set_code_lines(new_lines)
-end
-
-function ProblemWindow:buf_open()
-    ProblemWindow.super.buf_open(self)
-
-    local i = self:fold_range()
-    if i then
-        vim.api.nvim_buf_call(self.bufnr, function()
-            vim.cmd(("%d,%dfold"):format(1, i)) ---@diagnostic disable-line: param-type-mismatch
-        end)
-    end
-
-    -- if existed and self.cache.status == "ac" then
-    -- self:reset_code_lines()
-    -- end
-end
-
-return ProblemWindow
diff --git a/lua/leetcode/utils.lua b/lua/leetcode/util.lua
similarity index 80%
rename from lua/leetcode/utils.lua
rename to lua/leetcode/util.lua
index e2d9b1b5..f72a9652 100644
--- a/lua/leetcode/utils.lua
+++ b/lua/leetcode/util.lua
@@ -1,10 +1,10 @@
 local config = require("leetcode.config")
 local log = require("leetcode.logger")
 
----@class lc.Utils
-local utils = {}
+---@class leet.util
+local M = {}
 
-function utils.deprecate_usr_cmd(name, new)
+function M.deprecate_usr_cmd(name, new)
     vim.api.nvim_create_user_command(name, function()
         local cmd = require("leetcode.command")
         cmd.deprecate(name, new)
@@ -13,14 +13,14 @@ function utils.deprecate_usr_cmd(name, new)
 end
 
 ---@param fn string
-function utils.cmd(fn)
+function M.cmd(fn)
     return ("<cmd>lua require('leetcode.command').%s()<cr>"):format(fn)
 end
 
 ---@param title_slug string
 ---@param lang lc.lang
-function utils.detect_duplicate_question(title_slug, lang)
-    local tabs = utils.question_tabs()
+function M.detect_duplicate_question(title_slug, lang)
+    local tabs = M.question_tabs()
 
     for _, q in ipairs(tabs) do
         if title_slug == q.question.q.title_slug and lang == q.question.lang then
@@ -30,11 +30,11 @@ function utils.detect_duplicate_question(title_slug, lang)
 end
 
 ---@return { tabpage: integer, question: lc.ui.Question }[]
-function utils.question_tabs()
+function M.question_tabs()
     local questions = {}
 
     for _, q in ipairs(_Lc_state.questions) do
-        local tabp = utils.question_tabp(q)
+        local tabp = M.question_tabp(q)
         if tabp then
             table.insert(questions, { tabpage = tabp, question = q })
         end
@@ -45,7 +45,7 @@ end
 
 ---@param q lc.ui.Question
 ---@return integer|nil
-function utils.question_tabp(q)
+function M.question_tabp(q)
     local ok, tabp = pcall(vim.api.nvim_win_get_tabpage, q.winid)
     if ok then
         return tabp
@@ -53,9 +53,9 @@ function utils.question_tabp(q)
 end
 
 ---@return lc.ui.Question
-function utils.curr_question()
+function M.curr_question()
     local tabp = vim.api.nvim_get_current_tabpage()
-    local tabs = utils.question_tabs()
+    local tabs = M.question_tabs()
 
     local tab = vim.tbl_filter(function(tab)
         return tab.tabpage == tabp
@@ -68,7 +68,7 @@ function utils.curr_question()
 end
 
 ---@return lc.language
-function utils.get_lang(slug)
+function M.get_lang(slug)
     ---@param l lc.language
     return vim.tbl_filter(function(l)
         return l.slug == slug
@@ -76,7 +76,7 @@ function utils.get_lang(slug)
 end
 
 ---@return lc.language
-function utils.get_lang_by_name(name)
+function M.get_lang_by_name(name)
     ---@param l lc.language
     return vim.tbl_filter(function(l)
         return l.lang == name
@@ -85,7 +85,7 @@ end
 
 ---@param event lc.hook
 ---@return fun()[]|nil
-function utils.get_hooks(event)
+function M.get_hooks(event)
     local fns = config.user.hooks[event]
     if not fns then
         return
@@ -99,8 +99,8 @@ function utils.get_hooks(event)
 end
 
 ---@param event lc.hook
-function utils.exec_hooks(event, ...)
-    local fns = utils.get_hooks(event)
+function M.exec_hooks(event, ...)
+    local fns = M.get_hooks(event)
     if not fns then
         return log.error("unknown hook event: " .. event)
     end
@@ -115,7 +115,7 @@ end
 
 ---@param content string
 ---@param translated_content string
-function utils.translate(content, translated_content)
+function M.translate(content, translated_content)
     if config.is_cn then
         if config.user.cn.translate_problems then
             return translated_content or content
@@ -127,18 +127,18 @@ function utils.translate(content, translated_content)
     end
 end
 
-function utils.auth_guard()
+function M.auth_guard()
     if not config.auth.is_signed_in then
         error("User not logged-in", 0)
     end
 end
 
-function utils.norm_ins(str)
+function M.norm_ins(str)
     local ins = vim.inspect(str)
     return ins:sub(2, #ins - 1)
 end
 
-function utils.with_version(v, with, without)
+function M.with_version(v, with, without)
     with = with or function() end
     without = without or function() end
     return (vim.fn.has("nvim-" .. v) == 1 and with or without)()
@@ -146,11 +146,11 @@ end
 
 ---@param name lc.editor.section
 ---@param start? boolean
-function utils.section_tag(name, start)
+function M.section_tag(name, start)
     if name == "code" then
         return ("@leet %s"):format(start and "start" or "end")
     end
     return ("@leet %s %s"):format(name, start and "start" or "end")
 end
 
-return utils
+return M

From 007cad6d9c1999d1f3e7ac960c031e1d1910d2a9 Mon Sep 17 00:00:00 2001
From: kawre <kenterq@gmail.com>
Date: Fri, 1 Aug 2025 20:28:32 +0200
Subject: [PATCH 3/4] feat: runtime ui page

---
 LICENSE                                       |   2 +-
 .../non_standalone/leetcode.lua               |   2 +-
 lua/leetcode-ui/group/page/cache.lua          |   2 +-
 lua/leetcode-ui/group/page/cookie.lua         |   2 +-
 lua/leetcode-ui/group/page/menu.lua           |   2 +-
 lua/leetcode-ui/group/page/problems.lua       |   2 +-
 lua/leetcode-ui/group/page/signin.lua         |   2 +-
 lua/leetcode-ui/group/page/stats.lua          |   2 +-
 lua/leetcode-ui/lines/button/menu/back.lua    |   2 +-
 lua/leetcode-ui/lines/stats.lua               |   2 +-
 lua/leetcode-ui/question.lua                  |   6 +-
 lua/leetcode-ui/renderer/menu.lua             |   4 +-
 lua/leetcode-ui/renderer/result.lua           | 403 +++++------
 lua/leetcode/api/auth.lua                     |   2 +-
 lua/leetcode/api/init.lua                     | 300 ++++----
 lua/leetcode/api/interpreter.lua              | 158 ++--
 lua/leetcode/api/problems.lua                 | 410 +++++------
 lua/leetcode/cache/cookie.lua                 |   4 +-
 lua/leetcode/cache/problems.lua               |   8 +-
 lua/leetcode/{command => cmd}/arguments.lua   |   0
 lua/leetcode/cmd/init.lua                     | 681 ++++++++++++++++++
 lua/leetcode/command/init.lua                 | 657 -----------------
 lua/leetcode/config/init.lua                  | 202 +++---
 lua/leetcode/guard.lua                        |  14 +-
 lua/leetcode/init.lua                         | 224 +++---
 lua/leetcode/picker/question/init.lua         | 114 ++-
 lua/leetcode/picker/question/snacks.lua       |   2 +-
 lua/leetcode/picker/question/telescope.lua    |   2 +-
 lua/leetcode/problem/console/init.lua         |  68 +-
 lua/leetcode/problem/console/result.lua       |  22 +-
 lua/leetcode/problem/console/testcase.lua     | 150 +++-
 lua/leetcode/problem/description.lua          |  18 +-
 lua/leetcode/problem/init.lua                 | 446 ++++++++----
 lua/leetcode/problem/runner.lua               | 115 ++-
 lua/leetcode/ui/menu/nav/menu/cache/init.lua  |   2 +-
 lua/leetcode/ui/menu/nav/menu/cookie/init.lua |   2 +-
 .../ui/menu/nav/menu/problems/init.lua        |   2 +-
 .../ui/menu/nav/menu/statistics/init.lua      |   2 +-
 lua/leetcode/ui/problem/result/cases/case.lua |  55 ++
 lua/leetcode/ui/problem/result/cases/init.lua |  58 ++
 .../ui/problem/result/compile-error.lua       |   0
 lua/leetcode/ui/problem/result/header.lua     |  27 +
 lua/leetcode/ui/problem/result/init.lua       |  30 +
 .../ui/problem/result/internal-error.lua      |   0
 .../ui/problem/result/limit-exceeded.lua      |   0
 .../ui/problem/result/runtime-error.lua       |   0
 .../ui/problem/result/runtime/accepted.lua    |  55 ++
 .../ui/problem/result/runtime/init.lua        |  21 +
 .../ui/problem/result/submission-error.lua    |   0
 lua/leetcode/util.lua                         | 173 ++---
 queries/lua/highlights.scm                    |   4 +
 51 files changed, 2527 insertions(+), 1934 deletions(-)
 rename lua/leetcode/{command => cmd}/arguments.lua (100%)
 create mode 100644 lua/leetcode/cmd/init.lua
 delete mode 100644 lua/leetcode/command/init.lua
 create mode 100644 lua/leetcode/ui/problem/result/cases/case.lua
 create mode 100644 lua/leetcode/ui/problem/result/cases/init.lua
 create mode 100644 lua/leetcode/ui/problem/result/compile-error.lua
 create mode 100644 lua/leetcode/ui/problem/result/header.lua
 create mode 100644 lua/leetcode/ui/problem/result/init.lua
 create mode 100644 lua/leetcode/ui/problem/result/internal-error.lua
 create mode 100644 lua/leetcode/ui/problem/result/limit-exceeded.lua
 create mode 100644 lua/leetcode/ui/problem/result/runtime-error.lua
 create mode 100644 lua/leetcode/ui/problem/result/runtime/accepted.lua
 create mode 100644 lua/leetcode/ui/problem/result/runtime/init.lua
 create mode 100644 lua/leetcode/ui/problem/result/submission-error.lua
 create mode 100644 queries/lua/highlights.scm

diff --git a/LICENSE b/LICENSE
index a203365c..e7bd802b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2023 kawre
+Copyright (c) 2025 kawre
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/lua/leetcode-plugins/non_standalone/leetcode.lua b/lua/leetcode-plugins/non_standalone/leetcode.lua
index 195bb3db..c03ad4e0 100644
--- a/lua/leetcode-plugins/non_standalone/leetcode.lua
+++ b/lua/leetcode-plugins/non_standalone/leetcode.lua
@@ -49,7 +49,7 @@ leetcode.stop = vim.schedule_wrap(function()
 
     _Lc_state.menu:unmount()
 
-    vim.api.nvim_create_user_command("Leet", require("leetcode.command").start_with_cmd, {
+    vim.api.nvim_create_user_command("Leet", require("leetcode.cmd").start_with_cmd, {
         bar = true,
         bang = true,
         desc = "Open leetcode.nvim",
diff --git a/lua/leetcode-ui/group/page/cache.lua b/lua/leetcode-ui/group/page/cache.lua
index 2b46ffc2..b270529b 100644
--- a/lua/leetcode-ui/group/page/cache.lua
+++ b/lua/leetcode-ui/group/page/cache.lua
@@ -1,4 +1,4 @@
-local cmd = require("leetcode.command")
+local cmd = require("leetcode.cmd")
 
 local Title = require("leetcode-ui.lines.title")
 local Buttons = require("leetcode-ui.group.buttons.menu")
diff --git a/lua/leetcode-ui/group/page/cookie.lua b/lua/leetcode-ui/group/page/cookie.lua
index 81cab3da..ed691bfb 100644
--- a/lua/leetcode-ui/group/page/cookie.lua
+++ b/lua/leetcode-ui/group/page/cookie.lua
@@ -1,4 +1,4 @@
-local cmd = require("leetcode.command")
+local cmd = require("leetcode.cmd")
 
 local Title = require("leetcode-ui.lines.title")
 local Buttons = require("leetcode-ui.group.buttons.menu")
diff --git a/lua/leetcode-ui/group/page/menu.lua b/lua/leetcode-ui/group/page/menu.lua
index bc87ead7..1faa0fd2 100644
--- a/lua/leetcode-ui/group/page/menu.lua
+++ b/lua/leetcode-ui/group/page/menu.lua
@@ -1,4 +1,4 @@
-local cmd = require("leetcode.command")
+local cmd = require("leetcode.cmd")
 
 local Page = require("leetcode-ui.group.page")
 local Title = require("leetcode-ui.lines.title")
diff --git a/lua/leetcode-ui/group/page/problems.lua b/lua/leetcode-ui/group/page/problems.lua
index 8e0cfcdc..3caf7043 100644
--- a/lua/leetcode-ui/group/page/problems.lua
+++ b/lua/leetcode-ui/group/page/problems.lua
@@ -1,4 +1,4 @@
-local cmd = require("leetcode.command")
+local cmd = require("leetcode.cmd")
 
 local Title = require("leetcode-ui.lines.title")
 local Button = require("leetcode-ui.lines.button.menu")
diff --git a/lua/leetcode-ui/group/page/signin.lua b/lua/leetcode-ui/group/page/signin.lua
index 0ad0a4bc..fb21f2b6 100644
--- a/lua/leetcode-ui/group/page/signin.lua
+++ b/lua/leetcode-ui/group/page/signin.lua
@@ -1,4 +1,4 @@
-local cmd = require("leetcode.command")
+local cmd = require("leetcode.cmd")
 local config = require("leetcode.config")
 
 local Page = require("leetcode-ui.group.page")
diff --git a/lua/leetcode-ui/group/page/stats.lua b/lua/leetcode-ui/group/page/stats.lua
index 853517af..032936f5 100644
--- a/lua/leetcode-ui/group/page/stats.lua
+++ b/lua/leetcode-ui/group/page/stats.lua
@@ -1,4 +1,4 @@
-local cmd = require("leetcode.command")
+local cmd = require("leetcode.cmd")
 local config = require("leetcode.config")
 
 local Solved = require("leetcode-ui.lines.solved")
diff --git a/lua/leetcode-ui/lines/button/menu/back.lua b/lua/leetcode-ui/lines/button/menu/back.lua
index 453dd9c1..81ee1adc 100644
--- a/lua/leetcode-ui/lines/button/menu/back.lua
+++ b/lua/leetcode-ui/lines/button/menu/back.lua
@@ -1,5 +1,5 @@
 local MenuButton = require("leetcode-ui.lines.button.menu")
-local cmd = require("leetcode.command")
+local cmd = require("leetcode.cmd")
 
 ---@class lc.ui.Button.Menu.Back : lc.ui.Button.Menu
 local MenuBackButton = MenuButton:extend("LeetMenuBackButton")
diff --git a/lua/leetcode-ui/lines/stats.lua b/lua/leetcode-ui/lines/stats.lua
index 1fd089f3..dadcfbba 100644
--- a/lua/leetcode-ui/lines/stats.lua
+++ b/lua/leetcode-ui/lines/stats.lua
@@ -1,7 +1,7 @@
 local Lines = require("leetcode-ui.lines")
 local t = require("leetcode.translator")
 local log = require("leetcode.logger")
-local cmd = require("leetcode.command")
+local cmd = require("leetcode.cmd")
 local config = require("leetcode.config")
 
 ---@class lc.ui.menu.Stats : lc.ui.Lines
diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua
index b9a9516c..1b1d624a 100644
--- a/lua/leetcode-ui/question.lua
+++ b/lua/leetcode-ui/question.lua
@@ -18,7 +18,7 @@ local log = require("leetcode.logger")
 ---@field bufnr integer
 ---@field console lc.ui.Console
 ---@field lang string
----@field cache lc.cache.Question
+---@field cache leet.cache.problem
 ---@field reset boolean
 local Question = Object("LeetQuestion")
 
@@ -450,13 +450,13 @@ Question.change_lang = vim.schedule_wrap(function(self, lang)
     end
 end)
 
----@param problem lc.cache.Question
+---@param problem leet.cache.problem
 function Question:init(problem)
     self.cache = problem
     self.lang = config.lang
 end
 
----@type fun(question: lc.cache.Question): lc.ui.Question
+---@type fun(question: leet.cache.problem): lc.ui.Question
 local LeetQuestion = Question ---@diagnostic disable-line: assign-type-mismatch
 
 return LeetQuestion
diff --git a/lua/leetcode-ui/renderer/menu.lua b/lua/leetcode-ui/renderer/menu.lua
index 914b1842..0dfc4d21 100644
--- a/lua/leetcode-ui/renderer/menu.lua
+++ b/lua/leetcode-ui/renderer/menu.lua
@@ -156,7 +156,7 @@ function Menu:unmount()
         return
     end
 
-    require("leetcode.command").q_close_all()
+    require("leetcode.cmd").q_close_all()
 
     vim.schedule(function()
         if self.winid and vim.api.nvim_win_is_valid(self.winid) then
@@ -194,7 +194,7 @@ function Menu:mount()
                 self:set_page("signin")
                 log.err(err)
             else
-                local cmd = require("leetcode.command")
+                local cmd = require("leetcode.cmd")
                 cmd.start_user_session()
             end
         end)
diff --git a/lua/leetcode-ui/renderer/result.lua b/lua/leetcode-ui/renderer/result.lua
index 1221e0ea..86b49457 100644
--- a/lua/leetcode-ui/renderer/result.lua
+++ b/lua/leetcode-ui/renderer/result.lua
@@ -24,253 +24,254 @@ local log = require("leetcode.logger")
 local ResultLayout = Renderer:extend("LeetResultLayout")
 
 function ResultLayout:handle_accepted(item)
-    self:set_opts({ spacing = 2 })
-
-    local function perc_hi(perc) --
-        if perc >= 50 then
-            return "leetcode_ok"
-        elseif perc >= 10 then
-            return nil
-        elseif perc >= 0 then
-            return "leetcode_error"
-        end
-    end
-
-    local header = Header(item)
-    self:insert(header)
-
-    --------------------------------------------
-    --- Runtime
-    --------------------------------------------
-    local runtime = Lines()
-    local runtime_ms = item.display_runtime or vim.split(item.status_runtime, " ")[1] or "NIL"
-    runtime:append(runtime_ms):append(" ms", "leetcode_alt")
-    runtime:endl()
-
-    runtime:append(
-        ("%s %.2f%% "):format(t("Beats"), item.runtime_percentile),
-        perc_hi(item.runtime_percentile)
-    )
-
-    local lang = utils.get_lang_by_name(item.pretty_lang)
-    local lang_text = { item.pretty_lang, "Structure" }
-    if lang then
-        lang_text = { lang.icon .. " " .. lang.lang, lang.hl or "Structure" }
-    end
-
-    if config.translator then
-        runtime
-            :append("使用 ", "leetcode_normal")
-            :append(unpack(lang_text))
-            :append(" 的用户", "leetcode_normal")
-    else
-        runtime:append("of users with ", "leetcode_normal"):append(unpack(lang_text))
-    end
-
-    local runtime_title = Line():append("󰓅 " .. t("Runtime"))
-    self:insert(Pre(runtime_title, runtime))
-
-    --------------------------------------------
-    --- Memory
-    --------------------------------------------
-    local memory = Lines()
-    item.status_memory = item.status_memory:gsub("(%d+)%s*(%a+)", "%1 %2")
-    local s_mem = vim.split(item.status_memory, " ")
-    memory:append(s_mem[1] .. " "):append(s_mem[2], "leetcode_alt")
-    memory:endl()
-
-    memory:append(
-        ("%s %.2f%% "):format(t("Beats"), item.memory_percentile),
-        perc_hi(item.memory_percentile)
-    )
-    if config.translator then
-        memory
-            :append("使用 ", "leetcode_normal")
-            :append(unpack(lang_text))
-            :append(" 的用户", "leetcode_normal")
-    else
-        memory:append("of users with ", "leetcode_normal"):append(unpack(lang_text))
-    end
-
-    local memory_title = Line():append("󰍛 " .. t("Memory"))
-    self:insert(Pre(memory_title, memory))
-
-    --------------------------------------------
-    --- More challenges
-    --------------------------------------------
-    if not vim.tbl_isempty(self.parent.question.q.similar or {}) then
-        local title_txt = (" %s"):format(t("More challenges"))
-        local more_title = Line():append(title_txt, "leetcode_normal")
-        local similar = SimilarQuestions(self.parent.question.q.similar)
-        self:insert(Pre(more_title, similar))
-    end
+	self:set_opts({ spacing = 2 })
+
+	local function perc_hi(perc) --
+		if perc >= 50 then
+			return "leetcode_ok"
+		elseif perc >= 10 then
+			return nil
+		elseif perc >= 0 then
+			return "leetcode_error"
+		end
+	end
+
+	local header = Header(item)
+	self:insert(header)
+
+	--------------------------------------------
+	--- Runtime
+	--------------------------------------------
+	local runtime = Lines()
+	local runtime_ms = item.display_runtime or vim.split(item.status_runtime, " ")[1] or "NIL"
+	runtime:append(runtime_ms):append(" ms", "leetcode_alt")
+	runtime:endl()
+
+	runtime:append(
+		("%s %.2f%% "):format(t("Beats"), item.runtime_percentile),
+		perc_hi(item.runtime_percentile)
+	)
+
+	local lang = utils.get_lang_by_name(item.pretty_lang)
+	local lang_text = { item.pretty_lang, "Structure" }
+	if lang then
+		lang_text = { lang.icon .. " " .. lang.lang, lang.hl or "Structure" }
+	end
+
+	if config.translator then
+		runtime
+			:append("使用 ", "leetcode_normal")
+			:append(unpack(lang_text))
+			:append(" 的用户", "leetcode_normal")
+	else
+		runtime:append("of users with ", "leetcode_normal"):append(unpack(lang_text))
+	end
+
+	local runtime_title = Line():append("󰓅 " .. t("Runtime"))
+	self:insert(Pre(runtime_title, runtime))
+
+	--------------------------------------------
+	--- Memory
+	--------------------------------------------
+	local memory = Lines()
+	item.status_memory = item.status_memory:gsub("(%d+)%s*(%a+)", "%1 %2")
+	local s_mem = vim.split(item.status_memory, " ")
+	memory:append(s_mem[1] .. " "):append(s_mem[2], "leetcode_alt")
+	memory:endl()
+
+	memory:append(
+		("%s %.2f%% "):format(t("Beats"), item.memory_percentile),
+		perc_hi(item.memory_percentile)
+	)
+	if config.translator then
+		memory
+			:append("使用 ", "leetcode_normal")
+			:append(unpack(lang_text))
+			:append(" 的用户", "leetcode_normal")
+	else
+		memory:append("of users with ", "leetcode_normal"):append(unpack(lang_text))
+	end
+
+	local memory_title = Line():append("󰍛 " .. t("Memory"))
+	self:insert(Pre(memory_title, memory))
+
+	--------------------------------------------
+	--- More challenges
+	--------------------------------------------
+	if not vim.tbl_isempty(self.parent.question.q.similar or {}) then
+		local title_txt = (" %s"):format(t("More challenges"))
+		local more_title = Line():append(title_txt, "leetcode_normal")
+		local similar = SimilarQuestions(self.parent.question.q.similar)
+		self:insert(Pre(more_title, similar))
+	end
 end
 
 ---@private
 ---
 ---@param item lc.runtime
 function ResultLayout:handle_runtime(item) -- status code = 10
-    if item._.submission then
-        return self:handle_accepted(item)
-    end
+	if item._.submission then
+		return self:handle_accepted(item)
+	end
 
-    local header = Header(item)
-    self:insert(header)
+	local header = Header(item)
+	self:insert(header)
 
-    local cases = Cases(item, self.parent)
-    self:insert(cases)
+	local cases = Cases(item, self.parent)
+	self:insert(cases)
 end
 
 ---@private
 ---
 ---@param item lc.submission
 function ResultLayout:handle_submission_error(item) -- status code = 11
-    local header = Header(item)
-    self:insert(header)
-
-    self:insert(Case({ ---@diagnostic disable-line
-        input = vim.split(item.input, "\n"),
-        raw_input = item.last_testcase,
-        output = item.code_output,
-        expected = item.expected_output,
-        std_output = item.std_output,
-    }, false, self.parent.question))
+	local header = Header(item)
+	self:insert(header)
+
+	self:insert(Case({ ---@diagnostic disable-line
+		input = vim.split(item.input, "\n"),
+		raw_input = item.last_testcase,
+		output = item.code_output,
+		expected = item.expected_output,
+		std_output = item.std_output,
+	}, false, self.parent.question))
 end
 
 ---@private
 ---
 ---@param item lc.limit_exceeded_error
 function ResultLayout:handle_limit_exceeded(item) -- status code = 12,13,14
-    local header = Header(item)
-    self:insert(header)
-
-    if item._.submission then
-        local input = Input(
-            (" %s"):format(t("Last Executed Input")),
-            vim.split(item.last_testcase, "\n"),
-            self.parent.question.q.meta_data.params
-        )
-        self:insert(input)
-
-        local stdout = Stdout(item.std_output or "")
-        self:insert(stdout)
-    elseif item.std_output_list then
-        local stdout = Stdout(item.std_output_list[#item.std_output_list])
-        self:insert(stdout)
-    end
+	local header = Header(item)
+	self:insert(header)
+
+	if item._.submission then
+		local input = Input(
+			(" %s"):format(t("Last Executed Input")),
+			vim.split(item.last_testcase, "\n"),
+			self.parent.question.q.meta_data.params
+		)
+		self:insert(input)
+
+		local stdout = Stdout(item.std_output or "")
+		self:insert(stdout)
+	elseif item.std_output_list then
+		local stdout = Stdout(item.std_output_list[#item.std_output_list])
+		self:insert(stdout)
+	end
 end
 
 ---@private
 ---
 ---@param item lc.runtime_error
 function ResultLayout:handle_runtime_error(item) -- status code = 15
-    local header = Header(item)
-
-    local lines = Lines()
-    for line in vim.gsplit(item.full_runtime_error, "\n") do
-        lines:append(line, "leetcode_error"):endl()
-    end
-    self:insert(Pre(header, lines))
-
-    if item._.submission then
-        local pre_header = Line()
-        pre_header:append((" %s"):format(t("Last Executed Input")), "leetcode_normal")
-
-        local last_testcase = Line()
-        last_testcase:append(item.last_testcase:gsub("\n", " "), "leetcode_indent")
-
-        local last_exec = Pre(pre_header, last_testcase)
-        self:insert(last_exec)
-
-        local stdout = Stdout(item.std_output or "")
-        self:insert(stdout)
-    elseif item.std_output_list then
-        local stdout = Stdout(item.std_output_list[#item.std_output_list])
-        self:insert(stdout)
-    end
+	local header = Header(item)
+
+	local lines = Lines()
+	for line in vim.gsplit(item.full_runtime_error, "\n") do
+		lines:append(line, "leetcode_error"):endl()
+	end
+	self:insert(Pre(header, lines))
+
+	if item._.submission then
+		local pre_header = Line()
+		pre_header:append((" %s"):format(t("Last Executed Input")), "leetcode_normal")
+
+		local last_testcase = Line()
+		last_testcase:append(item.last_testcase:gsub("\n", " "), "leetcode_indent")
+
+		local last_exec = Pre(pre_header, last_testcase)
+		self:insert(last_exec)
+
+		local stdout = Stdout(item.std_output or "")
+		self:insert(stdout)
+	elseif item.std_output_list then
+		local stdout = Stdout(item.std_output_list[#item.std_output_list])
+		self:insert(stdout)
+	end
 end
 
 function ResultLayout:handle_internal_error(item) -- status code = 16
-    local header = Header(item)
-    self:insert(header)
+	local header = Header(item)
+	self:insert(header)
 end
 
 ---@private
 ---
 ---@param item lc.compile_error
 function ResultLayout:handle_compile_error(item) -- status code = 20
-    local header = Header(item)
+	local header = Header(item)
 
-    local lines = Lines()
-    for line in vim.gsplit(item.full_compile_error, "\n") do
-        lines:append(line, "leetcode_error"):endl()
-    end
+	local lines = Lines()
+	for line in vim.gsplit(item.full_compile_error, "\n") do
+		lines:append(line, "leetcode_error"):endl()
+	end
 
-    local pre = Pre(header, lines)
-    self:insert(pre)
+	local pre = Pre(header, lines)
+	self:insert(pre)
 end
 
 ---@param item lc.interpreter_response
 function ResultLayout:handle_res(item)
-    self:clear()
-    self:set_opts({ spacing = 1 })
-
-    local handlers = {
-        -- runtime
-        [10] = function()
-            self:handle_runtime(item --[[@as lc.runtime]])
-        end,
-        [11] = function()
-            self:handle_submission_error(item --[[@as lc.submission]])
-        end,
-
-        -- memory limit
-        [12] = function()
-            self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
-        end,
-        -- time limit
-        [13] = function()
-            self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
-        end,
-        -- output limit
-        [14] = function()
-            self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
-        end,
-
-        -- runtime error
-        [15] = function()
-            self:handle_runtime_error(item --[[@as lc.runtime_error]])
-        end,
-
-        -- internal error
-        [16] = function()
-            self:handle_internal_error(item --[[@as lc.internal_error]])
-        end,
-
-        -- compiler
-        [20] = function()
-            self:handle_compile_error(item --[[@as lc.compile_error]])
-        end,
-
-        -- unknown
-        ["unknown"] = function()
-            log.error("unknown runner status code: " .. item.status_code)
-        end,
-    }
-
-    local handler = handlers[item.status_code]
-    if handler then
-        handler()
-    else
-        handlers["unknown"]()
-    end
+	self:clear()
+	self:set_opts({ spacing = 1 })
+	Markup.log(item)
+
+	local handlers = {
+		-- runtime
+		[10] = function()
+			self:handle_runtime(item --[[@as lc.runtime]])
+		end,
+		[11] = function()
+			self:handle_submission_error(item --[[@as lc.submission]])
+		end,
+
+		-- memory limit
+		[12] = function()
+			self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
+		end,
+		-- time limit
+		[13] = function()
+			self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
+		end,
+		-- output limit
+		[14] = function()
+			self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
+		end,
+
+		-- runtime error
+		[15] = function()
+			self:handle_runtime_error(item --[[@as lc.runtime_error]])
+		end,
+
+		-- internal error
+		[16] = function()
+			self:handle_internal_error(item --[[@as lc.internal_error]])
+		end,
+
+		-- compiler
+		[20] = function()
+			self:handle_compile_error(item --[[@as lc.compile_error]])
+		end,
+
+		-- unknown
+		["unknown"] = function()
+			log.error("unknown runner status code: " .. item.status_code)
+		end,
+	}
+
+	local handler = handlers[item.status_code]
+	if handler then
+		handler()
+	else
+		handlers["unknown"]()
+	end
 end
 
 ---@param parent lc.ui.Console
 function ResultLayout:init(parent)
-    ResultLayout.super.init(self, {}, { spacing = 1 })
+	ResultLayout.super.init(self, {}, { spacing = 1 })
 
-    self.parent = parent
+	self.parent = parent
 end
 
 ---@type fun(parent: lc.ui.Console): lc.ui.Result
diff --git a/lua/leetcode/api/auth.lua b/lua/leetcode/api/auth.lua
index 12dfd497..93f13461 100644
--- a/lua/leetcode/api/auth.lua
+++ b/lua/leetcode/api/auth.lua
@@ -25,7 +25,7 @@ local function handle(res, err)
     end
 
     if err.msg then
-        require("leetcode.command").delete_cookie()
+        require("leetcode.cmd").delete_cookie()
         return nil, err
     end
 
diff --git a/lua/leetcode/api/init.lua b/lua/leetcode/api/init.lua
index 556af9ab..ddfc3a52 100644
--- a/lua/leetcode/api/init.lua
+++ b/lua/leetcode/api/init.lua
@@ -13,189 +13,189 @@ local curl = require("plenary.curl")
 ---@field post leet.api.method
 ---@field get leet.api.method
 ---@field put leet.api.method
----@overload fun(method: string, endpoint: string, opts?: table): any
+---@overload fun(method: string, opts?: table): any
 local M = setmetatable({}, {
-    __index = function(t, k)
-        if k == "post" or k == "get" or k == "put" then
-            t[k] = function(endpoint, opts)
-                opts = vim.tbl_extend("keep", opts or {}, { endpoint = endpoint })
-                return t(k, endpoint, opts)
-            end
-        else
-            t[k] = require("leetcode.api." .. k)
-        end
-        return rawget(t, k)
-    end,
-    __call = function(t, ...)
-        return t.curl(...)
-    end,
+	__index = function(t, k)
+		if k == "post" or k == "get" or k == "put" then
+			t[k] = function(endpoint, opts)
+				opts = vim.tbl_extend("keep", opts or {}, { endpoint = endpoint })
+				return t.curl(k, opts)
+			end
+		else
+			t[k] = require("leetcode.api." .. k)
+		end
+		return rawget(t, k)
+	end,
+	__call = function(t, ...)
+		return t.curl(...)
+	end,
 })
 
 local function headers()
-    local cookie = Cookie.get()
-
-    return vim.tbl_extend("force", {
-        ["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
-        ["Referer"] = ("https://leetcode.%s"):format(config.domain),
-        ["Origin"] = ("https://leetcode.%s/"):format(config.domain),
-        ["Content-Type"] = "application/json",
-        ["Accept"] = "application/json",
-        ["Host"] = ("leetcode.%s"):format(config.domain),
-        -- ["X-Requested-With"] = "XMLHttpRequest",
-    }, cookie and {
-        ["Cookie"] = cookie.str,
-        ["x-csrftoken"] = cookie.csrftoken,
-    } or {})
+	local cookie = Cookie.get()
+
+	return vim.tbl_extend("force", {
+		["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
+		["Referer"] = ("https://leetcode.%s"):format(config.domain),
+		["Origin"] = ("https://leetcode.%s/"):format(config.domain),
+		["Content-Type"] = "application/json",
+		["Accept"] = "application/json",
+		["Host"] = ("leetcode.%s"):format(config.domain),
+		-- ["X-Requested-With"] = "XMLHttpRequest",
+	}, cookie and {
+		["Cookie"] = cookie.str,
+		["x-csrftoken"] = cookie.csrftoken,
+	} or {})
 end
 
 ---@param err lc.err
 local function check_err(err)
-    if not err then
-        return
-    end
-
-    if err.status then
-        if err.status == 401 or err.status == 403 then
-            err.msg =
-                "Your cookie may have expired, or LeetCode has temporarily restricted API access"
-        end
-    end
-
-    return err
+	if not err then
+		return
+	end
+
+	if err.status then
+		if err.status == 401 or err.status == 403 then
+			err.msg =
+			"Your cookie may have expired, or LeetCode has temporarily restricted API access"
+		end
+	end
+
+	return err
 end
 
 ---@private
 ---@return table, lc.err
 local function handle_res(out)
-    local res, err
-    Markup.log.debug(out)
-
-    if out.exit ~= 0 then
-        err = {
-            code = out.exit,
-            msg = "curl failed",
-        }
-    elseif out.status >= 300 then
-        local ok, msg = pcall(function()
-            local dec = vim.json.decode(out.body)
-
-            if dec.error then
-                return dec.error
-            end
-
-            local tbl = {}
-            for _, e in ipairs(dec.errors) do
-                table.insert(tbl, e.message)
-            end
-
-            return table.concat(tbl, "\n")
-        end)
-
-        res = out.body
-        err = {
-            code = 0,
-            status = out.status,
-            response = out,
-            msg = "http error " .. out.status .. (ok and ("\n\n" .. msg) or ""),
-            out = out.body,
-        }
-    else
-        res = vim.json.decode(out.body)
-    end
-
-    return res, check_err(err)
+	local res, err
+	Markup.log.debug(out)
+
+	if out.exit ~= 0 then
+		err = {
+			code = out.exit,
+			msg = "curl failed",
+		}
+	elseif out.status >= 300 then
+		local ok, msg = pcall(function()
+			local dec = vim.json.decode(out.body)
+
+			if dec.error then
+				return dec.error
+			end
+
+			local tbl = {}
+			for _, e in ipairs(dec.errors) do
+				table.insert(tbl, e.message)
+			end
+
+			return table.concat(tbl, "\n")
+		end)
+
+		res = out.body
+		err = {
+			code = 0,
+			status = out.status,
+			response = out,
+			msg = "http error " .. out.status .. (ok and ("\n\n" .. msg) or ""),
+			out = out.body,
+		}
+	else
+		res = vim.json.decode(out.body)
+	end
+
+	return res, check_err(err)
 end
 
 ---@private
 ---@param method string
 ---@param params table
 function M.curl(method, params)
-    local params_cpy = vim.deepcopy(params)
-
-    params = vim.tbl_deep_extend("force", {
-        headers = headers(),
-        compressed = false,
-        retry = 5,
-        endpoint = urls.base,
-    }, params or {})
-    local url = ("https://leetcode.%s%s"):format(config.domain, params.endpoint)
-
-    if type(params.body) == "table" then
-        params.body = vim.json.encode(params.body)
-    end
-
-    local tries = params.retry
-    local function should_retry(err)
-        return err and err.status >= 500 and tries > 0
-    end
-
-    if params.callback then
-        local cb = vim.schedule_wrap(params.callback)
-        params.callback = function(out, _)
-            local res, err = handle_res(out)
-
-            if should_retry(err) then
-                Markup.log.debug("retry " .. tries)
-                params_cpy.retry = tries - 1
-                M.curl(method, params_cpy)
-            else
-                cb(res, err)
-            end
-        end
-
-        curl[method](url, params)
-    else
-        local out = curl[method](url, params)
-        local res, err = handle_res(out)
-
-        if should_retry(err) then
-            Markup.log.debug("retry " .. tries)
-            params_cpy.retry = tries - 1
-            M.curl(method, params_cpy)
-        else
-            return res, err
-        end
-    end
+	local params_cpy = vim.deepcopy(params)
+
+	params = vim.tbl_deep_extend("force", {
+		headers = headers(),
+		compressed = false,
+		retry = 5,
+		endpoint = urls.base,
+	}, params or {})
+	local url = ("https://leetcode.%s%s"):format(config.domain, params.endpoint)
+
+	if type(params.body) == "table" then
+		params.body = vim.json.encode(params.body)
+	end
+
+	local tries = params.retry
+	local function should_retry(err)
+		return err and err.status >= 500 and tries > 0
+	end
+
+	if params.callback then
+		local cb = vim.schedule_wrap(params.callback)
+		params.callback = function(out, _)
+			local res, err = handle_res(out)
+
+			if should_retry(err) then
+				Markup.log.debug("retry " .. tries)
+				params_cpy.retry = tries - 1
+				M.curl(method, params_cpy)
+			else
+				cb(res, err)
+			end
+		end
+
+		curl[method](url, params)
+	else
+		local out = curl[method](url, params)
+		local res, err = handle_res(out)
+
+		if should_retry(err) then
+			Markup.log.debug("retry " .. tries)
+			params_cpy.retry = tries - 1
+			M.curl(method, params_cpy)
+		else
+			return res, err
+		end
+	end
 end
 
 ---@param query string
 ---@param variables? table optional
 ---@param opts? { callback?: function, endpoint?: string }
 function M.query(query, variables, opts)
-    opts = vim.tbl_deep_extend("force", {
-        body = { query = query, variables = variables },
-    }, opts or {})
+	opts = vim.tbl_deep_extend("force", {
+		body = { query = query, variables = variables },
+	}, opts or {})
 
-    return M.curl("post", opts)
+	return M.curl("post", opts)
 end
 
 function M.normalize_similar_cn(s)
-    return vim.tbl_map(function(sq)
-        return {
-            title = sq.title,
-            translated_title = sq.translatedTitle,
-            paid_only = sq.isPaidOnly,
-            title_slug = sq.titleSlug,
-            difficulty = sq.difficulty,
-        }
-    end, vim.json.decode(s))
+	return vim.tbl_map(function(sq)
+		return {
+			title = sq.title,
+			translated_title = sq.translatedTitle,
+			paid_only = sq.isPaidOnly,
+			title_slug = sq.titleSlug,
+			difficulty = sq.difficulty,
+		}
+	end, vim.json.decode(s))
 end
 
----@param problems lc.cache.Question[]
+---@param problems leet.cache.problem[]
 ---@param titles { questionId: integer, title: string }[]
 function M.translate_titles(problems, titles)
-    local map = {}
-    for _, title in ipairs(titles) do
-        map[title.questionId] = title.title
-    end
-
-    return vim.tbl_map(function(p)
-        local title = map[tostring(p.id)]
-        if title then
-            p.title_cn = title
-        end
-        return p
-    end, problems)
+	local map = {}
+	for _, title in ipairs(titles) do
+		map[title.questionId] = title.title
+	end
+
+	return vim.tbl_map(function(p)
+		local title = map[tostring(p.id)]
+		if title then
+			p.title_cn = title
+		end
+		return p
+	end, problems)
 end
 
 return M
diff --git a/lua/leetcode/api/interpreter.lua b/lua/leetcode/api/interpreter.lua
index 5606550e..d7aca5f3 100644
--- a/lua/leetcode/api/interpreter.lua
+++ b/lua/leetcode/api/interpreter.lua
@@ -11,53 +11,69 @@ local interpreter = {}
 ---@param item lc.interpreter_response
 ---
 ---@return lc.interpreter_response
-function interpreter:handle_item(item)
-    local success = false
-    if item.status_code == 10 then
-        success = item.compare_result:match("^[1]+$") and true or false
-        item.status_msg = success and "Accepted" or "Wrong Answer"
-    end
-
-    local submission = false
-    if item.submission_id then
-        submission = not item.submission_id:find("runcode") and true or false
-    end
-    local hl = success and "leetcode_ok" or "leetcode_error"
-
-    if item.status_code == 15 and item.invalid_testcase then
-        item.status_msg = "Invalid Testcase"
-    end
-
-    item._ = {
-        title = " " .. t(item.status_msg),
-        hl = hl,
-        success = success,
-        submission = submission,
-    }
-
-    return item
+local function resolve_item(item)
+	local success = false
+	if item.status_code == 10 then
+		success = item.compare_result:match("^[1]+$") and true or false
+		item.status_msg = success and "Accepted" or "Wrong Answer"
+	end
+
+	local submission = false
+	if item.submission_id then
+		submission = not item.submission_id:find("runcode") and true or false
+	end
+	local hl = success and "leetcode_ok" or "leetcode_error"
+
+	if item.status_code == 15 and item.invalid_testcase then
+		item.status_msg = "Invalid Testcase"
+	end
+
+	item._ = {
+		title = " " .. t(item.status_msg),
+		hl = hl,
+		success = success,
+		submission = submission,
+	}
+
+	return item
 end
 
+---@param id string
+---@param cb function
+---
+---@return lc.Interpreter.Response
+local function check(id, cb)
+	local url = urls.check:format(id)
+	Leet.api.get(url, { callback = cb })
+end
+
+-- local function fetch(url, opts)
+--     Leet.api.post(url, {
+--         body = opts.body,
+--         callback = opts.callback,
+--     })
+-- end
+
 ---@param id string
 ---@param callback function
 function interpreter.listener(id, callback)
-    local function listen()
-        interpreter.check(id, function(item, err)
-            if err then -- error
-                callback(nil, nil, err)
-            elseif item.status_code then -- got results
-                item = interpreter:handle_item(item)
-                callback(item)
-            else -- still judging
-                local intervals = config.auth.is_premium and { 450, 450 } or { 450, 900 }
-                local interval = item.sate == "STARTED" and intervals[2] or intervals[1]
-                callback(nil, item.state)
-                vim.defer_fn(listen, interval)
-            end
-        end)
-    end
-
-    listen()
+	local function listen()
+		check(id, function(item, err)
+			if err then         -- error
+				callback(nil, nil, err)
+			elseif item.status_code then -- got results
+				item = resolve_item(item)
+				callback(item)
+			else -- still judging
+				local intervals = config.auth.is_premium and { 450, 450 } or { 450, 900 }
+				local interval = item.sate == "STARTED" and intervals[2] or intervals[1]
+				callback(nil, item.state)
+				vim.defer_fn(listen, interval)
+			end
+		end)
+	end
+
+	listen()
 end
 
 ---@class lc.Interpret.body
@@ -66,45 +82,29 @@ end
 ---@field data_input string
 
 ---@param submit boolean
----@param q lc.ui.Question
+---@param problem leet.problem
 ---@param body lc.Interpret.body|string
 ---@param callback function
-function interpreter.run(submit, q, body, callback)
-    local url = (submit and urls.submit or urls.interpret):format(q.q.title_slug)
-
-    interpreter.fetch(url, {
-        body = body,
-        callback = function(res, err)
-            if err then
-                if err.status == 429 then
-                    err.msg = "You have attempted to run code too soon"
-                    err.lvl = vim.log.levels.WARN
-                end
-                callback(nil, nil, err)
-            else
-                local id = submit and res.submission_id or res.interpret_id
-                q.console.testcase:snapshot(id, res)
-                q.console.result:clear()
-                interpreter.listener(id, callback)
-            end
-        end,
-    })
-end
-
----@param id string
----@param cb function
----
----@return lc.Interpreter.Response
-function interpreter.check(id, cb)
-    local url = urls.check:format(id)
-    Leet.api.get(url, { callback = cb })
-end
-
-function interpreter.fetch(url, opts)
-    Leet.api.post(url, {
-        body = opts.body,
-        callback = opts.callback,
-    })
+function interpreter.run(submit, problem, body, callback)
+	local url = (submit and urls.submit or urls.interpret):format(problem.problem.title_slug)
+
+	Leet.api.post(url, {
+		body = body,
+		callback = function(res, err)
+			if err then
+				if err.status == 429 then
+					err.msg = "You have attempted to run code too soon"
+					err.lvl = vim.log.levels.WARN
+				end
+				callback(nil, nil, err)
+			else
+				local id = submit and res.submission_id or res.interpret_id
+				problem.console.testcase:snapshot(id, res)
+				problem.console.result:clear()
+				interpreter.listener(id, callback)
+			end
+		end,
+	})
 end
 
 return interpreter
diff --git a/lua/leetcode/api/problems.lua b/lua/leetcode/api/problems.lua
index c959a631..0139654a 100644
--- a/lua/leetcode/api/problems.lua
+++ b/lua/leetcode/api/problems.lua
@@ -9,245 +9,245 @@ local M = {}
 ---@param cb? function
 ---@param noti? boolean
 ---
----@return lc.cache.Question[], lc.err
+---@return leet.cache.problem[], lc.err
 function M.all(cb, noti)
-    local endpoint = urls.problems:format("algorithms")
-
-    local spinner
-    if noti then
-        spinner = Spinner:start("updating problemlist cache", "points")
-    end
-
-    if cb then
-        Leet.api.get(endpoint, {
-            callback = function(res, err)
-                if err then
-                    if spinner then
-                        spinner:error(err.msg)
-                    end
-                    return cb(nil, err)
-                end
-
-                local problems = Leet.api.problems.normalize(res.stat_status_pairs)
-
-                if config.is_cn then
-                    if spinner then
-                        spinner:update("fetching title translations")
-                    end
-                    M.translated_titles(function(titles, terr)
-                        if terr then
-                            if spinner then
-                                spinner:error(terr.msg)
-                            end
-                            return cb(nil, terr)
-                        end
-
-                        problems = Leet.api.translate_titles(problems, titles)
-                        if spinner then
-                            spinner:success("cache updated")
-                        end
-
-                        cb(problems)
-                    end)
-                else
-                    if spinner then
-                        spinner:success("cache updated")
-                    end
-
-                    cb(problems)
-                end
-            end,
-        })
-    else
-        local res, err = Leet.api.get(endpoint)
-        if err then
-            if spinner then
-                spinner:error(err.msg)
-            end
-            return nil, err
-        end
-
-        local problems = Leet.api.problems.normalize(res.stat_status_pairs)
-
-        if config.is_cn then
-            local titles, terr = M.translated_titles()
-            if terr then
-                if spinner then
-                    spinner:error(terr.msg)
-                end
-                return nil, terr
-            end
-
-            if spinner then
-                spinner:success("problems cache updated")
-            end
-            return Leet.api.translate_titles(problems, titles)
-        else
-            if spinner then
-                spinner:success("problems cache updated")
-            end
-            return problems
-        end
-    end
+	local endpoint = urls.problems:format("algorithms")
+
+	local spinner
+	if noti then
+		spinner = Spinner:start("updating problemlist cache", "points")
+	end
+
+	if cb then
+		Leet.api.get(endpoint, {
+			callback = function(res, err)
+				if err then
+					if spinner then
+						spinner:error(err.msg)
+					end
+					return cb(nil, err)
+				end
+
+				local problems = Leet.api.problems.normalize(res.stat_status_pairs)
+
+				if config.is_cn then
+					if spinner then
+						spinner:update("fetching title translations")
+					end
+					M.translated_titles(function(titles, terr)
+						if terr then
+							if spinner then
+								spinner:error(terr.msg)
+							end
+							return cb(nil, terr)
+						end
+
+						problems = Leet.api.translate_titles(problems, titles)
+						if spinner then
+							spinner:success("cache updated")
+						end
+
+						cb(problems)
+					end)
+				else
+					if spinner then
+						spinner:success("cache updated")
+					end
+
+					cb(problems)
+				end
+			end,
+		})
+	else
+		local res, err = Leet.api.get(endpoint)
+		if err then
+			if spinner then
+				spinner:error(err.msg)
+			end
+			return nil, err
+		end
+
+		local problems = Leet.api.problems.normalize(res.stat_status_pairs)
+
+		if config.is_cn then
+			local titles, terr = M.translated_titles()
+			if terr then
+				if spinner then
+					spinner:error(terr.msg)
+				end
+				return nil, terr
+			end
+
+			if spinner then
+				spinner:success("problems cache updated")
+			end
+			return Leet.api.translate_titles(problems, titles)
+		else
+			if spinner then
+				spinner:success("problems cache updated")
+			end
+			return problems
+		end
+	end
 end
 
 function M.question_of_today(cb)
-    local query = queries.qot
-
-    Leet.api.query(query, {}, {
-        callback = function(res, err)
-            if err then
-                return cb(nil, err)
-            end
-
-            local tday_record = res.data["todayRecord"]
-            local question = config.is_cn and tday_record[1].question or tday_record.question
-            cb(question)
-        end,
-    })
+	local query = queries.qot
+
+	Leet.api.query(query, {}, {
+		callback = function(res, err)
+			if err then
+				return cb(nil, err)
+			end
+
+			local tday_record = res.data["todayRecord"]
+			local question = config.is_cn and tday_record[1].question or tday_record.question
+			cb(question)
+		end,
+	})
 end
 
 function M.translated_titles(cb)
-    local query = queries.translations
-
-    if cb then
-        Leet.api.query(query, {}, {
-            callback = function(res, err)
-                if err then
-                    return cb(nil, err)
-                end
-                cb(res.data.translations)
-            end,
-        })
-    else
-        local res, err = utils.query(query, {})
-        if err then
-            return nil, err
-        end
-        return res.data.translations
-    end
+	local query = queries.translations
+
+	if cb then
+		Leet.api.query(query, {}, {
+			callback = function(res, err)
+				if err then
+					return cb(nil, err)
+				end
+				cb(res.data.translations)
+			end,
+		})
+	else
+		local res, err = utils.query(query, {})
+		if err then
+			return nil, err
+		end
+		return res.data.translations
+	end
 end
 
 ---@param filters? table
 function M.random(filters)
-    local variables = {
-        categorySlug = "algorithms",
-        filters = filters or vim.empty_dict(),
-    }
+	local variables = {
+		categorySlug = "algorithms",
+		filters = filters or vim.empty_dict(),
+	}
 
-    local query = queries.random_question
+	local query = queries.random_question
 
-    local res, err = Leet.api.query(query, variables)
-    if err then
-        return nil, err
-    end
+	local res, err = Leet.api.query(query, variables)
+	if err then
+		return nil, err
+	end
 
-    local q = res.data.randomQuestion
+	local q = res.data.randomQuestion
 
-    if q == vim.NIL then
-        local msg = "Random question fetch responded with `null`"
+	if q == vim.NIL then
+		local msg = "Random question fetch responded with `null`"
 
-        if filters then
-            msg = msg .. ".\n\nMaybe invalid filters?\n" .. vim.inspect(filters)
-        end
+		if filters then
+			msg = msg .. ".\n\nMaybe invalid filters?\n" .. vim.inspect(filters)
+		end
 
-        return nil, { msg = msg, lvl = vim.log.levels.ERROR }
-    end
+		return nil, { msg = msg, lvl = vim.log.levels.ERROR }
+	end
 
-    if config.is_cn then
-        q = {
-            title_slug = q,
-            paid_only = Leet.cache.problems.by_slug(q).paid_only,
-        }
-    end
+	if config.is_cn then
+		q = {
+			title_slug = q,
+			paid_only = Leet.cache.problems.by_slug(q).paid_only,
+		}
+	end
 
-    if not config.auth.is_premium and q.paid_only then
-        err = err or {}
-        err.msg = "Drawn question is for premium users only. Please try again"
-        err.lvl = vim.log.levels.WARN
-        return nil, err
-    end
+	if not config.auth.is_premium and q.paid_only then
+		err = err or {}
+		err.msg = "Drawn question is for premium users only. Please try again"
+		err.lvl = vim.log.levels.WARN
+		return nil, err
+	end
 
-    return q
+	return q
 end
 
 ---@param qid integer
 ---@param lang lc.lang
 ---@param cb function
 function M.latest_submission(qid, lang, cb)
-    local url = urls.latest_submission:format(qid, lang)
-    Leet.api.get(url, { callback = cb })
+	local url = urls.latest_submission:format(qid, lang)
+	Leet.api.get(url, { callback = cb })
 end
 
 ---@param title_slug string
 ---
 ---@return lc.question_res|nil
-function M.by_title_slug(title_slug)
-    local variables = {
-        titleSlug = title_slug,
-    }
-
-    local query = queries.question
-
-    local res, err = Leet.api.query(query, variables)
-    if not res or err then
-        return log.err(err)
-    end
-
-    local q = res.data.question
-    q.meta_data = vim.json.decode(q.meta_data)
-    q.stats = vim.json.decode(q.stats)
-    if type(q.similar) == "string" then
-        q.similar = Leet.api.normalize_similar_cn(q.similar)
-    end
-
-    return q
+function M.by_slug(title_slug)
+	local variables = {
+		titleSlug = title_slug,
+	}
+
+	local query = queries.question
+
+	local res, err = Leet.api.query(query, variables)
+	if not res or err then
+		return log.err(err)
+	end
+
+	local q = res.data.question
+	q.meta_data = vim.json.decode(q.meta_data)
+	q.stats = vim.json.decode(q.stats)
+	if type(q.similar) == "string" then
+		q.similar = Leet.api.normalize_similar_cn(q.similar)
+	end
+
+	return q
 end
 
----@return lc.cache.Question[]
+---@return leet.cache.problem[]
 function M.normalize(problems)
-    problems = vim.tbl_filter(function(p)
-        return not p.stat.question__hide
-    end, problems)
-
-    local comp = function(a, b)
-        local a_fid = a.stat.frontend_question_id
-        local b_fid = b.stat.frontend_question_id
-
-        local is_num_a = tonumber(a_fid)
-        local is_num_b = tonumber(b_fid)
-
-        if is_num_a and is_num_b then
-            return tonumber(a_fid) < tonumber(b_fid)
-        elseif is_num_a then
-            return true
-        elseif is_num_b then
-            return false
-        else
-            return a_fid < b_fid
-        end
-    end
-    table.sort(problems, comp)
-
-    return vim.tbl_map(function(p)
-        return {
-            status = p.status == vim.NIL and "todo" or p.status, -- api returns nil for todo
-            id = p.stat.question_id,
-            frontend_id = p.stat.frontend_question_id,
-            title = p.stat.question__title,
-            title_cn = "",
-            title_slug = p.stat.question__title_slug,
-            link = ("https://leetcode.%s/problems/%s/"):format(
-                config.domain,
-                p.stat.question__title_slug
-            ),
-            paid_only = p.paid_only,
-            ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1),
-            difficulty = ({ "Easy", "Medium", "Hard" })(p.difficulty.level),
-            starred = p.is_favor,
-            topic_tags = {},
-        }
-    end, problems)
+	problems = vim.tbl_filter(function(p)
+		return not p.stat.question__hide
+	end, problems)
+
+	local comp = function(a, b)
+		local a_fid = a.stat.frontend_question_id
+		local b_fid = b.stat.frontend_question_id
+
+		local is_num_a = tonumber(a_fid)
+		local is_num_b = tonumber(b_fid)
+
+		if is_num_a and is_num_b then
+			return tonumber(a_fid) < tonumber(b_fid)
+		elseif is_num_a then
+			return true
+		elseif is_num_b then
+			return false
+		else
+			return a_fid < b_fid
+		end
+	end
+	table.sort(problems, comp)
+
+	return vim.tbl_map(function(p)
+		return {
+			status = p.status == vim.NIL and "todo" or p.status, -- api returns nil for todo
+			id = p.stat.question_id,
+			frontend_id = p.stat.frontend_question_id,
+			title = p.stat.question__title,
+			title_cn = "",
+			title_slug = p.stat.question__title_slug,
+			link = ("https://leetcode.%s/problems/%s/"):format(
+				config.domain,
+				p.stat.question__title_slug
+			),
+			paid_only = p.paid_only,
+			ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1),
+			difficulty = ({ "Easy", "Medium", "Hard" })(p.difficulty.level),
+			starred = p.is_favor,
+			topic_tags = {},
+		}
+	end, problems)
 end
 
 return M
diff --git a/lua/leetcode/cache/cookie.lua b/lua/leetcode/cache/cookie.lua
index 97838526..6ff48979 100644
--- a/lua/leetcode/cache/cookie.lua
+++ b/lua/leetcode/cache/cookie.lua
@@ -66,13 +66,13 @@ function M.get()
 
     local contents = M.read()
     if not contents then
-        require("leetcode.command").delete_cookie()
+        require("leetcode.cmd").delete_cookie()
         return
     end
 
     local cookie = M.parse(contents)
     if not cookie then
-        require("leetcode.command").delete_cookie()
+        require("leetcode.cmd").delete_cookie()
         return
     end
 
diff --git a/lua/leetcode/cache/problems.lua b/lua/leetcode/cache/problems.lua
index 61c784ac..a145c2f3 100644
--- a/lua/leetcode/cache/problems.lua
+++ b/lua/leetcode/cache/problems.lua
@@ -11,7 +11,7 @@ local file = config.storage.cache:joinpath(("problemlist%s"):format(config.is_cn
 ---@type { at: integer, payload: lc.cache.payload }
 local hist = nil
 
----@class lc.cache.Question
+---@class leet.cache.problem
 ---@field id integer
 ---@field frontend_id string
 ---@field link string
@@ -27,7 +27,7 @@ local hist = nil
 ---@class leet.cache.problems
 local M = {}
 
----@return lc.cache.Question[]
+---@return leet.cache.problem[]
 function M.get()
     return M.read().data
 end
@@ -83,7 +83,7 @@ function M.update()
     end, true)
 end
 
----@return lc.cache.Question
+---@return leet.cache.problem
 function M.by_slug(slug)
     local problems = M.get()
 
@@ -111,7 +111,7 @@ function M.write(payload)
     hist = { at = os.time(), payload = payload }
 end
 
----@alias lc.cache.payload { version: string, data: lc.cache.Question[], updated_at: integer, username: string }
+---@alias lc.cache.payload { version: string, data: leet.cache.problem[], updated_at: integer, username: string }
 
 ---@param str string
 ---
diff --git a/lua/leetcode/command/arguments.lua b/lua/leetcode/cmd/arguments.lua
similarity index 100%
rename from lua/leetcode/command/arguments.lua
rename to lua/leetcode/cmd/arguments.lua
diff --git a/lua/leetcode/cmd/init.lua b/lua/leetcode/cmd/init.lua
new file mode 100644
index 00000000..61017ccd
--- /dev/null
+++ b/lua/leetcode/cmd/init.lua
@@ -0,0 +1,681 @@
+local log = require("leetcode.logger")
+local arguments = require("leetcode.cmd.arguments")
+local config = require("leetcode.config")
+local event = require("nui.utils.autocmd").event
+local api = vim.api
+
+local t = require("leetcode.translator")
+
+---@class leet.cmd
+local cmd = {}
+
+---@param old_name string
+---@param new_name string
+function cmd.deprecate(old_name, new_name)
+	log.warn(("`%s` is deprecated, use `%s` instead."):format(old_name, new_name))
+end
+
+function cmd.cache_update()
+	require("leetcode.util").auth_guard()
+
+	require("leetcode.cache").update()
+end
+
+---@param options table<string, string[]>
+function cmd.problems(options)
+	require("leetcode.util").auth_guard()
+
+	local p = require("leetcode.cache.problems").get()
+	local picker = require("leetcode.picker")
+	picker.question(p, options)
+end
+
+---@param cb? function
+function cmd.cookie_prompt(cb)
+	local cookie = require("leetcode.cache.cookie")
+
+	local popup_options = {
+		relative = "editor",
+		position = {
+			row = "50%",
+			col = "50%",
+		},
+		size = 40,
+		border = {
+			style = "rounded",
+			text = {
+				top = (" %s "):format(t("Enter cookie")),
+				top_align = "left",
+			},
+		},
+		win_options = {
+			winhighlight = "Normal:Normal",
+		},
+	}
+
+	local NuiInput = require("nui.input")
+	local input = NuiInput(popup_options, {
+		prompt = " 󰆘 ",
+		on_submit = function(value)
+			local err = cookie.set(value)
+
+			if not err then
+				log.info("Sign-in successful")
+				cmd.start_user_session()
+			else
+				log.error("Sign-in failed: " .. err)
+			end
+
+			pcall(cb, not err and true or false)
+		end,
+	})
+
+	input:mount()
+
+	local keys = config.user.keys
+	input:map("n", keys.toggle, function()
+		input:unmount()
+	end)
+	input:on(event.BufLeave, function()
+		input:unmount()
+	end)
+end
+
+function cmd.sign_out()
+	cmd.menu()
+
+	log.warn("You're now signed out")
+	cmd.delete_cookie()
+	cmd.set_menu_page("signin")
+end
+
+---Sign out
+function cmd.delete_cookie()
+	config.auth = {}
+	local cookie = require("leetcode.cache.cookie")
+	cookie.delete()
+end
+
+cmd.q_close_all = function()
+	local utils = require("leetcode.util")
+	local qs = utils.question_tabs()
+
+	for _, tabp in ipairs(qs) do
+		tabp.question:unmount()
+	end
+end
+
+function cmd.exit()
+	local leetcode = require("leetcode")
+	leetcode.stop()
+end
+
+cmd.expire = vim.schedule_wrap(function()
+	local tabp = api.nvim_get_current_tabpage()
+	cmd.menu()
+
+	cmd.cookie_prompt(function(success)
+		if success then
+			if api.nvim_tabpage_is_valid(tabp) then
+				api.nvim_set_current_tabpage(tabp)
+			end
+			log.info("Successful re-login")
+		else
+			cmd.sign_out()
+		end
+	end)
+end)
+
+function cmd.qot()
+	require("leetcode.util").auth_guard()
+
+	local problems = require("leetcode.api.problems")
+	local Question = require("leetcode-ui.question")
+
+	problems.question_of_today(function(qot, err)
+		if err then
+			return log.err(err)
+		end
+		local problemlist = require("leetcode.cache.problems")
+		Question(problemlist.by_slug(qot.title_slug)):mount()
+	end)
+end
+
+function cmd.random_question(opts)
+	require("leetcode.util").auth_guard()
+
+	local problems = require("leetcode.cache.problems")
+	local question = require("leetcode.api.question")
+
+	if opts and opts.difficulty then
+		opts.difficulty = opts.difficulty[1]:upper()
+	end
+	if opts and opts.status then
+		opts.status = ({
+			ac = "AC",
+			notac = "TRIED",
+			todo = "NOT_STARTED",
+		})[opts.status[1]]
+	end
+
+	local q, err = question.random(opts)
+	if err then
+		return log.err(err)
+	end
+
+	local item = problems.by_slug(q.title_slug) or {}
+	local Question = require("leetcode-ui.question")
+	Question(item):mount()
+end
+
+function cmd.start_with_cmd()
+	local leetcode = require("leetcode")
+	if leetcode.start(false) then
+		cmd.menu()
+	end
+end
+
+function cmd.menu()
+	if Leet.menu.win:valid() then
+		local _, tabp = pcall(api.nvim_win_get_tabpage, Leet.menu.win.win)
+		api.nvim_set_current_tabpage(tabp)
+		Leet.menu.win:focus()
+	else
+		Leet.menu.win:show()
+	end
+end
+
+function cmd.yank()
+	local utils = require("leetcode.util")
+	local q = utils.curr_question()
+	if not q then
+		return
+	end
+
+	q:editor_yank_code()
+end
+
+---@param page lc-menu.page
+function cmd.set_menu_page(page)
+	_Lc_state.menu:set_page(page)
+end
+
+function cmd.start_user_session() --
+	cmd.set_menu_page("menu")
+	config.stats.update()
+end
+
+function cmd.question_tabs()
+	local picker = require("leetcode.picker")
+	picker.tabs()
+end
+
+function cmd.change_lang()
+	local utils = require("leetcode.util")
+	local q = utils.curr_question()
+	if q then
+		local picker = require("leetcode.picker")
+		picker.language(q)
+	end
+end
+
+function cmd.desc_toggle()
+	local utils = require("leetcode.util")
+	local q = utils.curr_question()
+	if q then
+		q.description:toggle()
+	end
+end
+
+function cmd.desc_toggle_stats()
+	local utils = require("leetcode.util")
+	local q = utils.curr_question()
+	if q then
+		q.description:toggle_stats()
+	end
+end
+
+function cmd.console()
+	local utils = require("leetcode.util")
+	local q = utils.curr_question()
+	if q then
+		q.console:toggle()
+	end
+end
+
+function cmd.info()
+	local utils = require("leetcode.util")
+	local q = utils.curr_question()
+	if q then
+		q.info:toggle()
+	end
+end
+
+function cmd.hints()
+	-- cmd.deprecate("Leet hints", "Leet info")
+	cmd.info()
+end
+
+function cmd.q_run()
+	Leet.guard.auth()
+
+	local q = Leet.util.curr_question()
+	if q then
+		q.console:run()
+	end
+end
+
+function cmd.q_submit()
+	local utils = require("leetcode.util")
+	utils.auth_guard()
+	local q = utils.curr_question()
+	if q then
+		q.console:run(true)
+	end
+end
+
+function cmd.ui_skills()
+	if config.is_cn then
+		return
+	end
+	local skills = require("leetcode-ui.popup.skills")
+	skills:show()
+end
+
+function cmd.ui_languages()
+	local languages = require("leetcode-ui.popup.languages")
+	languages:show()
+end
+
+function cmd.current()
+	return Leet.util.curr_question()
+end
+
+---@param cached leet.cache.problem
+---@return boolean
+function cmd.open(cached)
+	local tabp = Leet.util.detect_duplicate_question(cached.title_slug, config.lang)
+
+	if tabp then
+		vim.api.nvim_set_current_tabpage(tabp)
+		return true
+	end
+
+	local res = Leet.api.problems.by_slug(cached.title_slug)
+	if not res or (res.is_paid_only and not config.auth.is_premium) then
+		Markup.log.warn("Problem is for premium users only")
+		return false
+	end
+
+	Leet.problem(cached, res)
+	return true
+end
+
+function cmd.browser_open()
+	local utils = require("leetcode.util")
+	utils.auth_guard()
+	local q = utils.curr_question()
+
+	if q then
+		if vim.ui.open then
+			vim.ui.open(q.cache.link)
+		else
+			local command
+			local os_name = vim.loop.os_uname().sysname
+
+			if os_name == "Linux" then
+				command = string.format("xdg-open '%s'", q.cache.link)
+			elseif os_name == "Darwin" then
+				command = string.format("open '%s'", q.cache.link)
+			else
+				-- Fallback to Windows if uname is not available or does not match Linux/Darwin.
+				command = string.format("start \"\" \"%s\"", q.cache.link)
+			end
+
+			vim.fn.jobstart(command, { detach = true })
+		end
+	end
+end
+
+function cmd.reset()
+	local utils = require("leetcode.util")
+	utils.auth_guard()
+	local q = utils.curr_question()
+	if not q then
+		return
+	end
+
+	q:editor_reset_code()
+end
+
+function cmd.last_submit()
+	local utils = require("leetcode.util")
+	utils.auth_guard()
+	local q = utils.curr_question()
+	if not q then
+		return
+	end
+
+	local question_api = require("leetcode.api.question")
+	question_api.latest_submission(q.q.id, q.lang, function(res, err) --
+		if err then
+			if err.status == 404 then
+				log.error("You haven't submitted any code!")
+			else
+				log.err(err)
+			end
+
+			return
+		end
+
+		if type(res) == "table" and res.code then
+			---@type string
+			local lines = res.code
+			q:editor_section_replace(lines, "code")
+		else
+			log.error("Something went wrong")
+		end
+	end)
+end
+
+function cmd.restore()
+	local utils = require("leetcode.util")
+	local ui = require("leetcode-ui.utils")
+	local q = utils.curr_question()
+	if not q then
+		return
+	end
+
+	if
+		(q.winid and api.nvim_win_is_valid(q.winid))
+		and (q.bufnr and api.nvim_buf_is_valid(q.bufnr))
+	then
+		ui.win_set_buf(q.winid, q.bufnr)
+	end
+
+	q.description:show()
+	local winid, bufnr = q.description.winid, q.description.bufnr
+
+	if
+		(winid and api.nvim_win_is_valid(winid)) --
+		and (bufnr and api.nvim_buf_is_valid(bufnr))
+	then
+		ui.win_set_buf(q.winid, q.bufnr)
+	end
+end
+
+function cmd.inject()
+	local utils = require("leetcode.util")
+	local q = utils.curr_question()
+	if not q then
+		return
+	end
+
+	if q.bufnr and api.nvim_buf_is_valid(q.bufnr) then
+		local range = q:editor_section_range("code")
+
+		if not range:is_valid_or_log() then
+			return
+		end
+
+		local lines = table.concat(range.lines, "\n", range.start_i, range.end_i)
+
+		q:editor_reset()
+		q:editor_section_replace(lines, "code")
+	end
+end
+
+function cmd.fold()
+	local utils = require("leetcode.util")
+	local q = utils.curr_question()
+	if not q then
+		return
+	end
+
+	q:editor_fold_imports(true)
+end
+
+function cmd.get_active_session()
+	local sessions = config.sessions.all
+	return vim.tbl_filter(function(s)
+		return s.is_active
+	end, sessions)[1]
+end
+
+function cmd.get_session_by_name(name)
+	local sessions = config.sessions.all
+
+	name = name:lower()
+	if name == config.sessions.default then
+		name = ""
+	end
+	return vim.tbl_filter(function(s)
+		return s.name:lower() == name
+	end, sessions)[1]
+end
+
+function cmd.change_session(opts)
+	require("leetcode.util").auth_guard()
+
+	local name = opts.name[1] or config.sessions.default
+
+	local session = cmd.get_session_by_name(name)
+	if not session then
+		return log.error("Session not found")
+	end
+
+	local stats_api = require("leetcode.api.stats")
+	stats_api.change_session(session.id, function(_, err)
+		if err then
+			return log.err(err)
+		end
+		log.info(("Session changed to `%s`"):format(name))
+		config.stats.update()
+	end)
+end
+
+function cmd.create_session(opts)
+	require("leetcode.util").auth_guard()
+
+	local name = opts.name[1]
+	if not name then
+		return log.error("Session name not provided")
+	end
+
+	local stats_api = require("leetcode.api.stats")
+	stats_api.create_session(name, function(_, err)
+		if err then
+			return log.err(err)
+		end
+		log.info(("session `%s` created"):format(name))
+	end)
+end
+
+function cmd.update_sessions()
+	require("leetcode.util").auth_guard()
+
+	config.stats.update_sessions()
+end
+
+function cmd.fix()
+	require("leetcode.cache.cookie").delete()
+	require("leetcode.cache.problems").delete()
+	vim.cmd("qa!")
+end
+
+---@return string[], string[]
+function cmd.parse(args)
+	local parts = vim.split(vim.trim(args), "%s+")
+	if args:sub(-1) == " " then
+		parts[#parts + 1] = ""
+	end
+
+	local options = {}
+	for _, part in ipairs(parts) do
+		local opt = part:match("(.-)=.-")
+		if opt then
+			table.insert(options, opt)
+		end
+	end
+
+	return parts, options
+end
+
+---@param tbl table
+local function cmds_keys(tbl)
+	return vim.tbl_filter(function(key)
+		if type(key) ~= "string" then
+			return false
+		end
+		if key:sub(1, 1) == "_" then
+			return false
+		end
+		if tbl[key]._private then
+			return false
+		end
+
+		return true
+	end, vim.tbl_keys(tbl))
+end
+
+---@param _ string
+---@param line string
+---
+---@return string[]
+function cmd.complete(_, line)
+	local args, options = cmd.parse(line:gsub("Leet%s", ""))
+	return cmd.rec_complete(args, options, cmd.commands)
+end
+
+---@param args string[]
+---@param options string[]
+---@param cmds table<string,any>
+---
+---@return string[]
+function cmd.rec_complete(args, options, cmds)
+	if not cmds or vim.tbl_isempty(args) then
+		return {}
+	end
+
+	if not cmds._args and cmds[args[1]] then
+		return cmd.rec_complete(args, options, cmds[table.remove(args, 1)])
+	end
+
+	local txt, keys = args[#args], cmds_keys(cmds)
+	if cmds._args then
+		local option_keys = cmds_keys(cmds._args)
+		option_keys = vim.tbl_filter(function(key)
+			return not vim.tbl_contains(options, key)
+		end, option_keys)
+		option_keys = vim.tbl_map(function(key)
+			return ("%s="):format(key)
+		end, option_keys)
+		keys = vim.tbl_extend("force", keys, option_keys)
+
+		local s = vim.split(txt, "=")
+		if s[2] and cmds._args[s[1]] then
+			local vals = vim.split(s[2], ",")
+			return vim.tbl_filter(function(key)
+				return not vim.tbl_contains(vals, key) and key:find(vals[#vals], 1, true) == 1
+			end, cmds._args[s[1]])
+		end
+	end
+
+	return vim.tbl_filter(function(key)
+		return not vim.tbl_contains(args, key) and key:find(txt, 1, true) == 1
+	end, keys)
+end
+
+function cmd.exec(args)
+	local cmds = cmd.commands
+
+	local options = vim.empty_dict()
+	for s in vim.gsplit(args.args:lower(), "%s+", { trimempty = true }) do
+		local opt = vim.split(s, "=")
+
+		if opt[2] then
+			options[opt[1]] = vim.split(opt[2], ",", { trimempty = true })
+		elseif cmds then
+			cmds = cmds[s]
+		else
+			break
+		end
+	end
+
+	if cmds and type(cmds[1]) == "function" then
+		cmds[1](options) ---@diagnostic disable-line
+	else
+		log.error(("Invalid command: `%s %s`"):format(args.name, args.args))
+	end
+end
+
+function cmd.setup()
+	api.nvim_create_user_command("Leet", cmd.exec, {
+		bar = true,
+		bang = true,
+		nargs = "?",
+		desc = "Leet",
+		complete = cmd.complete,
+	})
+end
+
+cmd.commands = {
+	cmd.menu,
+
+	menu = { cmd.menu },
+	exit = { cmd.exit },
+	console = { cmd.console },
+	info = { cmd.info },
+	hints = { cmd.hints },
+	tabs = { cmd.question_tabs },
+	lang = { cmd.change_lang },
+	run = { cmd.q_run },
+	test = { cmd.q_run },
+	submit = { cmd.q_submit },
+	daily = { cmd.qot },
+	yank = { cmd.yank },
+	open = { cmd.browser_open },
+	reset = { cmd.reset },
+	last_submit = { cmd.last_submit },
+	restore = { cmd.restore },
+	inject = { cmd.inject },
+	fold = { cmd.fold },
+	-- session = {
+	--     change = {
+	--         cmd.change_session,
+	--         _args = arguments.session_change,
+	--     },
+	--     create = {
+	--         cmd.create_session,
+	--         _args = arguments.session_create,
+	--     },
+	--     update = { cmd.update_sessions },
+	-- },
+	list = {
+		cmd.problems,
+		_args = arguments.list,
+	},
+	random = {
+		cmd.random_question,
+		_args = arguments.random,
+	},
+	desc = {
+		cmd.desc_toggle,
+
+		stats = { cmd.desc_toggle_stats },
+		toggle = { cmd.desc_toggle },
+	},
+	cookie = {
+		update = { cmd.cookie_prompt },
+		delete = { cmd.sign_out },
+	},
+	cache = {
+		update = { cmd.cache_update },
+	},
+	fix = {
+		cmd.fix,
+		_private = true,
+	},
+}
+
+return cmd
diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua
deleted file mode 100644
index 35129036..00000000
--- a/lua/leetcode/command/init.lua
+++ /dev/null
@@ -1,657 +0,0 @@
-local log = require("leetcode.logger")
-local arguments = require("leetcode.command.arguments")
-local config = require("leetcode.config")
-local event = require("nui.utils.autocmd").event
-local api = vim.api
-
-local t = require("leetcode.translator")
-
----@class lc.Commands
-local cmd = {}
-
----@param old_name string
----@param new_name string
-function cmd.deprecate(old_name, new_name)
-    log.warn(("`%s` is deprecated, use `%s` instead."):format(old_name, new_name))
-end
-
-function cmd.cache_update()
-    require("leetcode.util").auth_guard()
-
-    require("leetcode.cache").update()
-end
-
----@param options table<string, string[]>
-function cmd.problems(options)
-    require("leetcode.util").auth_guard()
-
-    local p = require("leetcode.cache.problems").get()
-    local picker = require("leetcode.picker")
-    picker.question(p, options)
-end
-
----@param cb? function
-function cmd.cookie_prompt(cb)
-    local cookie = require("leetcode.cache.cookie")
-
-    local popup_options = {
-        relative = "editor",
-        position = {
-            row = "50%",
-            col = "50%",
-        },
-        size = 40,
-        border = {
-            style = "rounded",
-            text = {
-                top = (" %s "):format(t("Enter cookie")),
-                top_align = "left",
-            },
-        },
-        win_options = {
-            winhighlight = "Normal:Normal",
-        },
-    }
-
-    local NuiInput = require("nui.input")
-    local input = NuiInput(popup_options, {
-        prompt = " 󰆘 ",
-        on_submit = function(value)
-            local err = cookie.set(value)
-
-            if not err then
-                log.info("Sign-in successful")
-                cmd.start_user_session()
-            else
-                log.error("Sign-in failed: " .. err)
-            end
-
-            pcall(cb, not err and true or false)
-        end,
-    })
-
-    input:mount()
-
-    local keys = config.user.keys
-    input:map("n", keys.toggle, function()
-        input:unmount()
-    end)
-    input:on(event.BufLeave, function()
-        input:unmount()
-    end)
-end
-
-function cmd.sign_out()
-    cmd.menu()
-
-    log.warn("You're now signed out")
-    cmd.delete_cookie()
-    cmd.set_menu_page("signin")
-end
-
----Sign out
-function cmd.delete_cookie()
-    config.auth = {}
-    local cookie = require("leetcode.cache.cookie")
-    cookie.delete()
-end
-
-cmd.q_close_all = function()
-    local utils = require("leetcode.util")
-    local qs = utils.question_tabs()
-
-    for _, tabp in ipairs(qs) do
-        tabp.question:unmount()
-    end
-end
-
-function cmd.exit()
-    local leetcode = require("leetcode")
-    leetcode.stop()
-end
-
-cmd.expire = vim.schedule_wrap(function()
-    local tabp = api.nvim_get_current_tabpage()
-    cmd.menu()
-
-    cmd.cookie_prompt(function(success)
-        if success then
-            if api.nvim_tabpage_is_valid(tabp) then
-                api.nvim_set_current_tabpage(tabp)
-            end
-            log.info("Successful re-login")
-        else
-            cmd.sign_out()
-        end
-    end)
-end)
-
-function cmd.qot()
-    require("leetcode.util").auth_guard()
-
-    local problems = require("leetcode.api.problems")
-    local Question = require("leetcode-ui.question")
-
-    problems.question_of_today(function(qot, err)
-        if err then
-            return log.err(err)
-        end
-        local problemlist = require("leetcode.cache.problems")
-        Question(problemlist.by_slug(qot.title_slug)):mount()
-    end)
-end
-
-function cmd.random_question(opts)
-    require("leetcode.util").auth_guard()
-
-    local problems = require("leetcode.cache.problems")
-    local question = require("leetcode.api.question")
-
-    if opts and opts.difficulty then
-        opts.difficulty = opts.difficulty[1]:upper()
-    end
-    if opts and opts.status then
-        opts.status = ({
-            ac = "AC",
-            notac = "TRIED",
-            todo = "NOT_STARTED",
-        })[opts.status[1]]
-    end
-
-    local q, err = question.random(opts)
-    if err then
-        return log.err(err)
-    end
-
-    local item = problems.by_slug(q.title_slug) or {}
-    local Question = require("leetcode-ui.question")
-    Question(item):mount()
-end
-
-function cmd.start_with_cmd()
-    local leetcode = require("leetcode")
-    if leetcode.start(false) then
-        cmd.menu()
-    end
-end
-
-function cmd.menu()
-    if Leet.menu.win:valid() then
-        local _, tabp = pcall(api.nvim_win_get_tabpage, Leet.menu.win.win)
-        api.nvim_set_current_tabpage(tabp)
-        Leet.menu.win:focus()
-    else
-        Leet.menu.win:show()
-    end
-end
-
-function cmd.yank()
-    local utils = require("leetcode.util")
-    local q = utils.curr_question()
-    if not q then
-        return
-    end
-
-    q:editor_yank_code()
-end
-
----@param page lc-menu.page
-function cmd.set_menu_page(page)
-    _Lc_state.menu:set_page(page)
-end
-
-function cmd.start_user_session() --
-    cmd.set_menu_page("menu")
-    config.stats.update()
-end
-
-function cmd.question_tabs()
-    local picker = require("leetcode.picker")
-    picker.tabs()
-end
-
-function cmd.change_lang()
-    local utils = require("leetcode.util")
-    local q = utils.curr_question()
-    if q then
-        local picker = require("leetcode.picker")
-        picker.language(q)
-    end
-end
-
-function cmd.desc_toggle()
-    local utils = require("leetcode.util")
-    local q = utils.curr_question()
-    if q then
-        q.description:toggle()
-    end
-end
-
-function cmd.desc_toggle_stats()
-    local utils = require("leetcode.util")
-    local q = utils.curr_question()
-    if q then
-        q.description:toggle_stats()
-    end
-end
-
-function cmd.console()
-    local utils = require("leetcode.util")
-    local q = utils.curr_question()
-    if q then
-        q.console:toggle()
-    end
-end
-
-function cmd.info()
-    local utils = require("leetcode.util")
-    local q = utils.curr_question()
-    if q then
-        q.info:toggle()
-    end
-end
-
-function cmd.hints()
-    -- cmd.deprecate("Leet hints", "Leet info")
-    cmd.info()
-end
-
-function cmd.q_run()
-    local utils = require("leetcode.util")
-    utils.auth_guard()
-    local q = utils.curr_question()
-    if q then
-        q.console:run()
-    end
-end
-
-function cmd.q_submit()
-    local utils = require("leetcode.util")
-    utils.auth_guard()
-    local q = utils.curr_question()
-    if q then
-        q.console:run(true)
-    end
-end
-
-function cmd.ui_skills()
-    if config.is_cn then
-        return
-    end
-    local skills = require("leetcode-ui.popup.skills")
-    skills:show()
-end
-
-function cmd.ui_languages()
-    local languages = require("leetcode-ui.popup.languages")
-    languages:show()
-end
-
-function cmd.open()
-    local utils = require("leetcode.util")
-    utils.auth_guard()
-    local q = utils.curr_question()
-
-    if q then
-        if vim.ui.open then
-            vim.ui.open(q.cache.link)
-        else
-            local command
-            local os_name = vim.loop.os_uname().sysname
-
-            if os_name == "Linux" then
-                command = string.format("xdg-open '%s'", q.cache.link)
-            elseif os_name == "Darwin" then
-                command = string.format("open '%s'", q.cache.link)
-            else
-                -- Fallback to Windows if uname is not available or does not match Linux/Darwin.
-                command = string.format("start \"\" \"%s\"", q.cache.link)
-            end
-
-            vim.fn.jobstart(command, { detach = true })
-        end
-    end
-end
-
-function cmd.reset()
-    local utils = require("leetcode.util")
-    utils.auth_guard()
-    local q = utils.curr_question()
-    if not q then
-        return
-    end
-
-    q:editor_reset_code()
-end
-
-function cmd.last_submit()
-    local utils = require("leetcode.util")
-    utils.auth_guard()
-    local q = utils.curr_question()
-    if not q then
-        return
-    end
-
-    local question_api = require("leetcode.api.question")
-    question_api.latest_submission(q.q.id, q.lang, function(res, err) --
-        if err then
-            if err.status == 404 then
-                log.error("You haven't submitted any code!")
-            else
-                log.err(err)
-            end
-
-            return
-        end
-
-        if type(res) == "table" and res.code then
-            ---@type string
-            local lines = res.code
-            q:editor_section_replace(lines, "code")
-        else
-            log.error("Something went wrong")
-        end
-    end)
-end
-
-function cmd.restore()
-    local utils = require("leetcode.util")
-    local ui = require("leetcode-ui.utils")
-    local q = utils.curr_question()
-    if not q then
-        return
-    end
-
-    if
-        (q.winid and api.nvim_win_is_valid(q.winid))
-        and (q.bufnr and api.nvim_buf_is_valid(q.bufnr))
-    then
-        ui.win_set_buf(q.winid, q.bufnr)
-    end
-
-    q.description:show()
-    local winid, bufnr = q.description.winid, q.description.bufnr
-
-    if
-        (winid and api.nvim_win_is_valid(winid)) --
-        and (bufnr and api.nvim_buf_is_valid(bufnr))
-    then
-        ui.win_set_buf(q.winid, q.bufnr)
-    end
-end
-
-function cmd.inject()
-    local utils = require("leetcode.util")
-    local q = utils.curr_question()
-    if not q then
-        return
-    end
-
-    if q.bufnr and api.nvim_buf_is_valid(q.bufnr) then
-        local range = q:editor_section_range("code")
-
-        if not range:is_valid_or_log() then
-            return
-        end
-
-        local lines = table.concat(range.lines, "\n", range.start_i, range.end_i)
-
-        q:editor_reset()
-        q:editor_section_replace(lines, "code")
-    end
-end
-
-function cmd.fold()
-    local utils = require("leetcode.util")
-    local q = utils.curr_question()
-    if not q then
-        return
-    end
-
-    q:editor_fold_imports(true)
-end
-
-function cmd.get_active_session()
-    local sessions = config.sessions.all
-    return vim.tbl_filter(function(s)
-        return s.is_active
-    end, sessions)[1]
-end
-
-function cmd.get_session_by_name(name)
-    local sessions = config.sessions.all
-
-    name = name:lower()
-    if name == config.sessions.default then
-        name = ""
-    end
-    return vim.tbl_filter(function(s)
-        return s.name:lower() == name
-    end, sessions)[1]
-end
-
-function cmd.change_session(opts)
-    require("leetcode.util").auth_guard()
-
-    local name = opts.name[1] or config.sessions.default
-
-    local session = cmd.get_session_by_name(name)
-    if not session then
-        return log.error("Session not found")
-    end
-
-    local stats_api = require("leetcode.api.stats")
-    stats_api.change_session(session.id, function(_, err)
-        if err then
-            return log.err(err)
-        end
-        log.info(("Session changed to `%s`"):format(name))
-        config.stats.update()
-    end)
-end
-
-function cmd.create_session(opts)
-    require("leetcode.util").auth_guard()
-
-    local name = opts.name[1]
-    if not name then
-        return log.error("Session name not provided")
-    end
-
-    local stats_api = require("leetcode.api.stats")
-    stats_api.create_session(name, function(_, err)
-        if err then
-            return log.err(err)
-        end
-        log.info(("session `%s` created"):format(name))
-    end)
-end
-
-function cmd.update_sessions()
-    require("leetcode.util").auth_guard()
-
-    config.stats.update_sessions()
-end
-
-function cmd.fix()
-    require("leetcode.cache.cookie").delete()
-    require("leetcode.cache.problems").delete()
-    vim.cmd("qa!")
-end
-
----@return string[], string[]
-function cmd.parse(args)
-    local parts = vim.split(vim.trim(args), "%s+")
-    if args:sub(-1) == " " then
-        parts[#parts + 1] = ""
-    end
-
-    local options = {}
-    for _, part in ipairs(parts) do
-        local opt = part:match("(.-)=.-")
-        if opt then
-            table.insert(options, opt)
-        end
-    end
-
-    return parts, options
-end
-
----@param tbl table
-local function cmds_keys(tbl)
-    return vim.tbl_filter(function(key)
-        if type(key) ~= "string" then
-            return false
-        end
-        if key:sub(1, 1) == "_" then
-            return false
-        end
-        if tbl[key]._private then
-            return false
-        end
-
-        return true
-    end, vim.tbl_keys(tbl))
-end
-
----@param _ string
----@param line string
----
----@return string[]
-function cmd.complete(_, line)
-    local args, options = cmd.parse(line:gsub("Leet%s", ""))
-    return cmd.rec_complete(args, options, cmd.commands)
-end
-
----@param args string[]
----@param options string[]
----@param cmds table<string,any>
----
----@return string[]
-function cmd.rec_complete(args, options, cmds)
-    if not cmds or vim.tbl_isempty(args) then
-        return {}
-    end
-
-    if not cmds._args and cmds[args[1]] then
-        return cmd.rec_complete(args, options, cmds[table.remove(args, 1)])
-    end
-
-    local txt, keys = args[#args], cmds_keys(cmds)
-    if cmds._args then
-        local option_keys = cmds_keys(cmds._args)
-        option_keys = vim.tbl_filter(function(key)
-            return not vim.tbl_contains(options, key)
-        end, option_keys)
-        option_keys = vim.tbl_map(function(key)
-            return ("%s="):format(key)
-        end, option_keys)
-        keys = vim.tbl_extend("force", keys, option_keys)
-
-        local s = vim.split(txt, "=")
-        if s[2] and cmds._args[s[1]] then
-            local vals = vim.split(s[2], ",")
-            return vim.tbl_filter(function(key)
-                return not vim.tbl_contains(vals, key) and key:find(vals[#vals], 1, true) == 1
-            end, cmds._args[s[1]])
-        end
-    end
-
-    return vim.tbl_filter(function(key)
-        return not vim.tbl_contains(args, key) and key:find(txt, 1, true) == 1
-    end, keys)
-end
-
-function cmd.exec(args)
-    local cmds = cmd.commands
-
-    local options = vim.empty_dict()
-    for s in vim.gsplit(args.args:lower(), "%s+", { trimempty = true }) do
-        local opt = vim.split(s, "=")
-
-        if opt[2] then
-            options[opt[1]] = vim.split(opt[2], ",", { trimempty = true })
-        elseif cmds then
-            cmds = cmds[s]
-        else
-            break
-        end
-    end
-
-    if cmds and type(cmds[1]) == "function" then
-        cmds[1](options) ---@diagnostic disable-line
-    else
-        log.error(("Invalid command: `%s %s`"):format(args.name, args.args))
-    end
-end
-
-function cmd.setup()
-    api.nvim_create_user_command("Leet", cmd.exec, {
-        bar = true,
-        bang = true,
-        nargs = "?",
-        desc = "Leet",
-        complete = cmd.complete,
-    })
-end
-
-cmd.commands = {
-    cmd.menu,
-
-    menu = { cmd.menu },
-    exit = { cmd.exit },
-    console = { cmd.console },
-    info = { cmd.info },
-    hints = { cmd.hints },
-    tabs = { cmd.question_tabs },
-    lang = { cmd.change_lang },
-    run = { cmd.q_run },
-    test = { cmd.q_run },
-    submit = { cmd.q_submit },
-    daily = { cmd.qot },
-    yank = { cmd.yank },
-    open = { cmd.open },
-    reset = { cmd.reset },
-    last_submit = { cmd.last_submit },
-    restore = { cmd.restore },
-    inject = { cmd.inject },
-    fold = { cmd.fold },
-    -- session = {
-    --     change = {
-    --         cmd.change_session,
-    --         _args = arguments.session_change,
-    --     },
-    --     create = {
-    --         cmd.create_session,
-    --         _args = arguments.session_create,
-    --     },
-    --     update = { cmd.update_sessions },
-    -- },
-    list = {
-        cmd.problems,
-        _args = arguments.list,
-    },
-    random = {
-        cmd.random_question,
-        _args = arguments.random,
-    },
-    desc = {
-        cmd.desc_toggle,
-
-        stats = { cmd.desc_toggle_stats },
-        toggle = { cmd.desc_toggle },
-    },
-    cookie = {
-        update = { cmd.cookie_prompt },
-        delete = { cmd.sign_out },
-    },
-    cache = {
-        update = { cmd.cache_update },
-    },
-    fix = {
-        cmd.fix,
-        _private = true,
-    },
-}
-
-return cmd
diff --git a/lua/leetcode/config/init.lua b/lua/leetcode/config/init.lua
index adce46f4..abad4a11 100644
--- a/lua/leetcode/config/init.lua
+++ b/lua/leetcode/config/init.lua
@@ -2,133 +2,133 @@ local template = require("leetcode.config.template")
 local P = require("plenary.path")
 
 _Lc_state = {
-    menu = nil, ---@type lc.ui.Menu
-    questions = {}, ---@type lc.ui.Question[]
+	menu = nil, ---@type lc.ui.Menu
+	questions = {}, ---@type lc.ui.Question[]
 }
 
 local lazy_plugs = {}
 
----@class lc.Config
+---@class leet.config
 local config = {
-    default = template,
-    user = template,
-
-    name = "leetcode.nvim",
-    domain = "com",
-    is_cn = false,
-    debug = false,
-    lang = "cpp",
-    version = "1.0.1",
-    storage = {}, ---@type table<string, Path>
-    theme = {}, ---@type lc.highlights
-    plugins = {},
-
-    translator = false,
-
-    langs = require("leetcode.config.langs"),
-    icons = require("leetcode.config.icons"),
-    sessions = require("leetcode.config.sessions"),
-    stats = require("leetcode.config.stats"),
-    imports = require("leetcode.config.imports"),
-    hooks = require("leetcode.config.hooks"),
-
-    ---@type lc.UserStatus
-    auth = {}, ---@diagnostic disable-line
+	default = template,
+	user = template,
+
+	name = "leetcode.nvim",
+	domain = "com",
+	is_cn = false,
+	debug = false,
+	lang = "cpp",
+	version = "1.0.1",
+	storage = {}, ---@type table<string, Path>
+	theme = {}, ---@type lc.highlights
+	plugins = {},
+
+	translator = false,
+
+	langs = require("leetcode.config.langs"),
+	icons = require("leetcode.config.icons"),
+	sessions = require("leetcode.config.sessions"),
+	stats = require("leetcode.config.stats"),
+	imports = require("leetcode.config.imports"),
+	hooks = require("leetcode.config.hooks"),
+
+	---@type lc.UserStatus
+	auth = {}, ---@diagnostic disable-line
 }
 
 ---Merge configurations into default configurations and set it as user configurations.
 ---
 ---@param cfg lc.UserConfig Configurations to be merged.
 function config.apply(cfg)
-    config.user = vim.tbl_deep_extend("force", config.default, cfg or {})
-    config.load_plugins()
+	config.user = vim.tbl_deep_extend("force", config.default, cfg or {})
+	config.load_plugins()
 end
 
 function config.setup()
-    config.validate()
+	config.validate()
 
-    -- deprecate `directory` config
-    if config.user.directory then
-        local log = require("leetcode.logger")
-        log.warn("leetcode.nvim config: `directory` is deprecated. Use `storage.home` instead.")
-        config.user.storage.home = config.user.directory
-    end
+	-- deprecate `directory` config
+	if config.user.directory then
+		local log = require("leetcode.logger")
+		log.warn("leetcode.nvim config: `directory` is deprecated. Use `storage.home` instead.")
+		config.user.storage.home = config.user.directory
+	end
 
-    config.user.storage = vim.tbl_map(vim.fn.expand, config.user.storage)
+	config.user.storage = vim.tbl_map(vim.fn.expand, config.user.storage)
 
-    config.debug = config.user.debug or false ---@diagnostic disable-line
-    config.lang = config.user.lang
+	config.debug = config.user.debug or false ---@diagnostic disable-line
+	config.lang = config.user.lang
 
-    config.storage.home = P:new(config.user.storage.home) ---@diagnostic disable-line
-    config.storage.home:mkdir()
+	config.storage.home = P:new(config.user.storage.home) ---@diagnostic disable-line
+	config.storage.home:mkdir()
 
-    config.storage.cache = P:new(config.user.storage.cache) ---@diagnostic disable-line
-    config.storage.cache:mkdir()
+	config.storage.cache = P:new(config.user.storage.cache) ---@diagnostic disable-line
+	config.storage.cache:mkdir()
 
-    for _, plug_load_fn in ipairs(lazy_plugs) do
-        plug_load_fn()
-    end
+	for _, plug_load_fn in ipairs(lazy_plugs) do
+		plug_load_fn()
+	end
 end
 
 function config.validate()
-    local utils = require("leetcode.util")
-
-    assert(vim.fn.has("nvim-0.9.0") == 1, "Neovim >= 0.9.0 required")
-
-    if not utils.get_lang(config.lang) then
-        ---@type lc.lang[]
-        local lang_slugs = vim.tbl_map(function(lang)
-            return lang.slug
-        end, config.langs)
-
-        local matches = {}
-        for _, slug in ipairs(lang_slugs) do
-            local percent = slug:match(config.lang) or config.lang:match(slug)
-            if percent then
-                table.insert(matches, slug)
-            end
-        end
-
-        if not vim.tbl_isempty(matches) then
-            local log = require("leetcode.logger")
-            log.warn("Did you mean: { " .. table.concat(matches, ", ") .. " }?")
-        end
-
-        error("Unsupported Language: " .. config.lang)
-    end
+	local utils = require("leetcode.util")
+
+	assert(vim.fn.has("nvim-0.9.0") == 1, "Neovim >= 0.9.0 required")
+
+	if not utils.get_lang(config.lang) then
+		---@type lc.lang[]
+		local lang_slugs = vim.tbl_map(function(lang)
+			return lang.slug
+		end, config.langs)
+
+		local matches = {}
+		for _, slug in ipairs(lang_slugs) do
+			local percent = slug:match(config.lang) or config.lang:match(slug)
+			if percent then
+				table.insert(matches, slug)
+			end
+		end
+
+		if not vim.tbl_isempty(matches) then
+			local log = require("leetcode.logger")
+			log.warn("Did you mean: { " .. table.concat(matches, ", ") .. " }?")
+		end
+
+		error("Unsupported Language: " .. config.lang)
+	end
 end
 
 function config.load_plugins()
-    config.plugins = {}
-    local plugins = {}
-
-    if config.user.cn.enabled then
-        table.insert(plugins, "cn")
-    end
-
-    for plugin, enabled in pairs(config.user.plugins) do
-        if enabled then
-            table.insert(plugins, plugin)
-        end
-    end
-
-    for _, plugin in ipairs(plugins) do
-        local ok, plug = pcall(require, "leetcode-plugins." .. plugin)
-
-        if ok then
-            if not (plug.opts or {}).lazy then
-                plug.load()
-            else
-                table.insert(lazy_plugs, plug.load)
-            end
-            config.plugins[plugin] = true
-        else
-            table.insert(lazy_plugs, function()
-                local log = require("leetcode.logger")
-                log.error(plug)
-            end)
-        end
-    end
+	config.plugins = {}
+	local plugins = {}
+
+	if config.user.cn.enabled then
+		table.insert(plugins, "cn")
+	end
+
+	for plugin, enabled in pairs(config.user.plugins) do
+		if enabled then
+			table.insert(plugins, plugin)
+		end
+	end
+
+	for _, plugin in ipairs(plugins) do
+		local ok, plug = pcall(require, "leetcode-plugins." .. plugin)
+
+		if ok then
+			if not (plug.opts or {}).lazy then
+				plug.load()
+			else
+				table.insert(lazy_plugs, plug.load)
+			end
+			config.plugins[plugin] = true
+		else
+			table.insert(lazy_plugs, function()
+				local log = require("leetcode.logger")
+				log.error(plug)
+			end)
+		end
+	end
 end
 
 return config
diff --git a/lua/leetcode/guard.lua b/lua/leetcode/guard.lua
index d9d48418..39630ab4 100644
--- a/lua/leetcode/guard.lua
+++ b/lua/leetcode/guard.lua
@@ -1,14 +1,12 @@
+---@class leet.guard
 local M = {}
 
 function M.auth()
-    local auth_api = require("leetcode.api.auth")
-    local _, aerr = auth_api.user()
-
-    if aerr then
-        return aerr.msg
-    end
-
-    return true
+	local config = require("leetcode.config")
+	if not config.auth.is_signed_in then
+		Markup.log.error("You need to sign in to access this feature.")
+		return false
+	end
 end
 
 -- ---@param problem leet.res.problem
diff --git a/lua/leetcode/init.lua b/lua/leetcode/init.lua
index 54293f3b..764fbac0 100644
--- a/lua/leetcode/init.lua
+++ b/lua/leetcode/init.lua
@@ -1,149 +1,165 @@
+---@module 'markup'
+
 local config = require("leetcode.config")
 
 ---@class leet
 local M = {}
 
-_G.Leet = { ctx = {} }
+---@class leet.global
+---@field active_problems leet.problem[]
+---@field api leet.api
+---@field cache leet.cache
+---@field cmd leet.cmd
+---@field problem leet.problem
+---@field guard leet.guard
+---@field util leet.util
+---@field config leet.config
+_G.Leet = { ctx = {}, active_problems = {} }
 
 local function log_failed_to_init()
-    local log = require("leetcode.logger")
-    log.warn("Failed to initialize: `neovim` contains listed buffers")
+	local log = require("leetcode.logger")
+	log.warn("Failed to initialize: `neovim` contains listed buffers")
 end
 
 local function log_buf_not_empty(bufname)
-    local log = require("leetcode.logger")
-    if bufname and bufname ~= "" then
-        log.warn(("Failed to initialize: `%s` is not an empty buffer"):format(bufname))
-    else
-        log.warn("Failed to initialize: not an empty buffer")
-    end
+	local log = require("leetcode.logger")
+	if bufname and bufname ~= "" then
+		log.warn(("Failed to initialize: `%s` is not an empty buffer"):format(bufname))
+	else
+		log.warn("Failed to initialize: not an empty buffer")
+	end
 end
 
 local function buf_is_empty(buf)
-    local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, true)
-    return not (#lines > 1 or (#lines == 1 and lines[1]:len() > 0))
+	local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, true)
+	return not (#lines > 1 or (#lines == 1 and lines[1]:len() > 0))
 end
 
 ---@param on_vimenter boolean
 ---
 ---@return boolean, boolean? (skip, standalone)
 function M.should_skip(on_vimenter)
-    if on_vimenter then
-        if vim.fn.argc(-1) ~= 1 then
-            return true
-        end
-
-        local usr_arg, arg = config.user.arg, vim.fn.argv(0, -1)
-        if usr_arg ~= arg then
-            return true
-        end
-
-        if not buf_is_empty(0) then
-            log_buf_not_empty(usr_arg)
-            return true
-        end
-
-        return false, true
-    else
-        local listed_bufs = vim.tbl_filter(function(info)
-            return info.listed == 1
-        end, vim.fn.getbufinfo())
-
-        if #listed_bufs == 0 then
-            return false, true
-        elseif vim.fn.argc(-1) == 0 and #listed_bufs == 1 then
-            local buf = listed_bufs[1]
-
-            if vim.api.nvim_get_current_buf() ~= buf.bufnr then
-                if config.plugins.non_standalone then
-                    return false, false
-                else
-                    log_failed_to_init()
-                    return true
-                end
-            end
-
-            vim.schedule(function()
-                if buf.changed == 1 then
-                    vim.api.nvim_buf_delete(buf.bufnr, { force = true })
-                end
-            end)
-
-            return false, true
-        elseif #listed_bufs >= 1 then
-            if config.plugins.non_standalone then
-                return false, false
-            else
-                log_failed_to_init()
-                return true
-            end
-        end
-    end
+	if on_vimenter then
+		if vim.fn.argc(-1) ~= 1 then
+			return true
+		end
+
+		local usr_arg, arg = config.user.arg, vim.fn.argv(0, -1)
+		if usr_arg ~= arg then
+			return true
+		end
+
+		if not buf_is_empty(0) then
+			log_buf_not_empty(usr_arg)
+			return true
+		end
+
+		return false, true
+	else
+		local listed_bufs = vim.tbl_filter(function(info)
+			return info.listed == 1
+		end, vim.fn.getbufinfo())
+
+		if #listed_bufs == 0 then
+			return false, true
+		elseif vim.fn.argc(-1) == 0 and #listed_bufs == 1 then
+			local buf = listed_bufs[1]
+
+			if vim.api.nvim_get_current_buf() ~= buf.bufnr then
+				if config.plugins.non_standalone then
+					return false, false
+				else
+					log_failed_to_init()
+					return true
+				end
+			end
+
+			vim.schedule(function()
+				if buf.changed == 1 then
+					vim.api.nvim_buf_delete(buf.bufnr, { force = true })
+				end
+			end)
+
+			return false, true
+		elseif #listed_bufs >= 1 then
+			if config.plugins.non_standalone then
+				return false, false
+			else
+				log_failed_to_init()
+				return true
+			end
+		end
+	end
 end
 
 function M.setup_cmds()
-    require("leetcode.command").setup()
+	require("leetcode.cmd").setup()
 end
 
 ---@param on_vimenter boolean
 function M.start(on_vimenter)
-    local skip = M.should_skip(on_vimenter)
-    if skip then
-        return false
-    end
+	local skip = M.should_skip(on_vimenter)
+	if skip then
+		return false
+	end
 
-    config.setup()
+	config.setup()
 
-    _G.Leet.util = require("leetcode.util")
-    _G.Leet.api = require("leetcode.api")
-    _G.Leet.cache = require("leetcode.cache")
-    _G.Leet.guard = require("leetcode.guard")
+	_G.Leet = setmetatable({
+		ctx = {},
+		active_problems = {},
+	}, {
+		__index = function(t, k)
+			t[k] = require("leetcode." .. k)
+			return rawget(t, k)
+		end,
+	})
 
-    vim.api.nvim_set_current_dir(config.storage.home:absolute())
+	vim.api.nvim_set_current_dir(config.storage.home:absolute())
 
-    M.setup_cmds()
+	M.setup_cmds()
 
-    local theme = require("leetcode.theme")
-    theme.setup()
+	local theme = require("leetcode.theme")
+	theme.setup()
 
-    if not on_vimenter then
-        vim.cmd.enew()
-    end
+	if not on_vimenter then
+		vim.cmd.enew()
+	end
 
-    local menu = require("leetcode.ui.menu")
-    menu.win.buf = vim.api.nvim_get_current_buf()
-    menu.win:show()
-    menu:render()
+	local menu = require("leetcode.ui.menu")
+	menu.win.buf = vim.api.nvim_get_current_buf()
+	menu.win:show()
+	menu:render()
 
-    local utils = require("leetcode.util")
-    utils.exec_hooks("enter")
+	local utils = require("leetcode.util")
+	utils.exec_hooks("enter")
 
-    return true
+	return true
 end
 
 function M.stop()
-    vim.cmd("qa!")
+	vim.cmd("qa!")
 end
 
 ---@param cfg? lc.UserConfig
 function M.setup(cfg)
-    config.apply(cfg or {})
-
-    vim.api.nvim_create_user_command("Leet", require("leetcode.command").start_with_cmd, {
-        bar = true,
-        bang = true,
-        desc = "Open leetcode.nvim",
-    })
-
-    local group_id = vim.api.nvim_create_augroup("leetcode_start", { clear = true })
-    vim.api.nvim_create_autocmd("VimEnter", {
-        group = group_id,
-        pattern = "*",
-        nested = true,
-        callback = function()
-            M.start(true)
-        end,
-    })
+	config.apply(cfg or {})
+
+	vim.api.nvim_create_user_command("Leet", require("leetcode.cmd").start_with_cmd, {
+		bar = true,
+		bang = true,
+		desc = "Open leetcode.nvim",
+	})
+
+	local group_id = vim.api.nvim_create_augroup("leetcode_start", { clear = true })
+	vim.api.nvim_create_autocmd("VimEnter", {
+		group = group_id,
+		pattern = "*",
+		nested = true,
+		callback = function()
+			M.start(true)
+		end,
+	})
 end
 
 return M
diff --git a/lua/leetcode/picker/question/init.lua b/lua/leetcode/picker/question/init.lua
index aa24400b..49ddc79a 100644
--- a/lua/leetcode/picker/question/init.lua
+++ b/lua/leetcode/picker/question/init.lua
@@ -11,92 +11,90 @@ local P = {}
 P.width = 100
 P.height = 0.6
 
----@param items lc.cache.Question[]
+---@param items leet.cache.problem[]
 ---@param opts table<string, string[]>
 ---
----@return lc.cache.Question[]
+---@return leet.cache.problem[]
 function P.filter(items, opts)
-    if vim.tbl_isempty(opts or {}) then
-        return items
-    end
-
-    ---@param q lc.cache.Question
-    return vim.tbl_filter(function(q)
-        local diff_flag = true
-        if opts.difficulty and not vim.tbl_contains(opts.difficulty, q.difficulty:lower()) then
-            diff_flag = false
-        end
-
-        local status_flag = true
-        if opts.status and not vim.tbl_contains(opts.status, q.status) then
-            status_flag = false
-        end
-
-        return diff_flag and status_flag
-    end, items)
+	if vim.tbl_isempty(opts or {}) then
+		return items
+	end
+
+	---@param q leet.cache.problem
+	return vim.tbl_filter(function(q)
+		local diff_flag = true
+		if opts.difficulty and not vim.tbl_contains(opts.difficulty, q.difficulty:lower()) then
+			diff_flag = false
+		end
+
+		local status_flag = true
+		if opts.status and not vim.tbl_contains(opts.status, q.status) then
+			status_flag = false
+		end
+
+		return diff_flag and status_flag
+	end, items)
 end
 
----@param content lc.cache.Question[]
+---@param content leet.cache.problem[]
 ---@param opts table<string, string[]>
 ---
----@return { entry: any, value: lc.cache.Question }[]
+---@return { entry: any, value: leet.cache.problem }[]
 function P.items(content, opts)
-    local filtered = P.filter(content, opts)
-    return vim.tbl_map(function(item)
-        return { entry = P.entry(item), value = item }
-    end, filtered)
+	local filtered = P.filter(content, opts)
+	return vim.tbl_map(function(item)
+		return { entry = P.entry(item), value = item }
+	end, filtered)
 end
 
----@param question lc.cache.Question
+---@param question leet.cache.problem
 local function display_user_status(question)
-    if question.paid_only then
-        return config.auth.is_premium and config.icons.hl.unlock or config.icons.hl.lock
-    end
+	if question.paid_only then
+		return config.auth.is_premium and config.icons.hl.unlock or config.icons.hl.lock
+	end
 
-    return config.icons.hl.status[question.status] or { " " }
+	return config.icons.hl.status[question.status] or { " " }
 end
 
----@param question lc.cache.Question
+---@param question leet.cache.problem
 local function display_difficulty(question)
-    local hl = ui_utils.diff_to_hl(question.difficulty)
-    return { config.icons.square, hl }
+	local hl = ui_utils.diff_to_hl(question.difficulty)
+	return { config.icons.square, hl }
 end
 
----@param question lc.cache.Question
+---@param question leet.cache.problem
 local function display_question(question)
-    local index = { question.frontend_id .. ".", "leetcode_normal" }
-    local title = { utils.translate(question.title, question.title_cn) }
-    local ac_rate = { ("(%.1f%%)"):format(question.ac_rate), "leetcode_ref" }
+	local index = { question.frontend_id .. ".", "leetcode_normal" }
+	local title = { utils.translate(question.title, question.title_cn) }
+	local ac_rate = { ("(%.1f%%)"):format(question.ac_rate), "leetcode_ref" }
 
-    return unpack({ index, title, ac_rate })
+	return unpack({ index, title, ac_rate })
 end
 
 function P.entry(item)
-    return {
-        display_user_status(item),
-        display_difficulty(item),
-        display_question(item),
-    }
+	return {
+		display_user_status(item),
+		display_difficulty(item),
+		display_question(item),
+	}
 end
 
----@param item lc.cache.Question
+---@param item leet.cache.problem
 function P.ordinal(item)
-    return ("%s. %s %s %s"):format(
-        tostring(item.frontend_id),
-        item.title,
-        item.title_cn,
-        item.title_slug
-    )
+	return ("%s. %s %s %s"):format(
+		tostring(item.frontend_id),
+		item.title,
+		item.title_cn,
+		item.title_slug
+	)
 end
 
 function P.select(selection, close)
-    if selection.paid_only and not config.auth.is_premium then
-        return log.warn("Question is for premium users only")
-    end
-    if close then
-        close()
-    end
-    Problem(selection):show()
+	local did_open = Leet.cmd.open(selection)
+
+	if did_open and close then
+		close()
+	end
 end
 
 return P
diff --git a/lua/leetcode/picker/question/snacks.lua b/lua/leetcode/picker/question/snacks.lua
index a4e527b7..2d0544e5 100644
--- a/lua/leetcode/picker/question/snacks.lua
+++ b/lua/leetcode/picker/question/snacks.lua
@@ -4,7 +4,7 @@ local question_picker = require("leetcode.picker.question")
 
 local picker = require("snacks.picker")
 
----@param questions lc.cache.Question[]
+---@param questions leet.cache.problem[]
 return function(questions, opts)
     local items = question_picker.items(questions, opts)
     local finder_items = {}
diff --git a/lua/leetcode/picker/question/telescope.lua b/lua/leetcode/picker/question/telescope.lua
index b30137d6..298f7c4e 100644
--- a/lua/leetcode/picker/question/telescope.lua
+++ b/lua/leetcode/picker/question/telescope.lua
@@ -41,7 +41,7 @@ local theme = require("telescope.themes").get_dropdown({
     },
 })
 
----@param questions lc.cache.Question[]
+---@param questions leet.cache.problem[]
 return function(questions, opts)
     local items = question_picker.items(questions, opts)
 
diff --git a/lua/leetcode/problem/console/init.lua b/lua/leetcode/problem/console/init.lua
index 196cb9ab..7234b20f 100644
--- a/lua/leetcode/problem/console/init.lua
+++ b/lua/leetcode/problem/console/init.lua
@@ -1,37 +1,53 @@
 local Result = require("leetcode.problem.console.result")
 local Testcase = require("leetcode.problem.console.testcase")
+local Runner = require("leetcode.problem.runner")
 
 ---@class leet.problem.console
+---@field result leet.problem.console.result
+---@field testcase leet.problem.console.testcase
 local M = Markup.object()
 
 ---@param problem leet.problem
 function M:new(problem)
-    self.question = problem
-
-    self.result = Result()
-    self.testcase = Testcase()
-
-    self.layout = Markup.layout({
-        wins = {
-            ["result"] = self.result.win,
-            ["testcase"] = self.testcase.win,
-        },
-        layout = {
-            box = "horizontal",
-            focusable = true,
-            position = "bottom",
-            height = 0.3,
-            { win = "testcase", width = 0.4 },
-            { win = "result" },
-        },
-    })
-    -- self.renderer = Markup.renderer({
-    --     relative = "editor",
-    --     position = "bottom",
-    --     height = 0.3,
-    --     enter = false,
-    --     focusable = true,
-    -- })
+	self.problem = problem
+
+	self.result = Result(self)
+	self.testcase = Testcase(self)
+
+	self.layout = Markup.layout({
+		wins = {
+			["result"] = self.result.win,
+			["testcase"] = self.testcase.win,
+		},
+		show = false,
+		layout = {
+			box = "horizontal",
+			focusable = true,
+			position = "bottom",
+			height = 0.3,
+			{ win = "testcase", width = 0.4 },
+			{ win = "result" },
+		},
+	})
+end
+
+function M:run(submit)
+	local range = self.problem:editor_section_range("code")
+	if not range:is_valid_or_log() then
+		return
+	end
+
+	if Leet.config.user.console.open_on_runcode then
+		self.layout:show()
+	end
+
+	self.result.win:focus()
+
+	Runner.run(self.problem, submit)
+end
+
+function M:snapshot(id, res)
+
 end
 
 return M
diff --git a/lua/leetcode/problem/console/result.lua b/lua/leetcode/problem/console/result.lua
index c6c2ca94..4335c74f 100644
--- a/lua/leetcode/problem/console/result.lua
+++ b/lua/leetcode/problem/console/result.lua
@@ -1,13 +1,33 @@
+local m = require("markup")
+local Result = require("leetcode.ui.problem.result")
+
 ---@class leet.problem.console.result
+---@overload fun(): leet.problem.console.result
 local M = Markup.object()
 
-function M:new()
+---@param console leet.problem.console
+function M:new(console)
+    self.console = console
     self.win = Markup.window({
         box = "horizontal",
         title = "Results",
         border = "rounded",
         show = false,
     })
+    -- self.win = self.renderer.win
+end
+
+function M:clear()
+    vim.api.nvim_buf_set_lines(self.win.buf, 0, -1, false, {})
+end
+
+function M:handle(item)
+    -- self.renderer:render({
+    -- 	m.block("123")
+    -- })
+    local renderer = Markup.renderer({ show = false })
+    renderer.win:show()
+    renderer:render(Result({ item = item, console = self.console }))
 end
 
 return M
diff --git a/lua/leetcode/problem/console/testcase.lua b/lua/leetcode/problem/console/testcase.lua
index c207f73a..c647519f 100644
--- a/lua/leetcode/problem/console/testcase.lua
+++ b/lua/leetcode/problem/console/testcase.lua
@@ -1,13 +1,159 @@
+local t = require("leetcode.translator")
+
 ---@class leet.problem.console.testcase
+---@field console leet.problem.console
 local M = Markup.object()
 
-function M:new()
+---@param console leet.problem.console
+function M:new(console)
+    self.console = console
+    self.testcases = {}
+
     self.win = Markup.window({
         box = "horizontal",
-        border = "rounded",
         title = "Testcase",
+        border = "rounded",
         show = false,
+        file = self:path(),
+        bo = {
+            modifiable = true,
+        },
+        on_buf = function()
+            -- self:handle_buf()
+        end,
     })
+    self.win:on({
+        "TextChanged",
+        "TextChangedI",
+        "TextChangedP",
+        "InsertLeave",
+    }, function()
+        self:draw_extmarks()
+    end)
+end
+
+---@return string path, boolean existed
+function M:path()
+    local fn = self.console.problem:filename("txt")
+    vim.fn.mkdir(Leet.config.storage.home .. "/testcase", "p")
+    self.file = Leet.config.storage.home:joinpath("testcase/" .. fn)
+    local existed = self.file:exists()
+
+    -- if not existed then
+    self.file:write(self:build_lines(), "w")
+    -- end
+
+    return self.file:absolute(), existed
+end
+
+function M:build_lines()
+    local lines = {}
+
+    local t_list = self.console.problem.problem.testcase_list
+    self.testcase_len = #vim.split(t_list[1] or "", "\n")
+
+    for i, case in ipairs(t_list) do
+        if i ~= 1 then
+            table.insert(lines, "")
+        end
+        for s in vim.gsplit(case, "\n", { trimempty = true }) do
+            table.insert(lines, s)
+        end
+    end
+
+    -- vim.api.nvim_buf_set_lines(self.win.buf, 0, -1, false, lines)
+    return table.concat(lines, "\n")
+    -- return self:draw_extmarks()
+end
+
+local ns = vim.api.nvim_create_namespace("leet.problem.console.testcase.extmarks")
+
+function M:draw_extmarks()
+    if not Leet.config.user.console.testcase.virt_text then
+        return
+    end
+
+    local md = self.console.problem.problem.meta_data
+    if not md.params then
+        return
+    end
+
+    local buf = self.win.buf
+    local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
+    vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)
+
+    local j = 1
+    local pad = (" "):rep(2)
+    local function get_param(idx)
+        return {
+            { pad },
+            { "", "Operator" },
+            { " " },
+            { md.params[idx].name, "Comment" },
+            { " " },
+            { md.params[idx].type, "Type" },
+        }
+    end
+
+    local invalid = false
+
+    for i, line in ipairs(lines) do
+        pcall(function()
+            if lines[i - 1] == "" and lines[i] == "" then
+                invalid = true
+            end
+        end)
+
+        if line ~= "" then
+            local ok, text = pcall(get_param, j)
+            if not ok or invalid then
+                text = { { pad }, { (" %s"):format(t("invalid")), "leetcode_error" } }
+            end
+
+            vim.api.nvim_buf_set_extmark(buf, ns, i - 1, -1, {
+                virt_text = text,
+            })
+            j = j + 1
+        else
+            j = 1
+        end
+    end
+
+    return self
+end
+
+function M:content()
+    local lines = self.win:lines()
+    lines = vim.tbl_filter(function(line)
+        return line ~= ""
+    end, lines)
+    return table.concat(lines, "\n")
+end
+
+function M:snapshot(id, data)
+    if not data.test_case then
+        return
+    end
+    self.testcases[id] = data.test_case
+end
+
+---@return string[][]
+function M:by_id(id) --
+    local testcases = {} ---@type string[][]
+
+    local i, n = 0, self.testcase_len
+    for case in vim.gsplit(self.testcases[id] or "", "\n") do
+        local j = math.floor(i / n) + 1
+
+        if not testcases[j] then
+            testcases[j] = {}
+        end
+        table.insert(testcases[j], case)
+
+        i = i + 1
+    end
+
+    return testcases
 end
 
 return M
diff --git a/lua/leetcode/problem/description.lua b/lua/leetcode/problem/description.lua
index ba7ecd87..b3f24026 100644
--- a/lua/leetcode/problem/description.lua
+++ b/lua/leetcode/problem/description.lua
@@ -1,16 +1,16 @@
----@param leet.problem.description
+---@class leet.problem.description
 local M = Markup.object()
 
 ---@param problem leet.problem
 function M:new(problem)
-    self.problem = problem
-    self.renderer = Markup.renderer({
-        relative = "win",
-        position = "left",
-        width = 0.4,
-        enter = false,
-        focusable = true,
-    })
+	self.problem = problem
+	self.renderer = Markup.renderer({
+		relative = "win",
+		position = "left",
+		width = 0.4,
+		enter = false,
+		focusable = true,
+	})
 end
 
 return M
diff --git a/lua/leetcode/problem/init.lua b/lua/leetcode/problem/init.lua
index d87f8c31..2943f0ce 100644
--- a/lua/leetcode/problem/init.lua
+++ b/lua/leetcode/problem/init.lua
@@ -6,199 +6,337 @@ local Console = require("leetcode.problem.console")
 
 ---@class leet.problem
 ---@field cached leet.cache.problem
+---@field console leet.problem.console
+---@field description leet.problem.description
+---@overload fun(problem: leet.cache.problem): leet.problem
 local M = Markup.object()
 
+---@param cached leet.cache.problem
 ---@param problem leet.cache.problem
-function M:new(problem)
-    self.lang = config.lang
-    self.cached = problem
-end
-
-function M:show()
-    local tabp = Leet.util.detect_duplicate_question(self.cached.title_slug, config.lang)
-    if tabp then
-        return pcall(vim.api.nvim_set_current_tabpage, tabp)
-    end
-
-    local problem = Leet.api.problems.by_title_slug(self.cached.title_slug)
-    if
-        not problem --
-        or (problem.is_paid_only and not config.auth.is_premium)
-    then
-        return Markup.log.warn("Question is for premium users only")
-    end
-
-    self.problem = problem
-
-    vim.cmd.tabe()
-    local path = self:path()
-    vim.cmd("$tabe " .. path)
-
-    self.desc = Description(self)
-    self.console = Console(self)
-
-    -- self.win = Markup.window({
-    --     position = "current",
-    --     hijack = true,
-    --     -- fixed = true,
-    --     -- file = path,
-    --     minimal = false,
-    -- })
-    -- if self:snippet() then
-    --     self:handle_mount()
-    -- else
-    --     local msg = ("Snippet for `%s` not found. Select a different language"):format(self.lang)
-    --     Markup.log.warn(msg)
-    --
-    --     require("leetcode.pickers.language").pick_lang(self, function(snippet)
-    --         self.lang = snippet.t.slug
-    --         self:handle_mount()
-    --     end)
-    -- end
+function M:new(cached, problem)
+	self.lang = config.lang
+	self.cached = cached
+	self.problem = problem
+
+	table.insert(Leet.active_problems, self)
+	self:open_buf()
+
+	self.description = Description(self)
+	self.console = Console(self)
+end
+
+function M:tabpage()
+	return vim.api.nvim_win_get_tabpage(self.win)
+end
+
+function M:valid()
+	if not self.buf or not vim.api.nvim_buf_is_valid(self.buf) then
+		return false
+	end
+	if not self.win or not vim.api.nvim_win_is_valid(self.win) then
+		return false
+	end
+	return true
+end
+
+function M:open_buf()
+	if self.buf and vim.api.nvim_buf_is_valid(self.buf) then
+		return
+	end
+
+	local path, existed = self:path()
+	vim.cmd("$tabe " .. path)
+	self.buf = vim.api.nvim_get_current_buf()
+	self.win = vim.api.nvim_get_current_win()
+	vim.wo[self.win].winfixbuf = true
+
+	vim.bo[self.buf].buflisted = true
+	vim.api.nvim_win_set_buf(self.win, self.buf)
+
+	if config.user.editor.fold_imports then
+		self:editor_fold_imports(false)
+	end
+
+	if config.user.editor.reset_previous_code and (existed and self.cached.status == "ac") then
+		self:reset_previous_code()
+	end
+end
+
+function M:filename(ft)
+	local lang = Leet.util.get_lang(self.lang)
+	local alt = lang.alt and ("." .. lang.alt) or ""
+
+	return ("%s.%s%s.%s"):format(
+		self.problem.frontend_id,
+		self.problem.title_slug,
+		alt,
+		ft or lang.ft
+	)
 end
 
 ---@return string path, boolean existed
 function M:path()
-    local lang = Leet.util.get_lang(self.lang)
-    local alt = lang.alt and ("." .. lang.alt) or ""
+	local fn = self:filename()
+	self.file = config.storage.home:joinpath(fn)
+	local existed = self.file:exists()
 
-    local fn = ("%s.%s%s.%s"):format(
-        self.problem.frontend_id,
-        self.problem.title_slug,
-        alt,
-        lang.ft
-    )
-    self.file = config.storage.home:joinpath(fn)
-    local existed = self.file:exists()
+	if not existed then
+		self.file:write(self:snippet(), "w")
+	end
 
-    if not existed then
-        self.file:write(self:snippet(), "w")
-    end
-
-    return self.file:absolute(), existed
+	return self.file:absolute(), existed
 end
 
 ---@param raw? boolean
 function M:snippet(raw)
-    local snippets = self.problem.code_snippets ~= vim.NIL and self.problem.code_snippets or {}
-    local snip = vim.tbl_filter(function(snip)
-        return snip.lang_slug == self.lang
-    end, snippets)[1]
-    if not snip then
-        return
-    end
-
-    local code = snip.code:gsub("\r\n", "\n")
-    return raw and code or self:injector(code)
+	local snippets = self.problem.code_snippets ~= vim.NIL and self.problem.code_snippets or {}
+	local snip = vim.tbl_filter(function(snip)
+		return snip.lang_slug == self.lang
+	end, snippets)[1]
+	if not snip then
+		return
+	end
+
+	local code = snip.code:gsub("\r\n", "\n")
+	return raw and code or self:injector(code)
 end
 
 ---@param code string
 function M:injector(code)
-    local inject = config.user.injector[self.lang] or {}
+	local inject = config.user.injector[self.lang] or {}
 
-    local parts = { self:editor_section(code, "code") }
+	local parts = { self:editor_section(code, "code") }
 
-    local before = self:inject(true)
-    if before then
-        table.insert(parts, 1, before)
-    end
+	local before = self:inject(true)
+	if before then
+		table.insert(parts, 1, before)
+	end
 
-    local imports = self:inject_imports()
-    if imports then
-        table.insert(parts, 1, self:editor_section(table.concat(imports, "\n"), "imports"))
-    end
+	local imports = self:inject_imports()
+	if imports then
+		table.insert(parts, 1, self:editor_section(table.concat(imports, "\n"), "imports"))
+	end
 
-    local after = self:inject(false)
-    if after then
-        table.insert(parts, after)
-    end
+	local after = self:inject(false)
+	if after then
+		table.insert(parts, after)
+	end
 
-    local gap = (inject.gap or 1) + 1
-    return table.concat(parts, ("\n"):rep(gap))
+	local gap = (inject.gap or 1) + 1
+	return table.concat(parts, ("\n"):rep(gap))
 end
 
 ---@param before boolean
 ---@return string?
 function M:inject(before)
-    local inject = config.user.injector[self.lang] or {}
-    local inj = before and (inject.before or {}) or (inject.after or {})
-
-    local res
-    if type(inj) == "table" then
-        res = table.concat(inj, "\n")
-    elseif type(inj) == "string" then
-        res = inj
-    end
-
-    if res and res ~= "" then
-        return res
-    else
-        return nil
-    end
+	local inject = config.user.injector[self.lang] or {}
+	local inj = before and (inject.before or {}) or (inject.after or {})
+
+	local res
+	if type(inj) == "table" then
+		res = table.concat(inj, "\n")
+	elseif type(inj) == "string" then
+		res = inj
+	end
+
+	if res and res ~= "" then
+		return res
+	else
+		return nil
+	end
 end
 
 ---@return string[]?
 function M:inject_imports()
-    local inject = config.user.injector[self.lang] or {}
-
-    local imports = inject.imports
-    local default_imports = config.imports[self.lang]
-
-    local function valid_imports(tbl)
-        if vim.tbl_isempty(tbl or {}) then
-            return false
-        end
-        if not vim.islist(tbl) then
-            Markup.log.error("Invalid imports format for language: " .. self.lang)
-            return false
-        end
-        return true
-    end
-
-    if not imports then
-        return default_imports
-    end
-
-    if type(imports) == "function" then
-        local overriden = imports(vim.deepcopy(default_imports or {}))
-        if not valid_imports(overriden) then
-            return default_imports
-        end
-        return overriden
-    end
-
-    local resolved = type(imports) == "string" and { imports } or imports
-    if not valid_imports(resolved) then
-        return
-    end
-
-    local merged, seen = {}, {}
-    local function add(list)
-        for _, v in ipairs(list) do
-            if not seen[v] then
-                table.insert(merged, v)
-                seen[v] = true
-            end
-        end
-    end
-
-    add(default_imports)
-    add(resolved)
-
-    return not vim.tbl_isempty(merged) and merged or nil
+	local inject = config.user.injector[self.lang] or {}
+
+	local imports = inject.imports
+	local default_imports = config.imports[self.lang]
+
+	local function valid_imports(tbl)
+		if vim.tbl_isempty(tbl or {}) then
+			return false
+		end
+		if not vim.islist(tbl) then
+			Markup.log.error("Invalid imports format for language: " .. self.lang)
+			return false
+		end
+		return true
+	end
+
+	if not imports then
+		return default_imports
+	end
+
+	if type(imports) == "function" then
+		local overriden = imports(vim.deepcopy(default_imports or {}))
+		if not valid_imports(overriden) then
+			return default_imports
+		end
+		return overriden
+	end
+
+	local resolved = type(imports) == "string" and { imports } or imports
+	if not valid_imports(resolved) then
+		return
+	end
+
+	local merged, seen = {}, {}
+	local function add(list)
+		for _, v in ipairs(list) do
+			if not seen[v] then
+				table.insert(merged, v)
+				seen[v] = true
+			end
+		end
+	end
+
+	add(default_imports)
+	add(resolved)
+
+	return not vim.tbl_isempty(merged) and merged or nil
 end
 
 ---@param lines string|string[]
 ---@param name lc.editor.section
 ---@return string
 function M:editor_section(lines, name)
-    local comment = Leet.util.get_lang(self.lang).comment
+	local comment = Leet.util.get_lang(self.lang).comment
+
+	local start_tag = comment .. " " .. Leet.util.section_tag(name, true)
+	local end_tag = comment .. " " .. Leet.util.section_tag(name, false)
 
-    local start_tag = comment .. " " .. Leet.util.section_tag(name, true)
-    local end_tag = comment .. " " .. Leet.util.section_tag(name, false)
+	local str = type(lines) ~= "string" and table.concat(lines, "\n") or lines
+	return table.concat({ start_tag, str, end_tag }, "\n")
+end
+
+---@param name string
+---@param inclusive? boolean
+function M:editor_section_range(name, inclusive)
+	local lines = vim.api.nvim_buf_get_lines(self.buf, 0, -1, false)
+	local start_i, end_i
+
+	local start_tag = Leet.util.section_tag(name, true)
+	local end_tag = Leet.util.section_tag(name, false)
+
+	for i, line in ipairs(lines) do
+		if line:match(start_tag) then
+			start_i = i + (inclusive and 0 or 1)
+		elseif line:match(end_tag) then
+			end_i = i - (inclusive and 0 or 1)
+		end
+	end
+
+	local res = {
+		start_i = start_i,
+		end_i = end_i,
+		lines = lines,
+		complete = start_i and end_i,
+	}
+
+	res.log_not_found = function()
+		if res.complete then
+			return
+		end
+
+		local missing = {}
+		if not res.start_i then
+			table.insert(missing, ("`%s`"):format(start_tag))
+		end
+		if not res.end_i then
+			table.insert(missing, ("`%s`"):format(end_tag))
+		end
+
+		Markup.log.warn(table.concat(missing, " and ") .. " not found.")
+	end
+
+	res.is_partial = function()
+		return res.complete or (res.start_i or res.end_i)
+	end
+
+	res.is_valid_or_log = function()
+		if res.complete then
+			return true
+		else
+			res.log_not_found()
+			return false
+		end
+	end
+
+	return res
+end
+
+function M:editor_yank_code()
+	if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then
+		return
+	end
+
+	local range = self:editor_section_range("code")
+	if range:is_valid_or_log() then
+		vim.api.nvim_buf_call(self.buf, function()
+			vim.cmd(("%d,%dyank"):format(range.start_i, range.end_i))
+		end)
+	end
+end
+
+---@param strict? boolean
+function M:editor_fold_imports(strict)
+	if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then
+		return
+	end
+
+	local range = self:editor_section_range("imports", true)
+	if range.complete or range.end_i then
+		vim.api.nvim_buf_call(self.buf, function()
+			pcall(vim.cmd, ("%d,%dfold"):format(range.start_i or 1, range.end_i)) ---@diagnostic disable-line: param-type-mismatch
+		end)
+	elseif strict then
+		range.log_not_found()
+	end
+end
+
+---@param start_i integer
+---@param end_i integer
+---@param lines string[]|string
+function M:editor_set_lines(start_i, end_i, lines)
+	if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then
+		return
+	end
+
+	lines = type(lines) == "string" and vim.split(lines, "\n") or lines ---@cast lines string[]
+	vim.api.nvim_buf_set_lines(self.buf, start_i, end_i, false, lines)
+end
+
+---@param lines string[]|string
+---@param name string
+function M:editor_section_replace(lines, name)
+	local range = self:editor_section_range(name)
+
+	if range:is_valid_or_log() then
+		self:editor_set_lines(range.start_i - 1, range.end_i, lines)
+	end
+end
+
+function M:editor_reset_code()
+	local new_lines = self:snippet(true) or ""
+	self:editor_section_replace(new_lines, "code")
+end
+
+---@param submit boolean
+---@return string
+function M:editor_submit_lines(submit)
+	local range = self:editor_section_range("code")
+	assert(range.complete, "Code section not found")
+
+	local prefix = not submit and ("\n"):rep(range.start_i - 1) or ""
+	return prefix .. table.concat(range.lines, "\n", range.start_i, range.end_i)
+end
 
-    local str = type(lines) ~= "string" and table.concat(lines, "\n") or lines
-    return table.concat({ start_tag, str, end_tag }, "\n")
+function M:reset_previous_code()
+	self:editor_reset_code()
+	vim.schedule(function()
+		Markup.log("Previous code found and reset. To undo, simply press `u` or use `:undo`.")
+	end)
 end
 
 return M
diff --git a/lua/leetcode/problem/runner.lua b/lua/leetcode/problem/runner.lua
index 4e1dbc70..4f1591bf 100644
--- a/lua/leetcode/problem/runner.lua
+++ b/lua/leetcode/problem/runner.lua
@@ -1,6 +1,5 @@
 ---@module 'plenary'
 
-local log = require("leetcode.logger")
 local interpreter = require("leetcode.api.interpreter")
 local config = require("leetcode.config")
 local Judge = require("leetcode.logger.spinner.judge")
@@ -11,70 +10,54 @@ leetbody:touch()
 
 ---@class leet.problem.runner
 ---@field question lc.ui.Question
-local Runner = {}
-Runner.__index = Runner
-
-Runner.running = false
-
----@param self leet.problem.runner
----@param submit boolean
-Runner.run = vim.schedule_wrap(function(self, submit)
-    if Runner.running then
-        return log.warn("Runner is busy")
-    end
-
-    local ok, err = pcall(Runner.handle, self, submit)
-    if not ok then
-        self:stop()
-        log.error(err)
-    end
-end)
-
-Runner.stop = function()
-    Runner.running = false
-end
-
-function Runner:handle(submit)
-    Runner.running = true
-    local question = self.question
-
-    local body = {
-        lang = question.lang,
-        typed_code = self.question:editor_submit_lines(submit),
-        question_id = question.q.id,
-        data_input = not submit and question.console.testcase:content(),
-    }
-
-    local judge = Judge:init()
-    local function callback(item, state, err)
-        if err or item then
-            self:stop()
-        end
-
-        if item then
-            if item._.success then
-                judge:success(item.status_msg)
-            else
-                judge:error(item.status_msg)
-            end
-        elseif state then
-            judge:from_state(state)
-        elseif err then
-            judge:error(err.msg or "Something went wrong")
-        end
-
-        if item then
-            question.console.result:handle(item)
-        end
-    end
-
-    leetbody:write(vim.json.encode(body), "w")
-    interpreter.run(submit, question, leetbody:absolute(), callback)
-end
-
----@param question lc.ui.Question
-function Runner:init(question)
-    return setmetatable({ question = question }, self)
+local M = Markup.singleton()
+M.running = false
+
+---@param problem leet.problem
+function M.run(problem, submit)
+	if M.running then
+		return Markup.log.warn("Runner is busy")
+	end
+
+	local function _run()
+		M.running = true
+
+		local body = {
+			lang = problem.lang,
+			typed_code = problem:editor_submit_lines(submit),
+			question_id = problem.problem.id,
+			data_input = not submit and problem.console.testcase:content(),
+		}
+		Markup.log("body:", body)
+
+		local judge = Judge:init()
+		local function callback(item, state, err)
+			if err or item then
+				M.running = false
+			end
+
+			if item then
+				if item._.success then
+					judge:success(item.status_msg)
+				else
+					judge:error(item.status_msg)
+				end
+			elseif state then
+				judge:from_state(state)
+			elseif err then
+				judge:error(err.msg or "Something went wrong")
+			end
+
+			if item then
+				problem.console.result:handle(item)
+			end
+		end
+
+		leetbody:write(vim.json.encode(body), "w")
+		interpreter.run(submit, problem, leetbody:absolute(), callback)
+	end
+
+	vim.schedule(_run)
 end
 
-return Runner
+return M
diff --git a/lua/leetcode/ui/menu/nav/menu/cache/init.lua b/lua/leetcode/ui/menu/nav/menu/cache/init.lua
index 2b18f1da..6814fd7a 100644
--- a/lua/leetcode/ui/menu/nav/menu/cache/init.lua
+++ b/lua/leetcode/ui/menu/nav/menu/cache/init.lua
@@ -4,7 +4,7 @@ local Button = require("leetcode.ui.menu.button")
 local BackButton = require("leetcode.ui.menu.button.back")
 
 return m.component(function()
-    local cmd = require("leetcode.command")
+    local cmd = require("leetcode.cmd")
 
     return Buttons({
         Button({
diff --git a/lua/leetcode/ui/menu/nav/menu/cookie/init.lua b/lua/leetcode/ui/menu/nav/menu/cookie/init.lua
index bf5de5fa..ce32d4b2 100644
--- a/lua/leetcode/ui/menu/nav/menu/cookie/init.lua
+++ b/lua/leetcode/ui/menu/nav/menu/cookie/init.lua
@@ -4,7 +4,7 @@ local Button = require("leetcode.ui.menu.button")
 local BackButton = require("leetcode.ui.menu.button.back")
 
 return markup.component(function()
-    local cmd = require("leetcode.command")
+    local cmd = require("leetcode.cmd")
 
     return Buttons({
         Button({
diff --git a/lua/leetcode/ui/menu/nav/menu/problems/init.lua b/lua/leetcode/ui/menu/nav/menu/problems/init.lua
index c8280ac6..7605525e 100644
--- a/lua/leetcode/ui/menu/nav/menu/problems/init.lua
+++ b/lua/leetcode/ui/menu/nav/menu/problems/init.lua
@@ -5,7 +5,7 @@ local Button = require("leetcode.ui.menu.button")
 local BackButton = require("leetcode.ui.menu.button.back")
 
 return m.component(function()
-    local cmd = require("leetcode.command")
+    local cmd = require("leetcode.cmd")
 
     return Buttons({
         Button({
diff --git a/lua/leetcode/ui/menu/nav/menu/statistics/init.lua b/lua/leetcode/ui/menu/nav/menu/statistics/init.lua
index 84d7bd9e..5b3e68ee 100644
--- a/lua/leetcode/ui/menu/nav/menu/statistics/init.lua
+++ b/lua/leetcode/ui/menu/nav/menu/statistics/init.lua
@@ -5,7 +5,7 @@ local BackButton = require("leetcode.ui.menu.button.back")
 local config = require("leetcode.config")
 local log = require("leetcode.logger")
 
-local cmd = require("leetcode.command")
+local cmd = require("leetcode.cmd")
 
 return m.component(function()
     return Buttons({
diff --git a/lua/leetcode/ui/problem/result/cases/case.lua b/lua/leetcode/ui/problem/result/cases/case.lua
new file mode 100644
index 00000000..ada29d8a
--- /dev/null
+++ b/lua/leetcode/ui/problem/result/cases/case.lua
@@ -0,0 +1,55 @@
+local m = require("markup")
+
+local CaseBlock = m.component(function(props)
+    return m.block({
+        padding = { 1, 2 },
+        style = "LspInlayHint",
+        children = props.children,
+    })
+end)
+
+local Case = m.component(function(props)
+    local input = Markup.list(props.input)
+    local output = props.output
+    local expected = props.expected
+    local std_output = props.std_output
+    local console = props.console ---@type leet.problem.console
+    local params = console.problem.problem.meta_data.params
+
+    local std_lines = Markup.list(vim.split(std_output or "", "\n", { trimempty = true }))
+
+    return m.block({
+        spacing = 1,
+        m.block({
+            m.inline("Input"),
+            m.block({
+                spacing = 1,
+                input:map(function(case, i)
+                    local ok, param = pcall(function()
+                        return params[i].name
+                    end)
+                    return CaseBlock({
+                        ok and m.block(param .. " ="),
+                        m.block(case),
+                    })
+                end),
+            }),
+        }),
+        m.block({
+            m.inline(" Stdout"),
+            CaseBlock(std_lines:map(function(line)
+                return m.block(line)
+            end)),
+        }),
+        m.block({
+            m.inline("Output"),
+            CaseBlock(output),
+        }),
+        m.block({
+            m.inline("Expected"),
+            CaseBlock(expected),
+        }),
+    })
+end)
+
+return Case
diff --git a/lua/leetcode/ui/problem/result/cases/init.lua b/lua/leetcode/ui/problem/result/cases/init.lua
new file mode 100644
index 00000000..7ef1bcaf
--- /dev/null
+++ b/lua/leetcode/ui/problem/result/cases/init.lua
@@ -0,0 +1,58 @@
+local m = require("markup")
+local Case = require("leetcode.ui.problem.result.cases.case")
+
+local Cases = m.component(function(props)
+    local active, set_active = m.hooks.variable(1)
+
+    local item = props.item ---@type lc.runtime
+    local console = props.console ---@type leet.problem.console
+
+    local total = item.total_testcases ~= vim.NIL and item.total_testcases or 0
+    local testcases = console.testcase:by_id(item.submission_id)
+    local win = m.hooks.window()
+
+    m.hooks.effect(function()
+        local unmaps = {}
+        for i = 1, total do
+            local unmap = win:map("n", tostring(i), function()
+                set_active(i)
+            end)
+            table.insert(unmaps, unmap)
+        end
+        return function()
+            for _, v in ipairs(unmaps) do
+                v()
+            end
+        end
+    end, {})
+
+    local cases = Markup.list(testcases):map(function(case, i)
+        return Case({
+            input = case,
+            output = item.code_answer[i],
+            expected = item.expected_code_answer[i],
+            std_output = item.std_output_list[i],
+            console = console,
+        })
+    end)
+
+    local nav = Markup.list(testcases):map(function(_, i)
+        return m.inline({
+            style = active == i and "Error" or "DiagnosticOk",
+            m.inline("Case "),
+            m.inline((" (%d) "):format(i)),
+        })
+    end)
+
+    return m.block({
+        spacing = 1,
+        margin = { 1, 2 },
+        m.inline({
+            spacing = 1,
+            nav,
+        }),
+        cases[active],
+    })
+end)
+
+return Cases
diff --git a/lua/leetcode/ui/problem/result/compile-error.lua b/lua/leetcode/ui/problem/result/compile-error.lua
new file mode 100644
index 00000000..e69de29b
diff --git a/lua/leetcode/ui/problem/result/header.lua b/lua/leetcode/ui/problem/result/header.lua
new file mode 100644
index 00000000..7ef76932
--- /dev/null
+++ b/lua/leetcode/ui/problem/result/header.lua
@@ -0,0 +1,27 @@
+local m = require("markup")
+
+local Header = m.component(function(props)
+    local item = props.item
+
+    -- if item._.submission then
+    --     if item._.success then
+    --         self:append(" 🎉")
+    --     else
+    --         self:append(" | ")
+    --         self:append(testcases_passed(item), "leetcode_alt")
+    --     end
+    if not item.status_runtime then
+        return
+    end
+
+    return m.block({
+        m.inline(" Accepted 🎉", "DiagnosticSignOk"),
+        m.inline({
+            style = "Conceal",
+            " | Runtime: ",
+            item.status_runtime,
+        }),
+    })
+end)
+
+return Header
diff --git a/lua/leetcode/ui/problem/result/init.lua b/lua/leetcode/ui/problem/result/init.lua
new file mode 100644
index 00000000..d1dc3dd4
--- /dev/null
+++ b/lua/leetcode/ui/problem/result/init.lua
@@ -0,0 +1,30 @@
+local m = require("markup")
+local Runtime = require("leetcode.ui.problem.result.runtime")
+local SubmissionError = require("leetcode.ui.problem.result.submission-error")
+local LimitExceed = require("leetcode.ui.problem.result.limit-exceeded")
+local RuntimeError = require("leetcode.ui.problem.result.runtime-error")
+local InternalError = require("leetcode.ui.problem.result.internal-error")
+local CompileError = require("leetcode.ui.problem.result.compile-error")
+
+local App = m.component(function(props)
+    local item = props.item
+    local sc = item.status_code
+
+    if sc == 10 then
+        return Runtime({ item = item, console = props.console })
+    elseif sc == 11 then
+        return SubmissionError({ item = item })
+    elseif sc == 12 or sc == 13 or sc == 14 then
+        return LimitExceed({ item = item })
+    elseif sc == 15 then
+        return RuntimeError({ item = item })
+    elseif sc == 16 then
+        return InternalError({ item = item })
+    elseif sc == 20 then
+        return CompileError({ item = item })
+    else
+        return "unknown runner status code: " .. sc
+    end
+end)
+
+return App
diff --git a/lua/leetcode/ui/problem/result/internal-error.lua b/lua/leetcode/ui/problem/result/internal-error.lua
new file mode 100644
index 00000000..e69de29b
diff --git a/lua/leetcode/ui/problem/result/limit-exceeded.lua b/lua/leetcode/ui/problem/result/limit-exceeded.lua
new file mode 100644
index 00000000..e69de29b
diff --git a/lua/leetcode/ui/problem/result/runtime-error.lua b/lua/leetcode/ui/problem/result/runtime-error.lua
new file mode 100644
index 00000000..e69de29b
diff --git a/lua/leetcode/ui/problem/result/runtime/accepted.lua b/lua/leetcode/ui/problem/result/runtime/accepted.lua
new file mode 100644
index 00000000..745329be
--- /dev/null
+++ b/lua/leetcode/ui/problem/result/runtime/accepted.lua
@@ -0,0 +1,55 @@
+local m = require('markup')
+
+local function perc_hl(perc) --
+	if perc >= 50 then
+		return "leetcode_ok"
+	elseif perc >= 10 then
+		return nil
+	elseif perc >= 0 then
+		return "leetcode_error"
+	end
+end
+
+local App = m.component(function(props)
+	local item = props.item ---@type lc.runtime
+	local runtime_ms = item.display_runtime or vim.split(item.status_runtime, " ")[1] or "NIL"
+	local lang = Leet.util.get_lang_by_name(item.pretty_lang)
+
+	item.status_memory = item.status_memory:gsub("(%d+)%s*(%a+)", "%1 %2")
+	local s_mem = vim.split(item.status_memory, " ")
+
+	return m.block({
+		m.block({
+			m.inline("# Accepted "),
+			lang.icon .. " " .. lang.lang,
+			style = lang.hl or "Structure",
+			margin_bottom = 1,
+		}),
+		m.flex({
+			spacing = 3,
+			m.block({
+				m.block("  Runtime"),
+				m.inline({
+					runtime_ms,
+					m.inline("ms", "Conceal"),
+				}),
+				" | ",
+				m.inline(("%s %.2f%%"):format("Beats", item.runtime_percentile), perc_hl(item.runtime_percentile)),
+			}),
+			m.block({
+				m.block("󰍛 Memory"),
+				m.inline({
+					m.inline(s_mem, "Conceal"),
+				}),
+				" | ",
+				m.inline({
+					"Beats ",
+					("%.2f%%"):format(item.memory_percentile),
+					style = perc_hl(item.runtime_percentile),
+				}),
+			}),
+		}),
+	})
+end)
+
+return App
diff --git a/lua/leetcode/ui/problem/result/runtime/init.lua b/lua/leetcode/ui/problem/result/runtime/init.lua
new file mode 100644
index 00000000..b212b9a3
--- /dev/null
+++ b/lua/leetcode/ui/problem/result/runtime/init.lua
@@ -0,0 +1,21 @@
+local m = require("markup")
+local Accepted = require("leetcode.ui.problem.result.runtime.accepted")
+local Header = require("leetcode.ui.problem.result.header")
+local Cases = require("leetcode.ui.problem.result.cases")
+
+local Runtime = m.component(function(props)
+    local item = props.item ---@type lc.runtime
+    local console = props.console ---@type lc.runtime
+
+    if item._.submission then
+        return Accepted(item)
+    end
+
+    return m.block({
+        spacing = 1,
+        Header({ item = item }),
+        Cases({ item = item, console = console }),
+    })
+end)
+
+return Runtime
diff --git a/lua/leetcode/ui/problem/result/submission-error.lua b/lua/leetcode/ui/problem/result/submission-error.lua
new file mode 100644
index 00000000..e69de29b
diff --git a/lua/leetcode/util.lua b/lua/leetcode/util.lua
index f72a9652..c16cb8a8 100644
--- a/lua/leetcode/util.lua
+++ b/lua/leetcode/util.lua
@@ -5,152 +5,155 @@ local log = require("leetcode.logger")
 local M = {}
 
 function M.deprecate_usr_cmd(name, new)
-    vim.api.nvim_create_user_command(name, function()
-        local cmd = require("leetcode.command")
-        cmd.deprecate(name, new)
-        pcall(vim.cmd, new) ---@diagnostic disable-line
-    end, {})
+	vim.api.nvim_create_user_command(name, function()
+		local cmd = require("leetcode.cmd")
+		cmd.deprecate(name, new)
+		pcall(vim.cmd, new) ---@diagnostic disable-line
+	end, {})
 end
 
 ---@param fn string
 function M.cmd(fn)
-    return ("<cmd>lua require('leetcode.command').%s()<cr>"):format(fn)
+	return ("<cmd>lua require('leetcode.cmd').%s()<cr>"):format(fn)
 end
 
 ---@param title_slug string
 ---@param lang lc.lang
 function M.detect_duplicate_question(title_slug, lang)
-    local tabs = M.question_tabs()
-
-    for _, q in ipairs(tabs) do
-        if title_slug == q.question.q.title_slug and lang == q.question.lang then
-            return q.tabpage
-        end
-    end
+	for _, ap in ipairs(Leet.active_problems) do
+		if title_slug == ap.cached.title_slug and lang == ap.lang then
+			return ap:tabpage()
+		end
+	end
 end
 
 ---@return { tabpage: integer, question: lc.ui.Question }[]
 function M.question_tabs()
-    local questions = {}
+	local questions = {}
 
-    for _, q in ipairs(_Lc_state.questions) do
-        local tabp = M.question_tabp(q)
-        if tabp then
-            table.insert(questions, { tabpage = tabp, question = q })
-        end
-    end
+	for _, q in ipairs(Leet.active_problems) do
+		local tabp = q:tabpage()
+		if tabp then
+			table.insert(questions, { tabpage = tabp, question = q })
+		end
+	end
 
-    return questions
+	return questions
 end
 
 ---@param q lc.ui.Question
 ---@return integer|nil
 function M.question_tabp(q)
-    local ok, tabp = pcall(vim.api.nvim_win_get_tabpage, q.winid)
-    if ok then
-        return tabp
-    end
+	local ok, tabp = pcall(vim.api.nvim_win_get_tabpage, q.win.win)
+	if ok then
+		return tabp
+	end
 end
 
----@return lc.ui.Question
+---@return leet.problem
 function M.curr_question()
-    local tabp = vim.api.nvim_get_current_tabpage()
-    local tabs = M.question_tabs()
-
-    local tab = vim.tbl_filter(function(tab)
-        return tab.tabpage == tabp
-    end, tabs)[1] or {}
-    if tab.question then
-        return tab.question, tabp
-    else
-        log.error("No current question found")
-    end
+	local tabp = vim.api.nvim_get_current_tabpage()
+
+	for _, ap in ipairs(Leet.active_problems) do
+		if ap:tabpage() == tabp then
+			return ap
+		end
+	end
+
+	-- local tab = vim.tbl_filter(function(tab)
+	--     return tab.tabpage == tabp
+	-- end, tabs)[1] or {}
+	-- if tab.question then
+	--     return tab.question, tabp
+	-- else
+	--     log.error("No current question found")
+	-- end
 end
 
 ---@return lc.language
 function M.get_lang(slug)
-    ---@param l lc.language
-    return vim.tbl_filter(function(l)
-        return l.slug == slug
-    end, config.langs)[1]
+	---@param l lc.language
+	return vim.tbl_filter(function(l)
+		return l.slug == slug
+	end, config.langs)[1]
 end
 
 ---@return lc.language
 function M.get_lang_by_name(name)
-    ---@param l lc.language
-    return vim.tbl_filter(function(l)
-        return l.lang == name
-    end, config.langs)[1]
+	---@param l lc.language
+	return vim.tbl_filter(function(l)
+		return l.lang == name
+	end, config.langs)[1]
 end
 
 ---@param event lc.hook
 ---@return fun()[]|nil
 function M.get_hooks(event)
-    local fns = config.user.hooks[event]
-    if not fns then
-        return
-    end
+	local fns = config.user.hooks[event]
+	if not fns then
+		return
+	end
 
-    if type(fns) == "function" then
-        fns = { fns }
-    end
+	if type(fns) == "function" then
+		fns = { fns }
+	end
 
-    return vim.list_extend(fns, config.hooks[event] or {})
+	return vim.list_extend(fns, config.hooks[event] or {})
 end
 
 ---@param event lc.hook
 function M.exec_hooks(event, ...)
-    local fns = M.get_hooks(event)
-    if not fns then
-        return log.error("unknown hook event: " .. event)
-    end
-
-    for i, fn in ipairs(fns) do
-        local ok, msg = pcall(fn, ...)
-        if not ok then
-            log.error(("bad hook #%d in `%s` event: %s"):format(i, event, msg))
-        end
-    end
+	local fns = M.get_hooks(event)
+	if not fns then
+		return log.error("unknown hook event: " .. event)
+	end
+
+	for i, fn in ipairs(fns) do
+		local ok, msg = pcall(fn, ...)
+		if not ok then
+			log.error(("bad hook #%d in `%s` event: %s"):format(i, event, msg))
+		end
+	end
 end
 
 ---@param content string
 ---@param translated_content string
 function M.translate(content, translated_content)
-    if config.is_cn then
-        if config.user.cn.translate_problems then
-            return translated_content or content
-        else
-            return content or translated_content
-        end
-    else
-        return content
-    end
+	if config.is_cn then
+		if config.user.cn.translate_problems then
+			return translated_content or content
+		else
+			return content or translated_content
+		end
+	else
+		return content
+	end
 end
 
 function M.auth_guard()
-    if not config.auth.is_signed_in then
-        error("User not logged-in", 0)
-    end
+	if not config.auth.is_signed_in then
+		error("User not logged-in", 0)
+	end
 end
 
 function M.norm_ins(str)
-    local ins = vim.inspect(str)
-    return ins:sub(2, #ins - 1)
+	local ins = vim.inspect(str)
+	return ins:sub(2, #ins - 1)
 end
 
 function M.with_version(v, with, without)
-    with = with or function() end
-    without = without or function() end
-    return (vim.fn.has("nvim-" .. v) == 1 and with or without)()
+	with = with or function() end
+	without = without or function() end
+	return (vim.fn.has("nvim-" .. v) == 1 and with or without)()
 end
 
 ---@param name lc.editor.section
 ---@param start? boolean
 function M.section_tag(name, start)
-    if name == "code" then
-        return ("@leet %s"):format(start and "start" or "end")
-    end
-    return ("@leet %s %s"):format(name, start and "start" or "end")
+	if name == "code" then
+		return ("@leet %s"):format(start and "start" or "end")
+	end
+	return ("@leet %s %s"):format(name, start and "start" or "end")
 end
 
 return M
diff --git a/queries/lua/highlights.scm b/queries/lua/highlights.scm
new file mode 100644
index 00000000..c6f87876
--- /dev/null
+++ b/queries/lua/highlights.scm
@@ -0,0 +1,4 @@
+;; extends
+
+((identifier) @namespace.builtin
+							(#any-of? @namespace.builtin "Leet"))

From 015c2695481f4c65139b4cd9e05796f38db5fa93 Mon Sep 17 00:00:00 2001
From: kawre <kenterq@gmail.com>
Date: Sat, 2 Aug 2025 00:51:27 +0200
Subject: [PATCH 4/4] feat: handle status codes

---
 lua/leetcode-ui/renderer/result.lua           | 403 ++++++++-------
 lua/leetcode/api/problems.lua                 | 396 +++++++--------
 lua/leetcode/api/types.lua                    |   1 +
 lua/leetcode/problem/console/result.lua       |   8 +-
 lua/leetcode/problem/console/testcase.lua     |   7 +-
 lua/leetcode/problem/init.lua                 | 472 +++++++++---------
 .../ui/problem/result/cases/block.lua         |  11 +
 lua/leetcode/ui/problem/result/cases/case.lua |  44 +-
 lua/leetcode/ui/problem/result/cases/init.lua |   2 +-
 .../ui/problem/result/compile-error.lua       |  14 +
 lua/leetcode/ui/problem/result/header.lua     |  33 +-
 lua/leetcode/ui/problem/result/init.lua       |  13 +-
 lua/leetcode/ui/problem/result/input.lua      |  23 +
 .../ui/problem/result/internal-error.lua      |  12 +
 .../ui/problem/result/limit-exceeded.lua      |  23 +
 .../ui/problem/result/runtime-error.lua       |   0
 .../ui/problem/result/runtime/accepted.lua    | 100 ++--
 .../ui/problem/result/runtime/error.lua       |  37 ++
 .../ui/problem/result/runtime/init.lua        |   6 +-
 lua/leetcode/ui/problem/result/stdout.lua     |  25 +
 .../ui/problem/result/submission-error.lua    |  16 +
 21 files changed, 896 insertions(+), 750 deletions(-)
 create mode 100644 lua/leetcode/ui/problem/result/cases/block.lua
 create mode 100644 lua/leetcode/ui/problem/result/input.lua
 delete mode 100644 lua/leetcode/ui/problem/result/runtime-error.lua
 create mode 100644 lua/leetcode/ui/problem/result/runtime/error.lua
 create mode 100644 lua/leetcode/ui/problem/result/stdout.lua

diff --git a/lua/leetcode-ui/renderer/result.lua b/lua/leetcode-ui/renderer/result.lua
index 86b49457..1221e0ea 100644
--- a/lua/leetcode-ui/renderer/result.lua
+++ b/lua/leetcode-ui/renderer/result.lua
@@ -24,254 +24,253 @@ local log = require("leetcode.logger")
 local ResultLayout = Renderer:extend("LeetResultLayout")
 
 function ResultLayout:handle_accepted(item)
-	self:set_opts({ spacing = 2 })
-
-	local function perc_hi(perc) --
-		if perc >= 50 then
-			return "leetcode_ok"
-		elseif perc >= 10 then
-			return nil
-		elseif perc >= 0 then
-			return "leetcode_error"
-		end
-	end
-
-	local header = Header(item)
-	self:insert(header)
-
-	--------------------------------------------
-	--- Runtime
-	--------------------------------------------
-	local runtime = Lines()
-	local runtime_ms = item.display_runtime or vim.split(item.status_runtime, " ")[1] or "NIL"
-	runtime:append(runtime_ms):append(" ms", "leetcode_alt")
-	runtime:endl()
-
-	runtime:append(
-		("%s %.2f%% "):format(t("Beats"), item.runtime_percentile),
-		perc_hi(item.runtime_percentile)
-	)
-
-	local lang = utils.get_lang_by_name(item.pretty_lang)
-	local lang_text = { item.pretty_lang, "Structure" }
-	if lang then
-		lang_text = { lang.icon .. " " .. lang.lang, lang.hl or "Structure" }
-	end
-
-	if config.translator then
-		runtime
-			:append("使用 ", "leetcode_normal")
-			:append(unpack(lang_text))
-			:append(" 的用户", "leetcode_normal")
-	else
-		runtime:append("of users with ", "leetcode_normal"):append(unpack(lang_text))
-	end
-
-	local runtime_title = Line():append("󰓅 " .. t("Runtime"))
-	self:insert(Pre(runtime_title, runtime))
-
-	--------------------------------------------
-	--- Memory
-	--------------------------------------------
-	local memory = Lines()
-	item.status_memory = item.status_memory:gsub("(%d+)%s*(%a+)", "%1 %2")
-	local s_mem = vim.split(item.status_memory, " ")
-	memory:append(s_mem[1] .. " "):append(s_mem[2], "leetcode_alt")
-	memory:endl()
-
-	memory:append(
-		("%s %.2f%% "):format(t("Beats"), item.memory_percentile),
-		perc_hi(item.memory_percentile)
-	)
-	if config.translator then
-		memory
-			:append("使用 ", "leetcode_normal")
-			:append(unpack(lang_text))
-			:append(" 的用户", "leetcode_normal")
-	else
-		memory:append("of users with ", "leetcode_normal"):append(unpack(lang_text))
-	end
-
-	local memory_title = Line():append("󰍛 " .. t("Memory"))
-	self:insert(Pre(memory_title, memory))
-
-	--------------------------------------------
-	--- More challenges
-	--------------------------------------------
-	if not vim.tbl_isempty(self.parent.question.q.similar or {}) then
-		local title_txt = (" %s"):format(t("More challenges"))
-		local more_title = Line():append(title_txt, "leetcode_normal")
-		local similar = SimilarQuestions(self.parent.question.q.similar)
-		self:insert(Pre(more_title, similar))
-	end
+    self:set_opts({ spacing = 2 })
+
+    local function perc_hi(perc) --
+        if perc >= 50 then
+            return "leetcode_ok"
+        elseif perc >= 10 then
+            return nil
+        elseif perc >= 0 then
+            return "leetcode_error"
+        end
+    end
+
+    local header = Header(item)
+    self:insert(header)
+
+    --------------------------------------------
+    --- Runtime
+    --------------------------------------------
+    local runtime = Lines()
+    local runtime_ms = item.display_runtime or vim.split(item.status_runtime, " ")[1] or "NIL"
+    runtime:append(runtime_ms):append(" ms", "leetcode_alt")
+    runtime:endl()
+
+    runtime:append(
+        ("%s %.2f%% "):format(t("Beats"), item.runtime_percentile),
+        perc_hi(item.runtime_percentile)
+    )
+
+    local lang = utils.get_lang_by_name(item.pretty_lang)
+    local lang_text = { item.pretty_lang, "Structure" }
+    if lang then
+        lang_text = { lang.icon .. " " .. lang.lang, lang.hl or "Structure" }
+    end
+
+    if config.translator then
+        runtime
+            :append("使用 ", "leetcode_normal")
+            :append(unpack(lang_text))
+            :append(" 的用户", "leetcode_normal")
+    else
+        runtime:append("of users with ", "leetcode_normal"):append(unpack(lang_text))
+    end
+
+    local runtime_title = Line():append("󰓅 " .. t("Runtime"))
+    self:insert(Pre(runtime_title, runtime))
+
+    --------------------------------------------
+    --- Memory
+    --------------------------------------------
+    local memory = Lines()
+    item.status_memory = item.status_memory:gsub("(%d+)%s*(%a+)", "%1 %2")
+    local s_mem = vim.split(item.status_memory, " ")
+    memory:append(s_mem[1] .. " "):append(s_mem[2], "leetcode_alt")
+    memory:endl()
+
+    memory:append(
+        ("%s %.2f%% "):format(t("Beats"), item.memory_percentile),
+        perc_hi(item.memory_percentile)
+    )
+    if config.translator then
+        memory
+            :append("使用 ", "leetcode_normal")
+            :append(unpack(lang_text))
+            :append(" 的用户", "leetcode_normal")
+    else
+        memory:append("of users with ", "leetcode_normal"):append(unpack(lang_text))
+    end
+
+    local memory_title = Line():append("󰍛 " .. t("Memory"))
+    self:insert(Pre(memory_title, memory))
+
+    --------------------------------------------
+    --- More challenges
+    --------------------------------------------
+    if not vim.tbl_isempty(self.parent.question.q.similar or {}) then
+        local title_txt = (" %s"):format(t("More challenges"))
+        local more_title = Line():append(title_txt, "leetcode_normal")
+        local similar = SimilarQuestions(self.parent.question.q.similar)
+        self:insert(Pre(more_title, similar))
+    end
 end
 
 ---@private
 ---
 ---@param item lc.runtime
 function ResultLayout:handle_runtime(item) -- status code = 10
-	if item._.submission then
-		return self:handle_accepted(item)
-	end
+    if item._.submission then
+        return self:handle_accepted(item)
+    end
 
-	local header = Header(item)
-	self:insert(header)
+    local header = Header(item)
+    self:insert(header)
 
-	local cases = Cases(item, self.parent)
-	self:insert(cases)
+    local cases = Cases(item, self.parent)
+    self:insert(cases)
 end
 
 ---@private
 ---
 ---@param item lc.submission
 function ResultLayout:handle_submission_error(item) -- status code = 11
-	local header = Header(item)
-	self:insert(header)
-
-	self:insert(Case({ ---@diagnostic disable-line
-		input = vim.split(item.input, "\n"),
-		raw_input = item.last_testcase,
-		output = item.code_output,
-		expected = item.expected_output,
-		std_output = item.std_output,
-	}, false, self.parent.question))
+    local header = Header(item)
+    self:insert(header)
+
+    self:insert(Case({ ---@diagnostic disable-line
+        input = vim.split(item.input, "\n"),
+        raw_input = item.last_testcase,
+        output = item.code_output,
+        expected = item.expected_output,
+        std_output = item.std_output,
+    }, false, self.parent.question))
 end
 
 ---@private
 ---
 ---@param item lc.limit_exceeded_error
 function ResultLayout:handle_limit_exceeded(item) -- status code = 12,13,14
-	local header = Header(item)
-	self:insert(header)
-
-	if item._.submission then
-		local input = Input(
-			(" %s"):format(t("Last Executed Input")),
-			vim.split(item.last_testcase, "\n"),
-			self.parent.question.q.meta_data.params
-		)
-		self:insert(input)
-
-		local stdout = Stdout(item.std_output or "")
-		self:insert(stdout)
-	elseif item.std_output_list then
-		local stdout = Stdout(item.std_output_list[#item.std_output_list])
-		self:insert(stdout)
-	end
+    local header = Header(item)
+    self:insert(header)
+
+    if item._.submission then
+        local input = Input(
+            (" %s"):format(t("Last Executed Input")),
+            vim.split(item.last_testcase, "\n"),
+            self.parent.question.q.meta_data.params
+        )
+        self:insert(input)
+
+        local stdout = Stdout(item.std_output or "")
+        self:insert(stdout)
+    elseif item.std_output_list then
+        local stdout = Stdout(item.std_output_list[#item.std_output_list])
+        self:insert(stdout)
+    end
 end
 
 ---@private
 ---
 ---@param item lc.runtime_error
 function ResultLayout:handle_runtime_error(item) -- status code = 15
-	local header = Header(item)
-
-	local lines = Lines()
-	for line in vim.gsplit(item.full_runtime_error, "\n") do
-		lines:append(line, "leetcode_error"):endl()
-	end
-	self:insert(Pre(header, lines))
-
-	if item._.submission then
-		local pre_header = Line()
-		pre_header:append((" %s"):format(t("Last Executed Input")), "leetcode_normal")
-
-		local last_testcase = Line()
-		last_testcase:append(item.last_testcase:gsub("\n", " "), "leetcode_indent")
-
-		local last_exec = Pre(pre_header, last_testcase)
-		self:insert(last_exec)
-
-		local stdout = Stdout(item.std_output or "")
-		self:insert(stdout)
-	elseif item.std_output_list then
-		local stdout = Stdout(item.std_output_list[#item.std_output_list])
-		self:insert(stdout)
-	end
+    local header = Header(item)
+
+    local lines = Lines()
+    for line in vim.gsplit(item.full_runtime_error, "\n") do
+        lines:append(line, "leetcode_error"):endl()
+    end
+    self:insert(Pre(header, lines))
+
+    if item._.submission then
+        local pre_header = Line()
+        pre_header:append((" %s"):format(t("Last Executed Input")), "leetcode_normal")
+
+        local last_testcase = Line()
+        last_testcase:append(item.last_testcase:gsub("\n", " "), "leetcode_indent")
+
+        local last_exec = Pre(pre_header, last_testcase)
+        self:insert(last_exec)
+
+        local stdout = Stdout(item.std_output or "")
+        self:insert(stdout)
+    elseif item.std_output_list then
+        local stdout = Stdout(item.std_output_list[#item.std_output_list])
+        self:insert(stdout)
+    end
 end
 
 function ResultLayout:handle_internal_error(item) -- status code = 16
-	local header = Header(item)
-	self:insert(header)
+    local header = Header(item)
+    self:insert(header)
 end
 
 ---@private
 ---
 ---@param item lc.compile_error
 function ResultLayout:handle_compile_error(item) -- status code = 20
-	local header = Header(item)
+    local header = Header(item)
 
-	local lines = Lines()
-	for line in vim.gsplit(item.full_compile_error, "\n") do
-		lines:append(line, "leetcode_error"):endl()
-	end
+    local lines = Lines()
+    for line in vim.gsplit(item.full_compile_error, "\n") do
+        lines:append(line, "leetcode_error"):endl()
+    end
 
-	local pre = Pre(header, lines)
-	self:insert(pre)
+    local pre = Pre(header, lines)
+    self:insert(pre)
 end
 
 ---@param item lc.interpreter_response
 function ResultLayout:handle_res(item)
-	self:clear()
-	self:set_opts({ spacing = 1 })
-	Markup.log(item)
-
-	local handlers = {
-		-- runtime
-		[10] = function()
-			self:handle_runtime(item --[[@as lc.runtime]])
-		end,
-		[11] = function()
-			self:handle_submission_error(item --[[@as lc.submission]])
-		end,
-
-		-- memory limit
-		[12] = function()
-			self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
-		end,
-		-- time limit
-		[13] = function()
-			self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
-		end,
-		-- output limit
-		[14] = function()
-			self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
-		end,
-
-		-- runtime error
-		[15] = function()
-			self:handle_runtime_error(item --[[@as lc.runtime_error]])
-		end,
-
-		-- internal error
-		[16] = function()
-			self:handle_internal_error(item --[[@as lc.internal_error]])
-		end,
-
-		-- compiler
-		[20] = function()
-			self:handle_compile_error(item --[[@as lc.compile_error]])
-		end,
-
-		-- unknown
-		["unknown"] = function()
-			log.error("unknown runner status code: " .. item.status_code)
-		end,
-	}
-
-	local handler = handlers[item.status_code]
-	if handler then
-		handler()
-	else
-		handlers["unknown"]()
-	end
+    self:clear()
+    self:set_opts({ spacing = 1 })
+
+    local handlers = {
+        -- runtime
+        [10] = function()
+            self:handle_runtime(item --[[@as lc.runtime]])
+        end,
+        [11] = function()
+            self:handle_submission_error(item --[[@as lc.submission]])
+        end,
+
+        -- memory limit
+        [12] = function()
+            self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
+        end,
+        -- time limit
+        [13] = function()
+            self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
+        end,
+        -- output limit
+        [14] = function()
+            self:handle_limit_exceeded(item --[[@as lc.limit_exceeded_error]])
+        end,
+
+        -- runtime error
+        [15] = function()
+            self:handle_runtime_error(item --[[@as lc.runtime_error]])
+        end,
+
+        -- internal error
+        [16] = function()
+            self:handle_internal_error(item --[[@as lc.internal_error]])
+        end,
+
+        -- compiler
+        [20] = function()
+            self:handle_compile_error(item --[[@as lc.compile_error]])
+        end,
+
+        -- unknown
+        ["unknown"] = function()
+            log.error("unknown runner status code: " .. item.status_code)
+        end,
+    }
+
+    local handler = handlers[item.status_code]
+    if handler then
+        handler()
+    else
+        handlers["unknown"]()
+    end
 end
 
 ---@param parent lc.ui.Console
 function ResultLayout:init(parent)
-	ResultLayout.super.init(self, {}, { spacing = 1 })
+    ResultLayout.super.init(self, {}, { spacing = 1 })
 
-	self.parent = parent
+    self.parent = parent
 end
 
 ---@type fun(parent: lc.ui.Console): lc.ui.Result
diff --git a/lua/leetcode/api/problems.lua b/lua/leetcode/api/problems.lua
index 0139654a..32bf7e83 100644
--- a/lua/leetcode/api/problems.lua
+++ b/lua/leetcode/api/problems.lua
@@ -11,243 +11,243 @@ local M = {}
 ---
 ---@return leet.cache.problem[], lc.err
 function M.all(cb, noti)
-	local endpoint = urls.problems:format("algorithms")
-
-	local spinner
-	if noti then
-		spinner = Spinner:start("updating problemlist cache", "points")
-	end
-
-	if cb then
-		Leet.api.get(endpoint, {
-			callback = function(res, err)
-				if err then
-					if spinner then
-						spinner:error(err.msg)
-					end
-					return cb(nil, err)
-				end
-
-				local problems = Leet.api.problems.normalize(res.stat_status_pairs)
-
-				if config.is_cn then
-					if spinner then
-						spinner:update("fetching title translations")
-					end
-					M.translated_titles(function(titles, terr)
-						if terr then
-							if spinner then
-								spinner:error(terr.msg)
-							end
-							return cb(nil, terr)
-						end
-
-						problems = Leet.api.translate_titles(problems, titles)
-						if spinner then
-							spinner:success("cache updated")
-						end
-
-						cb(problems)
-					end)
-				else
-					if spinner then
-						spinner:success("cache updated")
-					end
-
-					cb(problems)
-				end
-			end,
-		})
-	else
-		local res, err = Leet.api.get(endpoint)
-		if err then
-			if spinner then
-				spinner:error(err.msg)
-			end
-			return nil, err
-		end
-
-		local problems = Leet.api.problems.normalize(res.stat_status_pairs)
-
-		if config.is_cn then
-			local titles, terr = M.translated_titles()
-			if terr then
-				if spinner then
-					spinner:error(terr.msg)
-				end
-				return nil, terr
-			end
-
-			if spinner then
-				spinner:success("problems cache updated")
-			end
-			return Leet.api.translate_titles(problems, titles)
-		else
-			if spinner then
-				spinner:success("problems cache updated")
-			end
-			return problems
-		end
-	end
+    local endpoint = urls.problems:format("algorithms")
+
+    local spinner
+    if noti then
+        spinner = Spinner:start("updating problemlist cache", "points")
+    end
+
+    if cb then
+        Leet.api.get(endpoint, {
+            callback = function(res, err)
+                if err then
+                    if spinner then
+                        spinner:error(err.msg)
+                    end
+                    return cb(nil, err)
+                end
+
+                local problems = Leet.api.problems.normalize(res.stat_status_pairs)
+
+                if config.is_cn then
+                    if spinner then
+                        spinner:update("fetching title translations")
+                    end
+                    M.translated_titles(function(titles, terr)
+                        if terr then
+                            if spinner then
+                                spinner:error(terr.msg)
+                            end
+                            return cb(nil, terr)
+                        end
+
+                        problems = Leet.api.translate_titles(problems, titles)
+                        if spinner then
+                            spinner:success("cache updated")
+                        end
+
+                        cb(problems)
+                    end)
+                else
+                    if spinner then
+                        spinner:success("cache updated")
+                    end
+
+                    cb(problems)
+                end
+            end,
+        })
+    else
+        local res, err = Leet.api.get(endpoint)
+        if err then
+            if spinner then
+                spinner:error(err.msg)
+            end
+            return nil, err
+        end
+
+        local problems = Leet.api.problems.normalize(res.stat_status_pairs)
+
+        if config.is_cn then
+            local titles, terr = M.translated_titles()
+            if terr then
+                if spinner then
+                    spinner:error(terr.msg)
+                end
+                return nil, terr
+            end
+
+            if spinner then
+                spinner:success("problems cache updated")
+            end
+            return Leet.api.translate_titles(problems, titles)
+        else
+            if spinner then
+                spinner:success("problems cache updated")
+            end
+            return problems
+        end
+    end
 end
 
 function M.question_of_today(cb)
-	local query = queries.qot
-
-	Leet.api.query(query, {}, {
-		callback = function(res, err)
-			if err then
-				return cb(nil, err)
-			end
-
-			local tday_record = res.data["todayRecord"]
-			local question = config.is_cn and tday_record[1].question or tday_record.question
-			cb(question)
-		end,
-	})
+    local query = queries.qot
+
+    Leet.api.query(query, {}, {
+        callback = function(res, err)
+            if err then
+                return cb(nil, err)
+            end
+
+            local tday_record = res.data["todayRecord"]
+            local question = config.is_cn and tday_record[1].question or tday_record.question
+            cb(question)
+        end,
+    })
 end
 
 function M.translated_titles(cb)
-	local query = queries.translations
-
-	if cb then
-		Leet.api.query(query, {}, {
-			callback = function(res, err)
-				if err then
-					return cb(nil, err)
-				end
-				cb(res.data.translations)
-			end,
-		})
-	else
-		local res, err = utils.query(query, {})
-		if err then
-			return nil, err
-		end
-		return res.data.translations
-	end
+    local query = queries.translations
+
+    if cb then
+        Leet.api.query(query, {}, {
+            callback = function(res, err)
+                if err then
+                    return cb(nil, err)
+                end
+                cb(res.data.translations)
+            end,
+        })
+    else
+        local res, err = utils.query(query, {})
+        if err then
+            return nil, err
+        end
+        return res.data.translations
+    end
 end
 
 ---@param filters? table
 function M.random(filters)
-	local variables = {
-		categorySlug = "algorithms",
-		filters = filters or vim.empty_dict(),
-	}
+    local variables = {
+        categorySlug = "algorithms",
+        filters = filters or vim.empty_dict(),
+    }
 
-	local query = queries.random_question
+    local query = queries.random_question
 
-	local res, err = Leet.api.query(query, variables)
-	if err then
-		return nil, err
-	end
+    local res, err = Leet.api.query(query, variables)
+    if err then
+        return nil, err
+    end
 
-	local q = res.data.randomQuestion
+    local q = res.data.randomQuestion
 
-	if q == vim.NIL then
-		local msg = "Random question fetch responded with `null`"
+    if q == vim.NIL then
+        local msg = "Random question fetch responded with `null`"
 
-		if filters then
-			msg = msg .. ".\n\nMaybe invalid filters?\n" .. vim.inspect(filters)
-		end
+        if filters then
+            msg = msg .. ".\n\nMaybe invalid filters?\n" .. vim.inspect(filters)
+        end
 
-		return nil, { msg = msg, lvl = vim.log.levels.ERROR }
-	end
+        return nil, { msg = msg, lvl = vim.log.levels.ERROR }
+    end
 
-	if config.is_cn then
-		q = {
-			title_slug = q,
-			paid_only = Leet.cache.problems.by_slug(q).paid_only,
-		}
-	end
+    if config.is_cn then
+        q = {
+            title_slug = q,
+            paid_only = Leet.cache.problems.by_slug(q).paid_only,
+        }
+    end
 
-	if not config.auth.is_premium and q.paid_only then
-		err = err or {}
-		err.msg = "Drawn question is for premium users only. Please try again"
-		err.lvl = vim.log.levels.WARN
-		return nil, err
-	end
+    if not config.auth.is_premium and q.paid_only then
+        err = err or {}
+        err.msg = "Drawn question is for premium users only. Please try again"
+        err.lvl = vim.log.levels.WARN
+        return nil, err
+    end
 
-	return q
+    return q
 end
 
 ---@param qid integer
 ---@param lang lc.lang
 ---@param cb function
 function M.latest_submission(qid, lang, cb)
-	local url = urls.latest_submission:format(qid, lang)
-	Leet.api.get(url, { callback = cb })
+    local url = urls.latest_submission:format(qid, lang)
+    Leet.api.get(url, { callback = cb })
 end
 
 ---@param title_slug string
 ---
 ---@return lc.question_res|nil
 function M.by_slug(title_slug)
-	local variables = {
-		titleSlug = title_slug,
-	}
+    local variables = {
+        titleSlug = title_slug,
+    }
 
-	local query = queries.question
+    local query = queries.question
 
-	local res, err = Leet.api.query(query, variables)
-	if not res or err then
-		return log.err(err)
-	end
+    local res, err = Leet.api.query(query, variables)
+    if not res or err then
+        return log.err(err)
+    end
 
-	local q = res.data.question
-	q.meta_data = vim.json.decode(q.meta_data)
-	q.stats = vim.json.decode(q.stats)
-	if type(q.similar) == "string" then
-		q.similar = Leet.api.normalize_similar_cn(q.similar)
-	end
+    local q = res.data.question
+    q.meta_data = vim.json.decode(q.meta_data)
+    q.stats = vim.json.decode(q.stats)
+    if type(q.similar) == "string" then
+        q.similar = Leet.api.normalize_similar_cn(q.similar)
+    end
 
-	return q
+    return q
 end
 
 ---@return leet.cache.problem[]
 function M.normalize(problems)
-	problems = vim.tbl_filter(function(p)
-		return not p.stat.question__hide
-	end, problems)
-
-	local comp = function(a, b)
-		local a_fid = a.stat.frontend_question_id
-		local b_fid = b.stat.frontend_question_id
-
-		local is_num_a = tonumber(a_fid)
-		local is_num_b = tonumber(b_fid)
-
-		if is_num_a and is_num_b then
-			return tonumber(a_fid) < tonumber(b_fid)
-		elseif is_num_a then
-			return true
-		elseif is_num_b then
-			return false
-		else
-			return a_fid < b_fid
-		end
-	end
-	table.sort(problems, comp)
-
-	return vim.tbl_map(function(p)
-		return {
-			status = p.status == vim.NIL and "todo" or p.status, -- api returns nil for todo
-			id = p.stat.question_id,
-			frontend_id = p.stat.frontend_question_id,
-			title = p.stat.question__title,
-			title_cn = "",
-			title_slug = p.stat.question__title_slug,
-			link = ("https://leetcode.%s/problems/%s/"):format(
-				config.domain,
-				p.stat.question__title_slug
-			),
-			paid_only = p.paid_only,
-			ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1),
-			difficulty = ({ "Easy", "Medium", "Hard" })(p.difficulty.level),
-			starred = p.is_favor,
-			topic_tags = {},
-		}
-	end, problems)
+    problems = vim.tbl_filter(function(p)
+        return not p.stat.question__hide
+    end, problems)
+
+    local comp = function(a, b)
+        local a_fid = a.stat.frontend_question_id
+        local b_fid = b.stat.frontend_question_id
+
+        local is_num_a = tonumber(a_fid)
+        local is_num_b = tonumber(b_fid)
+
+        if is_num_a and is_num_b then
+            return tonumber(a_fid) < tonumber(b_fid)
+        elseif is_num_a then
+            return true
+        elseif is_num_b then
+            return false
+        else
+            return a_fid < b_fid
+        end
+    end
+    table.sort(problems, comp)
+
+    return vim.tbl_map(function(p)
+        return {
+            status = p.status == vim.NIL and "todo" or p.status, -- api returns nil for todo
+            id = p.stat.question_id,
+            frontend_id = p.stat.frontend_question_id,
+            title = p.stat.question__title,
+            title_cn = "",
+            title_slug = p.stat.question__title_slug,
+            link = ("https://leetcode.%s/problems/%s/"):format(
+                config.domain,
+                p.stat.question__title_slug
+            ),
+            paid_only = p.paid_only,
+            ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1),
+            difficulty = ({ "Easy", "Medium", "Hard" })[p.difficulty.level],
+            starred = p.is_favor,
+            topic_tags = {},
+        }
+    end, problems)
 end
 
 return M
diff --git a/lua/leetcode/api/types.lua b/lua/leetcode/api/types.lua
index 7cd2f84d..feee3401 100644
--- a/lua/leetcode/api/types.lua
+++ b/lua/leetcode/api/types.lua
@@ -9,6 +9,7 @@
 ---@field hl string
 ---@field submission boolean
 ---@field success boolean
+---@field params? string[]
 
 ---@class lc.Interpreter.Response
 ---@field status_code integer
diff --git a/lua/leetcode/problem/console/result.lua b/lua/leetcode/problem/console/result.lua
index 4335c74f..f1c8f511 100644
--- a/lua/leetcode/problem/console/result.lua
+++ b/lua/leetcode/problem/console/result.lua
@@ -21,12 +21,12 @@ function M:clear()
     vim.api.nvim_buf_set_lines(self.win.buf, 0, -1, false, {})
 end
 
+---@param item lc.interpreter_response
 function M:handle(item)
-    -- self.renderer:render({
-    -- 	m.block("123")
-    -- })
-    local renderer = Markup.renderer({ show = false })
+    local renderer = Markup.renderer()
     renderer.win:show()
+    Markup.log(item.status_code)
+    item._.params = self.console.problem.problem.meta_data.params
     renderer:render(Result({ item = item, console = self.console }))
 end
 
diff --git a/lua/leetcode/problem/console/testcase.lua b/lua/leetcode/problem/console/testcase.lua
index c647519f..c4842406 100644
--- a/lua/leetcode/problem/console/testcase.lua
+++ b/lua/leetcode/problem/console/testcase.lua
@@ -17,10 +17,11 @@ function M:new(console)
         file = self:path(),
         bo = {
             modifiable = true,
+            buftype = "nofile",
         },
-        on_buf = function()
-            -- self:handle_buf()
-        end,
+        -- on_buf = function()
+        --     -- self:handle_buf()
+        -- end,
     })
     self.win:on({
         "TextChanged",
diff --git a/lua/leetcode/problem/init.lua b/lua/leetcode/problem/init.lua
index 2943f0ce..4f635dfd 100644
--- a/lua/leetcode/problem/init.lua
+++ b/lua/leetcode/problem/init.lua
@@ -14,329 +14,329 @@ local M = Markup.object()
 ---@param cached leet.cache.problem
 ---@param problem leet.cache.problem
 function M:new(cached, problem)
-	self.lang = config.lang
-	self.cached = cached
-	self.problem = problem
+    self.lang = config.lang
+    self.cached = cached
+    self.problem = problem
 
-	table.insert(Leet.active_problems, self)
-	self:open_buf()
+    table.insert(Leet.active_problems, self)
+    self:open_buf()
 
-	self.description = Description(self)
-	self.console = Console(self)
+    -- self.description = Description(self)
+    self.console = Console(self)
 end
 
 function M:tabpage()
-	return vim.api.nvim_win_get_tabpage(self.win)
+    return vim.api.nvim_win_get_tabpage(self.win)
 end
 
 function M:valid()
-	if not self.buf or not vim.api.nvim_buf_is_valid(self.buf) then
-		return false
-	end
-	if not self.win or not vim.api.nvim_win_is_valid(self.win) then
-		return false
-	end
-	return true
+    if not self.buf or not vim.api.nvim_buf_is_valid(self.buf) then
+        return false
+    end
+    if not self.win or not vim.api.nvim_win_is_valid(self.win) then
+        return false
+    end
+    return true
 end
 
 function M:open_buf()
-	if self.buf and vim.api.nvim_buf_is_valid(self.buf) then
-		return
-	end
-
-	local path, existed = self:path()
-	vim.cmd("$tabe " .. path)
-	self.buf = vim.api.nvim_get_current_buf()
-	self.win = vim.api.nvim_get_current_win()
-	vim.wo[self.win].winfixbuf = true
-
-	vim.bo[self.buf].buflisted = true
-	vim.api.nvim_win_set_buf(self.win, self.buf)
-
-	if config.user.editor.fold_imports then
-		self:editor_fold_imports(false)
-	end
-
-	if config.user.editor.reset_previous_code and (existed and self.cached.status == "ac") then
-		self:reset_previous_code()
-	end
+    if self.buf and vim.api.nvim_buf_is_valid(self.buf) then
+        return
+    end
+
+    local path, existed = self:path()
+    vim.cmd("$tabe " .. path)
+    self.buf = vim.api.nvim_get_current_buf()
+    self.win = vim.api.nvim_get_current_win()
+    vim.wo[self.win].winfixbuf = true
+
+    vim.bo[self.buf].buflisted = true
+    vim.api.nvim_win_set_buf(self.win, self.buf)
+
+    if config.user.editor.fold_imports then
+        self:editor_fold_imports(false)
+    end
+
+    if config.user.editor.reset_previous_code and (existed and self.cached.status == "ac") then
+        self:reset_previous_code()
+    end
 end
 
 function M:filename(ft)
-	local lang = Leet.util.get_lang(self.lang)
-	local alt = lang.alt and ("." .. lang.alt) or ""
-
-	return ("%s.%s%s.%s"):format(
-		self.problem.frontend_id,
-		self.problem.title_slug,
-		alt,
-		ft or lang.ft
-	)
+    local lang = Leet.util.get_lang(self.lang)
+    local alt = lang.alt and ("." .. lang.alt) or ""
+
+    return ("%s.%s%s.%s"):format(
+        self.problem.frontend_id,
+        self.problem.title_slug,
+        alt,
+        ft or lang.ft
+    )
 end
 
 ---@return string path, boolean existed
 function M:path()
-	local fn = self:filename()
-	self.file = config.storage.home:joinpath(fn)
-	local existed = self.file:exists()
+    local fn = self:filename()
+    self.file = config.storage.home:joinpath(fn)
+    local existed = self.file:exists()
 
-	if not existed then
-		self.file:write(self:snippet(), "w")
-	end
+    if not existed then
+        self.file:write(self:snippet(), "w")
+    end
 
-	return self.file:absolute(), existed
+    return self.file:absolute(), existed
 end
 
 ---@param raw? boolean
 function M:snippet(raw)
-	local snippets = self.problem.code_snippets ~= vim.NIL and self.problem.code_snippets or {}
-	local snip = vim.tbl_filter(function(snip)
-		return snip.lang_slug == self.lang
-	end, snippets)[1]
-	if not snip then
-		return
-	end
-
-	local code = snip.code:gsub("\r\n", "\n")
-	return raw and code or self:injector(code)
+    local snippets = self.problem.code_snippets ~= vim.NIL and self.problem.code_snippets or {}
+    local snip = vim.tbl_filter(function(snip)
+        return snip.lang_slug == self.lang
+    end, snippets)[1]
+    if not snip then
+        return
+    end
+
+    local code = snip.code:gsub("\r\n", "\n")
+    return raw and code or self:injector(code)
 end
 
 ---@param code string
 function M:injector(code)
-	local inject = config.user.injector[self.lang] or {}
+    local inject = config.user.injector[self.lang] or {}
 
-	local parts = { self:editor_section(code, "code") }
+    local parts = { self:editor_section(code, "code") }
 
-	local before = self:inject(true)
-	if before then
-		table.insert(parts, 1, before)
-	end
+    local before = self:inject(true)
+    if before then
+        table.insert(parts, 1, before)
+    end
 
-	local imports = self:inject_imports()
-	if imports then
-		table.insert(parts, 1, self:editor_section(table.concat(imports, "\n"), "imports"))
-	end
+    local imports = self:inject_imports()
+    if imports then
+        table.insert(parts, 1, self:editor_section(table.concat(imports, "\n"), "imports"))
+    end
 
-	local after = self:inject(false)
-	if after then
-		table.insert(parts, after)
-	end
+    local after = self:inject(false)
+    if after then
+        table.insert(parts, after)
+    end
 
-	local gap = (inject.gap or 1) + 1
-	return table.concat(parts, ("\n"):rep(gap))
+    local gap = (inject.gap or 1) + 1
+    return table.concat(parts, ("\n"):rep(gap))
 end
 
 ---@param before boolean
 ---@return string?
 function M:inject(before)
-	local inject = config.user.injector[self.lang] or {}
-	local inj = before and (inject.before or {}) or (inject.after or {})
-
-	local res
-	if type(inj) == "table" then
-		res = table.concat(inj, "\n")
-	elseif type(inj) == "string" then
-		res = inj
-	end
-
-	if res and res ~= "" then
-		return res
-	else
-		return nil
-	end
+    local inject = config.user.injector[self.lang] or {}
+    local inj = before and (inject.before or {}) or (inject.after or {})
+
+    local res
+    if type(inj) == "table" then
+        res = table.concat(inj, "\n")
+    elseif type(inj) == "string" then
+        res = inj
+    end
+
+    if res and res ~= "" then
+        return res
+    else
+        return nil
+    end
 end
 
 ---@return string[]?
 function M:inject_imports()
-	local inject = config.user.injector[self.lang] or {}
-
-	local imports = inject.imports
-	local default_imports = config.imports[self.lang]
-
-	local function valid_imports(tbl)
-		if vim.tbl_isempty(tbl or {}) then
-			return false
-		end
-		if not vim.islist(tbl) then
-			Markup.log.error("Invalid imports format for language: " .. self.lang)
-			return false
-		end
-		return true
-	end
-
-	if not imports then
-		return default_imports
-	end
-
-	if type(imports) == "function" then
-		local overriden = imports(vim.deepcopy(default_imports or {}))
-		if not valid_imports(overriden) then
-			return default_imports
-		end
-		return overriden
-	end
-
-	local resolved = type(imports) == "string" and { imports } or imports
-	if not valid_imports(resolved) then
-		return
-	end
-
-	local merged, seen = {}, {}
-	local function add(list)
-		for _, v in ipairs(list) do
-			if not seen[v] then
-				table.insert(merged, v)
-				seen[v] = true
-			end
-		end
-	end
-
-	add(default_imports)
-	add(resolved)
-
-	return not vim.tbl_isempty(merged) and merged or nil
+    local inject = config.user.injector[self.lang] or {}
+
+    local imports = inject.imports
+    local default_imports = config.imports[self.lang]
+
+    local function valid_imports(tbl)
+        if vim.tbl_isempty(tbl or {}) then
+            return false
+        end
+        if not vim.islist(tbl) then
+            Markup.log.error("Invalid imports format for language: " .. self.lang)
+            return false
+        end
+        return true
+    end
+
+    if not imports then
+        return default_imports
+    end
+
+    if type(imports) == "function" then
+        local overriden = imports(vim.deepcopy(default_imports or {}))
+        if not valid_imports(overriden) then
+            return default_imports
+        end
+        return overriden
+    end
+
+    local resolved = type(imports) == "string" and { imports } or imports
+    if not valid_imports(resolved) then
+        return
+    end
+
+    local merged, seen = {}, {}
+    local function add(list)
+        for _, v in ipairs(list) do
+            if not seen[v] then
+                table.insert(merged, v)
+                seen[v] = true
+            end
+        end
+    end
+
+    add(default_imports)
+    add(resolved)
+
+    return not vim.tbl_isempty(merged) and merged or nil
 end
 
 ---@param lines string|string[]
 ---@param name lc.editor.section
 ---@return string
 function M:editor_section(lines, name)
-	local comment = Leet.util.get_lang(self.lang).comment
+    local comment = Leet.util.get_lang(self.lang).comment
 
-	local start_tag = comment .. " " .. Leet.util.section_tag(name, true)
-	local end_tag = comment .. " " .. Leet.util.section_tag(name, false)
+    local start_tag = comment .. " " .. Leet.util.section_tag(name, true)
+    local end_tag = comment .. " " .. Leet.util.section_tag(name, false)
 
-	local str = type(lines) ~= "string" and table.concat(lines, "\n") or lines
-	return table.concat({ start_tag, str, end_tag }, "\n")
+    local str = type(lines) ~= "string" and table.concat(lines, "\n") or lines
+    return table.concat({ start_tag, str, end_tag }, "\n")
 end
 
 ---@param name string
 ---@param inclusive? boolean
 function M:editor_section_range(name, inclusive)
-	local lines = vim.api.nvim_buf_get_lines(self.buf, 0, -1, false)
-	local start_i, end_i
-
-	local start_tag = Leet.util.section_tag(name, true)
-	local end_tag = Leet.util.section_tag(name, false)
-
-	for i, line in ipairs(lines) do
-		if line:match(start_tag) then
-			start_i = i + (inclusive and 0 or 1)
-		elseif line:match(end_tag) then
-			end_i = i - (inclusive and 0 or 1)
-		end
-	end
-
-	local res = {
-		start_i = start_i,
-		end_i = end_i,
-		lines = lines,
-		complete = start_i and end_i,
-	}
-
-	res.log_not_found = function()
-		if res.complete then
-			return
-		end
-
-		local missing = {}
-		if not res.start_i then
-			table.insert(missing, ("`%s`"):format(start_tag))
-		end
-		if not res.end_i then
-			table.insert(missing, ("`%s`"):format(end_tag))
-		end
-
-		Markup.log.warn(table.concat(missing, " and ") .. " not found.")
-	end
-
-	res.is_partial = function()
-		return res.complete or (res.start_i or res.end_i)
-	end
-
-	res.is_valid_or_log = function()
-		if res.complete then
-			return true
-		else
-			res.log_not_found()
-			return false
-		end
-	end
-
-	return res
+    local lines = vim.api.nvim_buf_get_lines(self.buf, 0, -1, false)
+    local start_i, end_i
+
+    local start_tag = Leet.util.section_tag(name, true)
+    local end_tag = Leet.util.section_tag(name, false)
+
+    for i, line in ipairs(lines) do
+        if line:match(start_tag) then
+            start_i = i + (inclusive and 0 or 1)
+        elseif line:match(end_tag) then
+            end_i = i - (inclusive and 0 or 1)
+        end
+    end
+
+    local res = {
+        start_i = start_i,
+        end_i = end_i,
+        lines = lines,
+        complete = start_i and end_i,
+    }
+
+    res.log_not_found = function()
+        if res.complete then
+            return
+        end
+
+        local missing = {}
+        if not res.start_i then
+            table.insert(missing, ("`%s`"):format(start_tag))
+        end
+        if not res.end_i then
+            table.insert(missing, ("`%s`"):format(end_tag))
+        end
+
+        Markup.log.warn(table.concat(missing, " and ") .. " not found.")
+    end
+
+    res.is_partial = function()
+        return res.complete or (res.start_i or res.end_i)
+    end
+
+    res.is_valid_or_log = function()
+        if res.complete then
+            return true
+        else
+            res.log_not_found()
+            return false
+        end
+    end
+
+    return res
 end
 
 function M:editor_yank_code()
-	if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then
-		return
-	end
-
-	local range = self:editor_section_range("code")
-	if range:is_valid_or_log() then
-		vim.api.nvim_buf_call(self.buf, function()
-			vim.cmd(("%d,%dyank"):format(range.start_i, range.end_i))
-		end)
-	end
+    if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then
+        return
+    end
+
+    local range = self:editor_section_range("code")
+    if range:is_valid_or_log() then
+        vim.api.nvim_buf_call(self.buf, function()
+            vim.cmd(("%d,%dyank"):format(range.start_i, range.end_i))
+        end)
+    end
 end
 
 ---@param strict? boolean
 function M:editor_fold_imports(strict)
-	if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then
-		return
-	end
-
-	local range = self:editor_section_range("imports", true)
-	if range.complete or range.end_i then
-		vim.api.nvim_buf_call(self.buf, function()
-			pcall(vim.cmd, ("%d,%dfold"):format(range.start_i or 1, range.end_i)) ---@diagnostic disable-line: param-type-mismatch
-		end)
-	elseif strict then
-		range.log_not_found()
-	end
+    if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then
+        return
+    end
+
+    local range = self:editor_section_range("imports", true)
+    if range.complete or range.end_i then
+        vim.api.nvim_buf_call(self.buf, function()
+            pcall(vim.cmd, ("%d,%dfold"):format(range.start_i or 1, range.end_i)) ---@diagnostic disable-line: param-type-mismatch
+        end)
+    elseif strict then
+        range.log_not_found()
+    end
 end
 
 ---@param start_i integer
 ---@param end_i integer
 ---@param lines string[]|string
 function M:editor_set_lines(start_i, end_i, lines)
-	if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then
-		return
-	end
+    if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then
+        return
+    end
 
-	lines = type(lines) == "string" and vim.split(lines, "\n") or lines ---@cast lines string[]
-	vim.api.nvim_buf_set_lines(self.buf, start_i, end_i, false, lines)
+    lines = type(lines) == "string" and vim.split(lines, "\n") or lines ---@cast lines string[]
+    vim.api.nvim_buf_set_lines(self.buf, start_i, end_i, false, lines)
 end
 
 ---@param lines string[]|string
 ---@param name string
 function M:editor_section_replace(lines, name)
-	local range = self:editor_section_range(name)
+    local range = self:editor_section_range(name)
 
-	if range:is_valid_or_log() then
-		self:editor_set_lines(range.start_i - 1, range.end_i, lines)
-	end
+    if range:is_valid_or_log() then
+        self:editor_set_lines(range.start_i - 1, range.end_i, lines)
+    end
 end
 
 function M:editor_reset_code()
-	local new_lines = self:snippet(true) or ""
-	self:editor_section_replace(new_lines, "code")
+    local new_lines = self:snippet(true) or ""
+    self:editor_section_replace(new_lines, "code")
 end
 
 ---@param submit boolean
 ---@return string
 function M:editor_submit_lines(submit)
-	local range = self:editor_section_range("code")
-	assert(range.complete, "Code section not found")
+    local range = self:editor_section_range("code")
+    assert(range.complete, "Code section not found")
 
-	local prefix = not submit and ("\n"):rep(range.start_i - 1) or ""
-	return prefix .. table.concat(range.lines, "\n", range.start_i, range.end_i)
+    local prefix = not submit and ("\n"):rep(range.start_i - 1) or ""
+    return prefix .. table.concat(range.lines, "\n", range.start_i, range.end_i)
 end
 
 function M:reset_previous_code()
-	self:editor_reset_code()
-	vim.schedule(function()
-		Markup.log("Previous code found and reset. To undo, simply press `u` or use `:undo`.")
-	end)
+    self:editor_reset_code()
+    vim.schedule(function()
+        Markup.log("Previous code found and reset. To undo, simply press `u` or use `:undo`.")
+    end)
 end
 
 return M
diff --git a/lua/leetcode/ui/problem/result/cases/block.lua b/lua/leetcode/ui/problem/result/cases/block.lua
new file mode 100644
index 00000000..5fe152c1
--- /dev/null
+++ b/lua/leetcode/ui/problem/result/cases/block.lua
@@ -0,0 +1,11 @@
+local m = require("markup")
+
+local CaseBlock = m.component(function(props)
+    return m.block({
+        padding = { 1, 2 },
+        style = "LspInlayHint",
+        children = m.button(props.children),
+    })
+end)
+
+return CaseBlock
diff --git a/lua/leetcode/ui/problem/result/cases/case.lua b/lua/leetcode/ui/problem/result/cases/case.lua
index ada29d8a..4d87a5f7 100644
--- a/lua/leetcode/ui/problem/result/cases/case.lua
+++ b/lua/leetcode/ui/problem/result/cases/case.lua
@@ -1,46 +1,20 @@
 local m = require("markup")
+local CaseBlock = require("leetcode.ui.problem.result.cases.block")
+local Stdout = require("leetcode.ui.problem.result.stdout")
+local Input = require("leetcode.ui.problem.result.input")
 
-local CaseBlock = m.component(function(props)
-    return m.block({
-        padding = { 1, 2 },
-        style = "LspInlayHint",
-        children = props.children,
-    })
-end)
-
+---@param props { input: string[], output: string, expected: string, stdout: string, params: string[] }
 local Case = m.component(function(props)
-    local input = Markup.list(props.input)
+    local expanded, set_expanded = m.hooks.variable(true)
+
     local output = props.output
     local expected = props.expected
-    local std_output = props.std_output
-    local console = props.console ---@type leet.problem.console
-    local params = console.problem.problem.meta_data.params
-
-    local std_lines = Markup.list(vim.split(std_output or "", "\n", { trimempty = true }))
+    local params = props.params
 
     return m.block({
         spacing = 1,
-        m.block({
-            m.inline("Input"),
-            m.block({
-                spacing = 1,
-                input:map(function(case, i)
-                    local ok, param = pcall(function()
-                        return params[i].name
-                    end)
-                    return CaseBlock({
-                        ok and m.block(param .. " ="),
-                        m.block(case),
-                    })
-                end),
-            }),
-        }),
-        m.block({
-            m.inline(" Stdout"),
-            CaseBlock(std_lines:map(function(line)
-                return m.block(line)
-            end)),
-        }),
+        Input({ input = props.input, params = params }),
+        Stdout({ stdout = props.stdout }),
         m.block({
             m.inline("Output"),
             CaseBlock(output),
diff --git a/lua/leetcode/ui/problem/result/cases/init.lua b/lua/leetcode/ui/problem/result/cases/init.lua
index 7ef1bcaf..77ed2135 100644
--- a/lua/leetcode/ui/problem/result/cases/init.lua
+++ b/lua/leetcode/ui/problem/result/cases/init.lua
@@ -31,7 +31,7 @@ local Cases = m.component(function(props)
             input = case,
             output = item.code_answer[i],
             expected = item.expected_code_answer[i],
-            std_output = item.std_output_list[i],
+            stdout = item.std_output_list[i],
             console = console,
         })
     end)
diff --git a/lua/leetcode/ui/problem/result/compile-error.lua b/lua/leetcode/ui/problem/result/compile-error.lua
index e69de29b..8a90101a 100644
--- a/lua/leetcode/ui/problem/result/compile-error.lua
+++ b/lua/leetcode/ui/problem/result/compile-error.lua
@@ -0,0 +1,14 @@
+local m = require("markup")
+
+---@param props { item: lc.compile_error }
+local CompileError = m.component(function(props)
+    local item = props.item
+
+    return m.block({
+        Markup.list(vim.split(item.full_compile_error, "\n")):map(function(line)
+            return m.block(line)
+        end),
+    })
+end)
+
+return CompileError
diff --git a/lua/leetcode/ui/problem/result/header.lua b/lua/leetcode/ui/problem/result/header.lua
index 7ef76932..de2e7130 100644
--- a/lua/leetcode/ui/problem/result/header.lua
+++ b/lua/leetcode/ui/problem/result/header.lua
@@ -1,26 +1,25 @@
 local m = require("markup")
 
+local function testcases_passed(item)
+    local correct = item.total_correct ~= vim.NIL and item.total_correct or "0"
+    local total = item.total_testcases ~= vim.NIL and item.total_testcases or "0"
+
+    return ("%d/%d %s"):format(correct, total, "testcases passed")
+end
+
+---@param props { item: lc.interpreter_response }
 local Header = m.component(function(props)
     local item = props.item
 
-    -- if item._.submission then
-    --     if item._.success then
-    --         self:append(" 🎉")
-    --     else
-    --         self:append(" | ")
-    --         self:append(testcases_passed(item), "leetcode_alt")
-    --     end
-    if not item.status_runtime then
-        return
-    end
-
     return m.block({
-        m.inline(" Accepted 🎉", "DiagnosticSignOk"),
-        m.inline({
-            style = "Conceal",
-            " | Runtime: ",
-            item.status_runtime,
-        }),
+        m.inline(item._.title, item._.hl),
+        item._.submission
+                and (
+                    item._.success and m.inline(" 🎉") --
+                    or m.inline(" | " .. testcases_passed(item), "leetcode_alt")
+                )
+            or item.status_runtime
+                and m.inline(" | Runtime: " .. item.status_runtime, "Conceal"),
     })
 end)
 
diff --git a/lua/leetcode/ui/problem/result/init.lua b/lua/leetcode/ui/problem/result/init.lua
index d1dc3dd4..20ebe6d7 100644
--- a/lua/leetcode/ui/problem/result/init.lua
+++ b/lua/leetcode/ui/problem/result/init.lua
@@ -2,25 +2,32 @@ local m = require("markup")
 local Runtime = require("leetcode.ui.problem.result.runtime")
 local SubmissionError = require("leetcode.ui.problem.result.submission-error")
 local LimitExceed = require("leetcode.ui.problem.result.limit-exceeded")
-local RuntimeError = require("leetcode.ui.problem.result.runtime-error")
+local RuntimeError = require("leetcode.ui.problem.result.runtime.error")
 local InternalError = require("leetcode.ui.problem.result.internal-error")
 local CompileError = require("leetcode.ui.problem.result.compile-error")
 
+---@param props { item: lc.interpreter_response, console: leet.problem.console }
 local App = m.component(function(props)
-    local item = props.item
+    local item = props.item ---@type any
     local sc = item.status_code
 
     if sc == 10 then
-        return Runtime({ item = item, console = props.console })
+        ---@cast item lc.runtime
+        return Runtime({ item = item })
     elseif sc == 11 then
+        ---@cast item lc.submission
         return SubmissionError({ item = item })
     elseif sc == 12 or sc == 13 or sc == 14 then
+        ---@cast item lc.limit_exceeded_error
         return LimitExceed({ item = item })
     elseif sc == 15 then
+        ---@cast item lc.runtime_error
         return RuntimeError({ item = item })
     elseif sc == 16 then
+        ---@cast item lc.internal_error
         return InternalError({ item = item })
     elseif sc == 20 then
+        ---@cast item lc.compile_error
         return CompileError({ item = item })
     else
         return "unknown runner status code: " .. sc
diff --git a/lua/leetcode/ui/problem/result/input.lua b/lua/leetcode/ui/problem/result/input.lua
new file mode 100644
index 00000000..bf7bad24
--- /dev/null
+++ b/lua/leetcode/ui/problem/result/input.lua
@@ -0,0 +1,23 @@
+local m = require("markup")
+local CaseBlock = require("leetcode.ui.problem.result.cases.block")
+
+---@param props { input: string[], params: string[], title?: any }
+local Input = m.component(function(props)
+    return m.block({
+        props.title or "Input",
+        m.block({
+            spacing = 1,
+            Markup.list(props.input):map(function(case, i)
+                local ok, param = pcall(function()
+                    return props.params[i].name
+                end)
+                return CaseBlock({
+                    ok and m.block(param .. " ="),
+                    m.block(case),
+                })
+            end),
+        }),
+    })
+end)
+
+return Input
diff --git a/lua/leetcode/ui/problem/result/internal-error.lua b/lua/leetcode/ui/problem/result/internal-error.lua
index e69de29b..87411b5a 100644
--- a/lua/leetcode/ui/problem/result/internal-error.lua
+++ b/lua/leetcode/ui/problem/result/internal-error.lua
@@ -0,0 +1,12 @@
+local m = require("markup")
+
+---@param props { item: lc.internal_error }
+local InternalError = m.component(function(props)
+    local item = props.item
+
+    return m.block({
+        "internal error",
+    })
+end)
+
+return InternalError
diff --git a/lua/leetcode/ui/problem/result/limit-exceeded.lua b/lua/leetcode/ui/problem/result/limit-exceeded.lua
index e69de29b..d9e55b18 100644
--- a/lua/leetcode/ui/problem/result/limit-exceeded.lua
+++ b/lua/leetcode/ui/problem/result/limit-exceeded.lua
@@ -0,0 +1,23 @@
+local m = require("markup")
+local Input = require("leetcode.ui.problem.result.input")
+local Stdout = require("leetcode.ui.problem.result.stdout")
+
+---@param props { item: lc.limit_exceeded_error }
+local LimitExceed = m.component(function(props)
+    local item = props.item
+
+    if item._.submission then
+        return m.block({
+            Input({
+                title = (" %s"):format("Last Executed Input"),
+                input = vim.split(item.last_testcase, "\n"),
+                params = item._.params,
+            }),
+            Stdout({ stdout = item.std_output }),
+        })
+    else
+        return Stdout({ stdout = item.std_output_list[#item.std_output_list] })
+    end
+end)
+
+return LimitExceed
diff --git a/lua/leetcode/ui/problem/result/runtime-error.lua b/lua/leetcode/ui/problem/result/runtime-error.lua
deleted file mode 100644
index e69de29b..00000000
diff --git a/lua/leetcode/ui/problem/result/runtime/accepted.lua b/lua/leetcode/ui/problem/result/runtime/accepted.lua
index 745329be..c623f2cb 100644
--- a/lua/leetcode/ui/problem/result/runtime/accepted.lua
+++ b/lua/leetcode/ui/problem/result/runtime/accepted.lua
@@ -1,55 +1,59 @@
-local m = require('markup')
+local m = require("markup")
 
-local function perc_hl(perc) --
-	if perc >= 50 then
-		return "leetcode_ok"
-	elseif perc >= 10 then
-		return nil
-	elseif perc >= 0 then
-		return "leetcode_error"
-	end
+local function perc_hl(perc)
+    if perc >= 50 then
+        return "leetcode_ok"
+    elseif perc >= 10 then
+        return nil
+    elseif perc >= 0 then
+        return "leetcode_error"
+    end
 end
 
-local App = m.component(function(props)
-	local item = props.item ---@type lc.runtime
-	local runtime_ms = item.display_runtime or vim.split(item.status_runtime, " ")[1] or "NIL"
-	local lang = Leet.util.get_lang_by_name(item.pretty_lang)
+---@param props { item: lc.runtime }
+local Accepted = m.component(function(props)
+    local item = props.item
+    local runtime_ms = item.display_runtime or vim.split(item.status_runtime, " ")[1] or "NIL"
+    local lang = Leet.util.get_lang_by_name(item.pretty_lang)
 
-	item.status_memory = item.status_memory:gsub("(%d+)%s*(%a+)", "%1 %2")
-	local s_mem = vim.split(item.status_memory, " ")
+    item.status_memory = item.status_memory:gsub("(%d+)%s*(%a+)", "%1 %2")
+    local s_mem = vim.split(item.status_memory, " ")
 
-	return m.block({
-		m.block({
-			m.inline("# Accepted "),
-			lang.icon .. " " .. lang.lang,
-			style = lang.hl or "Structure",
-			margin_bottom = 1,
-		}),
-		m.flex({
-			spacing = 3,
-			m.block({
-				m.block("  Runtime"),
-				m.inline({
-					runtime_ms,
-					m.inline("ms", "Conceal"),
-				}),
-				" | ",
-				m.inline(("%s %.2f%%"):format("Beats", item.runtime_percentile), perc_hl(item.runtime_percentile)),
-			}),
-			m.block({
-				m.block("󰍛 Memory"),
-				m.inline({
-					m.inline(s_mem, "Conceal"),
-				}),
-				" | ",
-				m.inline({
-					"Beats ",
-					("%.2f%%"):format(item.memory_percentile),
-					style = perc_hl(item.runtime_percentile),
-				}),
-			}),
-		}),
-	})
+    return m.block({
+        m.block({
+            m.inline("# Accepted "),
+            lang.icon .. " " .. lang.lang,
+            style = lang.hl or "Structure",
+            margin_bottom = 1,
+        }),
+        m.flex({
+            spacing = 3,
+            m.block({
+                m.block("  Runtime"),
+                m.inline({
+                    runtime_ms,
+                    m.inline("ms", "Conceal"),
+                }),
+                " | ",
+                m.inline(
+                    ("%s %.2f%%"):format("Beats", item.runtime_percentile),
+                    perc_hl(item.runtime_percentile)
+                ),
+            }),
+            m.block({
+                m.block("󰍛 Memory"),
+                m.inline({
+                    m.inline(s_mem, "Conceal"),
+                }),
+                " | ",
+                m.inline({
+                    "Beats ",
+                    ("%.2f%%"):format(item.memory_percentile),
+                    style = perc_hl(item.runtime_percentile),
+                }),
+            }),
+        }),
+    })
 end)
 
-return App
+return Accepted
diff --git a/lua/leetcode/ui/problem/result/runtime/error.lua b/lua/leetcode/ui/problem/result/runtime/error.lua
new file mode 100644
index 00000000..355cf22d
--- /dev/null
+++ b/lua/leetcode/ui/problem/result/runtime/error.lua
@@ -0,0 +1,37 @@
+local m = require("markup")
+local Header = require("leetcode.ui.problem.result.header")
+local Stdout = require("leetcode.ui.problem.result.stdout")
+local Input = require("leetcode.ui.problem.result.input")
+
+---@param props { item: lc.runtime_error }
+local RuntimeError = m.component(function(props)
+    local item = props.item
+    local params = item._.params or {}
+
+    return {
+        m.block({
+            Header({ item = item }),
+            Markup.list(vim.split(item.full_runtime_error, "\n")):map(function(line)
+                return m.block(line)
+            end),
+        }),
+        item._.submission
+                and m.block({
+                    Input({
+                        title = m.block(
+                            (" %s"):format("Last Executed Input"),
+                            "leetcode_normal"
+                        ),
+                        input = vim.split(item.last_testcase, "\n"),
+                        params = params,
+                    }),
+                    item.std_output and Stdout({ stdout = item.std_output }),
+                })
+            or item.std_output_list
+                and m.block({
+                    Stdout({ stdout = item.std_output_list[#item.std_output_list] }),
+                }),
+    }
+end)
+
+return RuntimeError
diff --git a/lua/leetcode/ui/problem/result/runtime/init.lua b/lua/leetcode/ui/problem/result/runtime/init.lua
index b212b9a3..8f3dc2b0 100644
--- a/lua/leetcode/ui/problem/result/runtime/init.lua
+++ b/lua/leetcode/ui/problem/result/runtime/init.lua
@@ -3,9 +3,9 @@ local Accepted = require("leetcode.ui.problem.result.runtime.accepted")
 local Header = require("leetcode.ui.problem.result.header")
 local Cases = require("leetcode.ui.problem.result.cases")
 
+---@param props { item: lc.runtime, console: leet.problem.console }
 local Runtime = m.component(function(props)
-    local item = props.item ---@type lc.runtime
-    local console = props.console ---@type lc.runtime
+    local item = props.item
 
     if item._.submission then
         return Accepted(item)
@@ -14,7 +14,7 @@ local Runtime = m.component(function(props)
     return m.block({
         spacing = 1,
         Header({ item = item }),
-        Cases({ item = item, console = console }),
+        Cases({ item = item }),
     })
 end)
 
diff --git a/lua/leetcode/ui/problem/result/stdout.lua b/lua/leetcode/ui/problem/result/stdout.lua
new file mode 100644
index 00000000..bff3d367
--- /dev/null
+++ b/lua/leetcode/ui/problem/result/stdout.lua
@@ -0,0 +1,25 @@
+local m = require("markup")
+local CaseBlock = require("leetcode.ui.problem.result.cases.block")
+
+---@param props { stdout: string }
+local Stdout = m.component(function(props)
+    local expanded, set_expanded = m.hooks.variable(true)
+
+    local std_lines = Markup.list(vim.split(props.stdout or "", "\n", { trimempty = true }))
+
+    if #std_lines == 0 then
+        return
+    end
+
+    return m.block({
+        m.block({
+            m.button(expanded and "" or ""),
+            " Stdout ",
+        }),
+        expanded and CaseBlock(std_lines:map(function(line)
+            return m.block(line)
+        end)),
+    })
+end)
+
+return Stdout
diff --git a/lua/leetcode/ui/problem/result/submission-error.lua b/lua/leetcode/ui/problem/result/submission-error.lua
index e69de29b..e267333e 100644
--- a/lua/leetcode/ui/problem/result/submission-error.lua
+++ b/lua/leetcode/ui/problem/result/submission-error.lua
@@ -0,0 +1,16 @@
+local m = require("markup")
+local Case = require("leetcode.ui.problem.result.cases.case")
+
+---@param props { item: lc.submission }
+local SubmissionError = m.component(function(props)
+    local item = props.item ---@type lc.submission
+
+    return Case({
+        input = vim.split(item.input, "\n"),
+        output = item.code_output,
+        expected = item.expected_output,
+        std_output = item.std_output,
+    })
+end)
+
+return SubmissionError
