灵符lingfu
L

lingcoder

↓ 0 downloads

LingCoder: a Geany / VS Code-style IDE for Ling, written in Ling. Explorer, tabs, syntax-coloured editor with lint, find in files, command palette, run/build panel, and hot-reloaded extensions - including a Claude assistant.

Keywords ide, editor, ling, claude, extensions
lingfu install lingcoder
LicenseApache-2.0 OR MIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (34)

ling/shell.ling
# shell.ling - menu bar, popup menus, context menus, the buffer tab strip, the activity
# bar and the status bar. Menu item format: "Label|action|Shortcut|icon"; "-" separates;
# a label ending in " >" opens the submenu named "sub:<name>".

fn menu_titles() { return ["File", "Edit", "View", "Go", "Run", "Tools", "Help"] }

fn menu_def(m) {
    if m == 0.0 {
        return ["New File|file.new|Ctrl+N|file", "Open File...|file.open|Ctrl+O|folder", "Open Folder...|file.open_folder|Ctrl+Shift+O|folder_open", "Open Recent >|sub:recent||refresh", "-", "Save|file.save|Ctrl+S|save", "Save As...|file.save_as||save", "Save All|file.save_all|Ctrl+Alt+S|save", "-", "Close Tab|file.close_tab|Ctrl+W|x", "Close Folder|file.close_folder||folder", "-", "Settings...|file.settings|Ctrl+,|gear", "Quit|file.quit|Ctrl+Q|x"]
    }
    if m == 1.0 {
        return ["Undo|edit.undo|Ctrl+Z|undo", "Redo|edit.redo|Ctrl+Y|redo", "-", "Cut|edit.cut|Ctrl+X|slice", "Copy|edit.copy|Ctrl+C|copy", "Paste|edit.paste|Ctrl+V|copy", "Select All|edit.select_all|Ctrl+A|select", "-", "Find|edit.find|Ctrl+F|search", "Replace|edit.replace|Ctrl+H|edit", "Find in Files|edit.findfiles|Ctrl+Shift+F|search", "-", "Toggle Comment|edit.comment|Ctrl+/|text", "Duplicate Line|edit.dup_line|Ctrl+D|copy", "Move Line Up|edit.line_up|Alt+Up|arrow_up", "Move Line Down|edit.line_down|Alt+Down|arrow_down"]
    }
    if m == 2.0 {
        return ["Explorer|view.files|Ctrl+Shift+E|folder", "Search|view.search|Ctrl+Shift+F|search", "Extensions|view.ext|Ctrl+Shift+X|chip", "-", "Sidebar|view.side|F12|panel", "Panel|view.dock|Ctrl+J|bar", "Problems|view.problems|Ctrl+Shift+M|info", "Output|view.output|Ctrl+Shift+U|terminal", "-", "Bigger Text|text.bigger|Ctrl+Shift+=|plus", "Smaller Text|text.smaller|Ctrl+Shift+-|minus", "Text Size 100%|text.reset|Ctrl+0|refresh", "-", "Welcome|view.welcome||home"]
    }
    if m == 3.0 {
        return ["Go to File...|go.file|Ctrl+P|search", "Go to Line...|go.line|Ctrl+G|chev_r", "Command Palette...|go.palette|Ctrl+Shift+P|terminal", "-", "Next Tab|go.next_tab|Ctrl+PgDn|chev_r", "Previous Tab|go.prev_tab|Ctrl+PgUp|chev_l", "-", "Next Problem|go.next_problem|F8|arrow_down", "Previous Problem|go.prev_problem|Shift+F8|arrow_up"]
    }
    if m == 4.0 {
        return ["Run File|run.file|F5|play", "Run Project|run.project|Ctrl+F5|play", "Build Project|run.build|F7|build", "Stop|run.stop|Shift+F5|stop", "-", "Show Output|view.output|Ctrl+Shift+U|terminal", "Clear Output|run.clear||trash", "-", "Choose ling exe...|run.pick_exe||gear"]
    }
    if m == 5.0 {
        bind base = ["Command Palette...|go.palette|Ctrl+Shift+P|terminal", "Extensions Panel|view.ext|Ctrl+Shift+X|chip", "Reload Extensions|tools.ext_reload||refresh", "New Extension...|tools.ext_new||plus", "Open Extensions Folder|tools.ext_folder||folder"]
        bind extras = st_get("ext_menu", [])
        if len(extras) > 0.5 {
            bind base = list_push(base, "-")
            bind i = 0.0
            while i < len(extras) {
                bind base = list_push(base, list_get(extras, i))
                bind i = i + 1.0
            }
        }
        return base
    }
    return ["Welcome|view.welcome||home", "Keyboard Shortcuts|help.shortcuts|F1|bars", "Extension API (docs)|help.extapi||script", "-", "About LingCoder|help.about||info"]
}

fn menu_sub_def(name) {
    if name == "recent" { return st_get("recent_menu", ["(empty)|none||"]) }
    return []
}

# 1 checked / 0 unchecked / -1 not checkable
fn menu_check(action) {
    if action == "view.files" { return bool_num(st_get("side_tab", "files") == "files" && st_get("show_side", 1.0) > 0.5) }
    if action == "view.search" { return bool_num(st_get("side_tab", "files") == "search" && st_get("show_side", 1.0) > 0.5) }
    if action == "view.ext" { return bool_num(st_get("side_tab", "files") == "ext" && st_get("show_side", 1.0) > 0.5) }
    if action == "view.side" { return st_get("show_side", 1.0) }
    if action == "view.dock" { return st_get("show_dock", 1.0) }
    if action == "view.problems" { return bool_num(st_get("dock_tab", 0.0) == 0.0 && st_get("show_dock", 1.0) > 0.5) }
    if action == "view.output" { return bool_num(st_get("dock_tab", 0.0) == 1.0 && st_get("show_dock", 1.0) > 0.5) }
    return 0.0 - 1.0
}

fn menu_item_h(s) {
    if s == "-" { return 7.0 }
    return 22.0
}
fn menu_field(item, k) {
    bind parts = str_split(item, "|")
    if k >= len(parts) { return "" }
    return list_get(parts, k)
}

fn menu_bar() {
    bind W = get_width()
    g_rect(0.0, 0.0, W, MENU_H, C_BG_PANEL)
    u_hsep(0.0, MENU_H - 1.0, W)
    icon_draw("script", 13.0, 12.0, 13.0, C_YELLOW)
    bind titles = menu_titles()
    bind x = 26.0
    bind open = st_get("menu_open", 0.0 - 1.0)
    bind i = 0.0
    while i < len(titles) {
        bind t = list_get(titles, i)
        bind w = g_text_w(12.0, t) + 18.0
        bind hot = u_hot(x, 0.0, w, MENU_H - 1.0)
        bind col = menu_color(i)
        if open == i || hot { g_rrect_a(x, 2.0, w, MENU_H - 5.0, 3.0, col, 0.28) }
        g_text(x + 9.0, 5.0, 12.0, sel(open == i, C_TEXT_STRONG, col), t)
        if hot && u_pressed() {
            if open == i {
                st_set("menu_open", 0.0 - 1.0)
                st_set("menu_sub", "")
            } else {
                st_set("menu_open", i)
                st_set("menu_x", x)
                st_set("menu_sub", "")
            }
            st_set("menu_click_eat", 1.0)
        } else if hot && open >= 0.0 && open != i {
            st_set("menu_open", i)
            st_set("menu_x", x)
            st_set("menu_sub", "")
        }
        bind x = x + w + 2.0
        bind i = i + 1.0
    }
    # right side: app - folder / file, a dirty star, and the background-jobs chip
    bind bi = buf_cur()
    bind fname = sel(bi >= 0.0, p_base(buf_path(bi)), "")
    bind ttl = "LingCoder  -  " + proj_name()
    if fname != "" { bind ttl = ttl + "  /  " + fname + sel(buf_dirty(bi), " *", "") }
    g_text_r(W - 10.0, 5.0, 12.0, C_TEXT_DIM, g_fit(12.0, W * 0.4, ttl))
    bind jl = jobs_line()
    if jl != "" {
        bind jw = g_text_w(11.0, jl) + 16.0
        bind jx = W - 26.0 - g_text_w(12.0, ttl) - jw
        g_rrect(jx, 3.0, jw, 18.0, 3.0, c_mix(C_BG_DEEP, C_GREEN, 0.25))
        g_text(jx + 8.0, 6.0, 11.0, C_TEXT_STRONG, jl)
        if u_hot(jx, 3.0, jw, 18.0) { u_tip("Working in the background - you can keep editing") }
    }
}

# draws a popup list at (x,y); returns the clicked action or ""
fn popup_list(items, x, y, mcol, level) {
    bind w = 200.0
    bind h = 8.0
    bind i = 0.0
    while i < len(items) {
        bind it = list_get(items, i)
        bind h = h + menu_item_h(it)
        if it != "-" {
            bind sc = menu_field(it, 2)
            bind w = max(w, 28.0 + g_text_w(12.0, menu_field(it, 0)) + sel(sc == "", 26.0, g_text_w(11.0, sc) + 36.0))
        }
        bind i = i + 1.0
    }
    bind W = get_width()
    bind H = get_height()
    bind w = min(w, W - 8.0)
    if x + w > W { bind x = W - w - 2.0 }
    bind full = h
    bind vh = min(h, H - 8.0)
    if y + vh > H - 2.0 { bind y = H - vh - 2.0 }
    if y < 2.0 { bind y = 2.0 }
    bind okey = "popup_off" + num_str(level)
    bind off = 0.0
    if full > vh {
        bind off = clampf(st_get(okey, 0.0), 0.0, full - vh)
        if u_in(x, y, w, vh) && u_scroll() != 0.0 {
            bind off = clampf(off - u_scroll() * 36.0, 0.0, full - vh)
            st_set("wheel_used", 1.0)
        }
        st_set(okey, off)
    } else { st_set(okey, 0.0) }
    px_rrect(x + 2.0, y + 3.0, w, vh, 6.0, 0.0, 0.35)
    g_rrect(x, y, w, vh, 5.0, C_POPUP)
    g_rframe(x, y, w, vh, 5.0, C_POPUP_LINE)
    if u_in(x, y, w, vh) { st_set("popup_hit", 1.0) }
    bind out = ""
    bind cy = y + 4.0 - off
    u_clip(x, y, w, vh)
    bind i = 0.0
    while i < len(items) {
        bind it = list_get(items, i)
        bind ih = menu_item_h(it)
        bind vis = cy + ih >= y && cy <= y + vh
        if vis && it == "-" {
            g_rect(x + 8.0, cy + 3.0, w - 16.0, 1.0, C_LINE)
        } else if vis && it != "-" {
            bind label = menu_field(it, 0)
            bind act = menu_field(it, 1)
            bind shortcut = menu_field(it, 2)
            bind icon = menu_field(it, 3)
            bind hot = u_hot(x + 2.0, cy, w - 4.0, ih) && u_my() >= y && u_my() < y + vh
            if hot { g_rrect_a(x + 3.0, cy, w - 6.0, ih, 3.0, mcol, 0.30) }
            bind chk = menu_check(act)
            if chk > 0.5 { g_rrect(x + 10.0, cy + 6.0, 10.0, 10.0, 2.0, mcol) }
            else if icon != "" { icon_draw(icon, x + 15.0, cy + ih * 0.5, 11.0, mcol) }
            bind is_sub = str_ends_with(label, " >")
            bind shown = label
            if is_sub { bind shown = str_slice(label, 0.0, str_len(label) - 2.0) }
            g_text(x + 28.0, cy + 4.0, 12.0, sel(hot, C_TEXT_STRONG, C_TEXT), shown)
            if is_sub { icon_draw("chev_r", x + w - 12.0, cy + ih * 0.5, 9.0, C_TEXT_DIM) }
            else if shortcut != "" { g_text_r(x + w - 10.0, cy + 5.0, 11.0, C_TEXT_DIM, shortcut) }
            if hot {
                if is_sub {
                    st_set("menu_sub", str_slice(act, 4.0, str_len(act) - 4.0))
                    st_set("menu_sub_x", x + w - 2.0)
                    st_set("menu_sub_y", cy - 4.0)
                } else if level < 0.5 {
                    st_set("menu_sub", "")
                }
                if u_pressed() && !is_sub && act != "none" { bind out = act }
            }
        }
        bind cy = cy + ih
        bind i = i + 1.0
    }
    u_unclip()
    if full > vh {
        bind th = max(24.0, vh * vh / full)
        bind ty = y + (vh - th) * off / (full - vh)
        g_rrect_a(x + w - 6.0, ty, 4.0, th, 2.0, C_TEXT_DIM, 0.6)
    }
    return out
}

# the open menu; call near the end of the frame. returns the action or ""
fn menu_overlay() {
    bind open = st_get("menu_open", 0.0 - 1.0)
    if open < 0.0 { return "" }
    st_set("blk", 0.0)
    st_set("popup_hit", 0.0)
    bind mcol = menu_color(open)
    bind act = popup_list(menu_def(open), st_get("menu_x", 0.0), MENU_H - 2.0, mcol, 0.0)
    bind sub = st_get("menu_sub", "")
    if sub != "" {
        bind sact = popup_list(menu_sub_def(sub), st_get("menu_sub_x", 0.0), st_get("menu_sub_y", 0.0), mcol, 1.0)
        if sact != "" { bind act = sact }
    }
    if u_pressed() && act == "" && st_get("menu_click_eat", 0.0) < 0.5 {
        if u_my() >= MENU_H && st_get("popup_hit", 0.0) < 0.5 {
            st_set("menu_open", 0.0 - 1.0)
            st_set("menu_sub", "")
        }
    }
    if key_pressed("escape") {
        st_set("menu_open", 0.0 - 1.0)
        st_set("menu_sub", "")
    }
    st_set("menu_click_eat", 0.0)
    if act != "" {
        st_set("menu_open", 0.0 - 1.0)
        st_set("menu_sub", "")
    }
    return act
}

# ── context menus ─────────────────────────────────────────────────────────────
fn ctx_open(kind, arg, x, y) {
    st_set("ctx_kind", kind)
    st_set("ctx_arg", arg)
    st_set("ctx_x", x)
    st_set("ctx_y", y)
    st_set("ctx_open", 1.0)
    st_set("ctx_eat", 1.0)
    st_set("menu_sub", "")
}

fn ctx_items(kind) {
    if kind == "file" {
        return ["Open|fx.open||file", "Rename...|fx.rename||edit", "Duplicate|fx.dup||copy", "Copy Path|fx.copy_path||copy", "Show in Explorer|fx.reveal||folder", "-", "Run File|fx.run|F5|play", "-", "Delete...|fx.delete||trash"]
    }
    if kind == "folder" {
        return ["New File...|fx.new_file||file", "New Folder...|fx.new_folder||folder", "Rename...|fx.rename||edit", "Copy Path|fx.copy_path||copy", "Show in Explorer|fx.reveal||folder", "-", "Delete...|fx.delete||trash"]
    }
    if kind == "root" {
        return ["New File...|fx.new_file||file", "New Folder...|fx.new_folder||folder", "Refresh|fx.refresh||refresh", "Show in Explorer|fx.reveal||folder"]
    }
    if kind == "tab" {
        return ["Close|tabx.close|Ctrl+W|x", "Close Others|tabx.others||x", "Close All|tabx.all||x", "-", "Copy Path|tabx.copy_path||copy", "Show in Explorer|tabx.reveal||folder"]
    }
    if kind == "edit" {
        bind base = ["Cut|edit.cut|Ctrl+X|slice", "Copy|edit.copy|Ctrl+C|copy", "Paste|edit.paste|Ctrl+V|copy", "-", "Toggle Comment|edit.comment|Ctrl+/|text", "Find|edit.find|Ctrl+F|search", "Select All|edit.select_all|Ctrl+A|select"]
        bind extras = st_get("ext_ctx_edit", [])
        if len(extras) > 0.5 {
            bind base = list_push(base, "-")
            bind i = 0.0
            while i < len(extras) {
                bind base = list_push(base, list_get(extras, i))
                bind i = i + 1.0
            }
        }
        return base
    }
    return []
}

fn ctx_overlay() {
    if st_get("ctx_open", 0.0) < 0.5 { return "" }
    st_set("blk", 0.0)
    st_set("popup_hit", 0.0)
    bind act = popup_list(ctx_items(st_get("ctx_kind", "")), st_get("ctx_x", 0.0), st_get("ctx_y", 0.0), C_BLUE, 0.0)
    if act != "" {
        st_set("ctx_open", 0.0)
        st_set("menu_sub", "")
        do_ctx_action(act)
        return act
    }
    if (u_pressed() || u_rpressed()) && st_get("ctx_eat", 0.0) < 0.5 && st_get("popup_hit", 0.0) < 0.5 { st_set("ctx_open", 0.0) }
    if key_pressed("escape") { st_set("ctx_open", 0.0) }
    st_set("ctx_eat", 0.0)
    return ""
}

# ── buffer tab strip ──────────────────────────────────────────────────────────
fn tab_w(bi) {
    return clampf(g_text_w(12.0, p_base(buf_path(bi))) + 46.0, 90.0, 220.0)
}
fn tabs_row(x0, y, w) {
    g_rect(x0, y, w, TABS_H, C_BG_SIDE)
    u_hsep(x0, y + TABS_H - 1.0, w)
    # a pinned extension tab (the Claude chat tab) sits before the file tabs
    if st_get("ptab_on", 0.0) > 0.5 {
        bind title = st_get("ptab_title", "Panel")
        bind pw = clampf(g_text_w(12.0, title) + 48.0, 90.0, 200.0)
        bind on = st_get("ptab_active", 0.0) > 0.5
        bind hot = u_hot(x0, y, pw, TABS_H - 1.0)
        if on {
            g_rect(x0, y, pw, TABS_H - 1.0, C_BG_APP)
            g_rect(x0, y, pw, 2.0, C_VIOLET)
        } else if hot {
            g_rect(x0, y + 2.0, pw, TABS_H - 3.0, C_HOVER_ROW)
        }
        icon_draw(st_get("ptab_icon", "chip"), x0 + 13.0, y + TABS_H * 0.5, 10.0, C_VIOLET)
        g_text_fit(x0 + 24.0, y + 9.0, pw - 44.0, 12.0, sel(on, C_TEXT_STRONG, C_TEXT), title)
        bind cbx = x0 + pw - 18.0
        bind chot = u_hot(cbx, y + 8.0, 14.0, 14.0)
        if chot {
            g_rrect(cbx, y + 8.0, 14.0, 14.0, 3.0, C_BTN_HOV)
            icon_draw("x", cbx + 7.0, y + 15.0, 8.0, C_TEXT_STRONG)
            if u_pressed() { ide_close_panel_tab()  st_set("tab_eat", 1.0) }
        }
        if hot && !chot && u_pressed() && st_get("tab_eat", 0.0) < 0.5 {
            st_set("ptab_active", 1.0)
            st_set("focus", "")
            st_set("tab_eat", 1.0)
        }
        if hot && u_mpressed() { ide_close_panel_tab() }
        bind x0 = x0 + pw + 1.0
        bind w = w - pw - 1.0
        g_vsep(x0 - 1.0, y + 4.0, TABS_H - 8.0)
    }
    bind n = buf_count()
    if n < 0.5 {
        st_set("tab_eat", 0.0)
        g_text(x0 + 12.0, y + 9.0, 11.0, C_TEXT_DIM, "Ctrl+N new file    Ctrl+O open    Ctrl+P go to file    Ctrl+Shift+P commands")
        return 0.0
    }
    # total width -> wheel scroll offset
    bind total = 0.0
    bind i = 0.0
    while i < n {
        bind total = total + tab_w(buf_at(i)) + 1.0
        bind i = i + 1.0
    }
    bind off = clampf(st_get("tabs_off", 0.0), 0.0, max(0.0, total - w))
    if u_hot(x0, y, w, TABS_H) && u_scroll() != 0.0 && st_get("wheel_used", 0.0) < 0.5 {
        bind off = clampf(off - u_scroll() * 40.0, 0.0, max(0.0, total - w))
        st_set("wheel_used", 1.0)
    }
    st_set("tabs_off", off)
    u_clip(x0, y, w, TABS_H)
    bind cur = st_get("buf_cur", 0.0)
    bind x = x0 - off
    bind i = 0.0
    while i < n {
        bind bi = buf_at(i)
        bind tw = tab_w(bi)
        if x + tw > x0 && x < x0 + w {
            bind on = i == cur
            bind hot = u_hot(x, y, tw, TABS_H - 1.0)
            bind col = ext_col(p_ext(buf_path(bi)))
            if on {
                g_rect(x, y, tw, TABS_H - 1.0, C_BG_APP)
                g_rect(x, y, tw, 2.0, col)
            } else if hot {
                g_rect(x, y + 2.0, tw, TABS_H - 3.0, C_HOVER_ROW)
            }
            icon_draw(ext_icon(p_ext(buf_path(bi))), x + 13.0, y + TABS_H * 0.5, 10.0, col)
            bind room = tw - 44.0
            g_text_fit(x + 24.0, y + 9.0, room, 12.0, sel(on, C_TEXT_STRONG, C_TEXT), p_base(buf_path(bi)))
            # dirty dot, or a close box where the pointer is
            bind cbx = x + tw - 18.0
            bind chot = u_hot(cbx, y + 8.0, 14.0, 14.0)
            if chot {
                g_rrect(cbx, y + 8.0, 14.0, 14.0, 3.0, C_BTN_HOV)
                icon_draw("x", cbx + 7.0, y + 15.0, 8.0, C_TEXT_STRONG)
                if u_pressed() { tab_close_ask(i)  st_set("tab_eat", 1.0) }
            } else if buf_dirty(bi) {
                g_disc(cbx + 7.0, y + 15.0, 3.5, sel(on, C_TEXT_STRONG, C_TEXT_DIM))
            }
            if hot && !chot {
                if u_pressed() && st_get("tab_eat", 0.0) < 0.5 {
                    st_set("buf_cur", i)
                    st_set("ptab_active", 0.0)
                    st_set("focus", "code")
                    ed_sync_text()
                }
                if u_mpressed() { tab_close_ask(i) }
                if u_rpressed() {
                    st_set("ctx_tab", i)
                    ctx_open("tab", num_str(i), u_mx(), u_my() + 8.0)
                }
                if hot { u_tip(buf_path(bi)) }
            }
        }
        bind x = x + tw + 1.0
        bind i = i + 1.0
    }
    u_unclip()
    st_set("tab_eat", 0.0)
    return 0.0
}

# ── activity bar (far left) ───────────────────────────────────────────────────
fn act_bar(y, h) {
    g_rect(0.0, y, ACT_W, h, C_BG_SIDE)
    g_vsep(ACT_W - 1.0, y, h)
    bind tabs = ["files", "search", "ext"]
    bind icons = ["folder", "search", "chip"]
    bind tips = ["Explorer (Ctrl+Shift+E)", "Search (Ctrl+Shift+F)", "Extensions (Ctrl+Shift+X)"]
    bind cur = st_get("side_tab", "files")
    bind shown = st_get("show_side", 1.0) > 0.5
    bind iy = y + 8.0
    bind i = 0.0
    while i < len(tabs) {
        bind t = list_get(tabs, i)
        bind on = shown && cur == t
        bind hot = u_hot(0.0, iy, ACT_W, 40.0)
        if on { g_rect(0.0, iy, 2.0, 40.0, c_pal(i + 3.0)) }
        if hot { g_rect_a(0.0, iy, ACT_W, 40.0, C_HOVER_ROW, 0.8) }
        icon_draw(list_get(icons, i), ACT_W * 0.5, iy + 20.0, 17.0, sel(on, C_TEXT_STRONG, sel(hot, C_TEXT, C_TEXT_DIM)))
        if hot { u_tip(list_get(tips, i)) }
        if hot && u_pressed() { side_show(t) }
        bind iy = iy + 44.0
        bind i = i + 1.0
    }
    # one icon per extension panel (the Claude panel lands here)
    bind pn = st_llen("ext_panels")
    bind k = 0.0
    while k < pn {
        bind rec = str_split(st_lget("ext_panels", k), "|")
        bind stem = list_get(rec, 0)
        bind icon = sel(len(rec) > 2.0, list_get(rec, 2), "chip")
        bind t = "ext:" + stem
        bind on = shown && cur == t
        bind hot = u_hot(0.0, iy, ACT_W, 40.0)
        if on { g_rect(0.0, iy, 2.0, 40.0, c_pal(k)) }
        if hot { g_rect_a(0.0, iy, ACT_W, 40.0, C_HOVER_ROW, 0.8) }
        icon_draw(icon, ACT_W * 0.5, iy + 20.0, 17.0, sel(on, C_TEXT_STRONG, sel(hot, C_TEXT, C_TEXT_DIM)))
        if hot { u_tip(list_get(rec, 1)) }
        if hot && u_pressed() { side_show(t) }
        bind iy = iy + 44.0
        bind k = k + 1.0
    }
    # settings gear at the bottom
    bind gy = y + h - 44.0
    bind ghot = u_hot(0.0, gy, ACT_W, 40.0)
    icon_draw("gear", ACT_W * 0.5, gy + 20.0, 16.0, sel(ghot, C_TEXT_STRONG, C_TEXT_DIM))
    if ghot { u_tip("Settings (Ctrl+,)") }
    if ghot && u_pressed() { do_action("file.settings") }
    return 0.0
}
fn side_show(t) {
    if st_get("side_tab", "files") == t && st_get("show_side", 1.0) > 0.5 {
        st_set("show_side", 0.0)
    } else {
        st_set("side_tab", t)
        st_set("show_side", 1.0)
        if t == "search" { st_set("focus", "search_q")  st_set("text_owner", "") }
    }
}

# ── status bar ────────────────────────────────────────────────────────────────
fn status_bar() {
    bind W = get_width()
    bind H = get_height()
    bind y = H - STATUS_H
    g_rect(0.0, y, W, STATUS_H, C_BG_PANEL)
    u_hsep(0.0, y, W)
    # fading status message on the left
    bind cur_status = st_get("status", "Ready")
    if cur_status != st_get("status_seen", "") {
        st_set("status_seen", cur_status)
        st_set("status_t", st_get("now", 0.0))
    }
    bind age = st_get("now", 0.0) - st_get("status_t", 0.0)
    bind fade = clampf((age - 6.0) / 6.0, 0.0, 1.0)
    # right: cursor / language / problems / frame time
    bind right = ""
    bind bi = buf_cur()
    bind pr = st_llen("problems")
    bind pcol = sel(pr > 0.5, C_YELLOW, C_TEXT_DIM)
    bind rx = W - 10.0
    g_text_r(rx, y + 5.0, 11.0, C_TEXT_DIM, num_str(round(st_get("frame_ms", 0.0))) + " ms")
    bind rx = rx - 56.0
    if bi >= 0.0 {
        bind lang = buf_lang(bi)
        g_text_r(rx, y + 5.0, 11.0, C_TEXT_DIM, lang)
        bind rx = rx - g_text_w(11.0, lang) - 18.0
        bind pct = "Ln " + num_str(st_get(buf_key(bi, "cl"), 0.0) + 1.0) + ", Col " + num_str(st_get(buf_key(bi, "cc"), 0.0) + 1.0)
        bind nsel = ed_sel_count(bi)
        if nsel > 0.5 { bind pct = pct + "  (" + num_str(nsel) + " selected)" }
        bind phot = u_hot(rx - g_text_w(11.0, pct), y + 1.0, g_text_w(11.0, pct) + 6.0, STATUS_H - 2.0)
        g_text_r(rx, y + 5.0, 11.0, sel(phot, C_TEXT_STRONG, C_TEXT_DIM), pct)
        if phot { u_tip("Go to line (Ctrl+G)") }
        if phot && u_pressed() { do_action("go.line") }
        bind rx = rx - g_text_w(11.0, pct) - 18.0
    }
    bind ptxt = num_str(pr) + " problem" + sel(pr == 1.0, "", "s")
    bind phot2 = u_hot(rx - g_text_w(11.0, ptxt), y + 1.0, g_text_w(11.0, ptxt) + 6.0, STATUS_H - 2.0)
    g_text_r(rx, y + 5.0, 11.0, sel(phot2, C_TEXT_STRONG, pcol), ptxt)
    if phot2 { u_tip("Problems (Ctrl+Shift+M)") }
    if phot2 && u_pressed() { do_action("view.problems") }
    bind rx = rx - g_text_w(11.0, ptxt) - 18.0
    g_text_fit(10.0, y + 5.0, max(60.0, rx - 30.0), 11.0, c_mix(C_TEXT, 4408131.0, fade * 0.8), cur_status)
    return 0.0
}

# "2 jobs" chip text for the menu bar; extensions report theirs via st "ext_busy_<stem>"
fn jobs_line() {
    bind n = 0.0
    if st_get("run_job", 0.0 - 1.0) >= 0.0 { bind n = n + 1.0 }
    bind i = 0.0
    while i < st_llen("ext_names") {
        if st_get("ext_busy_" + st_lget("ext_names", i), 0.0) > 0.5 { bind n = n + 1.0 }
        bind i = i + 1.0
    }
    if n < 0.5 { return "" }
    if n == 1.0 { return "1 job" }
    return num_str(n) + " jobs"
}

# tooltips and drag ghosts, drawn over panels but under overlays
fn draw_floating() {
    bind tip = st_get("tip", "")
    if tip != "" {
        bind w = g_text_w(11.0, tip) + 14.0
        bind tx = min(u_mx() + 14.0, get_width() - w - 4.0)
        bind ty = min(u_my() + 18.0, get_height() - 26.0)
        g_rrect(tx, ty, w, 20.0, 4.0, C_POPUP)
        g_rframe(tx, ty, w, 20.0, 4.0, C_POPUP_LINE)
        g_text(tx + 7.0, ty + 4.0, 11.0, C_TEXT, tip)
    }
    return 0.0
}

← Back to index