noraneko · drops

dl.f3liz.casa/drop を読んでいます

std-tsubaki-runtime

Tsubaki(Julia の書き心地の小さな言語)の runtime と、その最初の言葉(ops/std.tsubaki: get(d, :key, 既定) / copy / put / merge、40px の単位、view と effect のことば、色の棚 Theme)。a[begin + i] と begin ... end が書ける。言葉は t(:鍵) で書けて、字は drop の strings.toml から(選ぶのは殻)。タブのことば(見ていてほしい出来事、目印と覚書、字を打つ欄)

uuid 8a0e317f-4d94-4917-aef4-ad26aac11653

install

Press inside noraneko. It catches the link, opens its settings with this uuid, downloads the xpi, checks the sha256 and the registry's signature, and shows you everything below again before anything runs.

install in noraneko In another browser this is a plain link to the xpi. Stock Firefox will refuse it as unverified; paste the uuid into about:nora:settings in noraneko instead.

who

gh/nyanrus

what is served

lib · std-tsubaki-runtime@noraneko.app · version 0.15.2 · 147099 bytes sha256 699955b3f6b9c901240c330ec4093ba7ad24bc030141ead4da35d49bed28df03 built from drops/std-tsubaki-runtime/src @ 59b96c854c (2026-09-14T07:24:00Z) served at https://dl.f3liz.casa/drop/8a0e317f-4d94-4917-aef4-ad26aac11653/

in the registry

drops/std-tsubaki-runtime/ @ 59b96c854c — この版が出てきた木 library: 直接入れるものではなく、これを [deps] に書いた drop に付いてくる

the source, as it is

These come from source.json next to the manifest — the same text the xpi carries under source/, and dl checks its sha256 against the signed manifest before handing it over. Not a copy of the repository: what you read and what runs came out of the same build. 1 files; press one to open it.

ops/std.tsubaki · 436 lines
# SPDX-License-Identifier: MPL-2.0
#
# std: the few words a drop's logic keeps reaching for. The runtime loads this
# before the drop's own ops/*.tsubaki, so they are simply there.
#
# 鍵のこと。JS から渡ってくる Dict の鍵は文字列しかない(JS の object に
# 他の鍵は無い)。ここで組む Dict は `:width => 40px` の Symbol -- Julia で
# 「名前」を綴るとやりかた。**`:name` で訊けば、どちらの綴りでも見つかる**:
# `get(a, :id, "")` は `"id"` の鍵も `:id` の鍵も読むし、`put` は、もう居る
# ほうに書く(両方が増えないように)。
#
# 逆は、そうしない。`get(a, "id", "")` は文字列の鍵だけを見る -- Julia の
# Dict では Symbol と文字列は別の鍵で、そこは言語のほうに合わせている。
# 「名前で訊く」が `:name` で、そのときだけ、JS から来たかどうかを気にしなくて
# いい、という形。
#
# `a[:id]` には、この作法を教えられない(Dict の添字は dispatch を通らない)。
# `get` は Julia の形のまま -- 既定はいつも声に出して言う -- なので、二引数の
# `get` はここには無い。

"d の中で、k と同じ意味の鍵。Symbol と、その名前の文字列は、同じ鍵。無ければ nothing"
function samekey(d::Dict, k::Symbol)
    name = string(k)
    for kk in keys(d)
        if kk isa Symbol
            if kk == k
                return kk
            end
        elseif kk isa String
            if kk == name
                return kk
            end
        end
    end
    return nothing
end

function get(d::Dict, k::Symbol, default)
    kk = samekey(d, k)
    if kk isa Nothing
        return default
    end
    return d[kk]
end

function haskey(d::Dict, k::Symbol)
    return !(samekey(d, k) isa Nothing)
end

"a copy of d. (Tsubaki has no `copy` for a Dict of its own yet.)"
function copy(d::Dict)
    n = Dict()
    for k in keys(d)
        n[k] = d[k]
    end
    return n
end

"その場で書く。同じ意味の鍵がもう居るなら、そちらに書く(両方が増えないように)"
function setkey!(d::Dict, k, value)
    d[k] = value
    return d
end

function setkey!(d::Dict, k::Symbol, value)
    kk = samekey(d, k)
    if kk isa Nothing
        d[k] = value
    else
        d[kk] = value
    end
    return d
end

"a copy of d with one key set: the state is never changed in place"
function put(d::Dict, key, value)
    return setkey!(copy(d), key, value)
end

"二つを重ねた新しい Dict。同じ鍵は、後ろが勝つ"
function merge(a::Dict, b::Dict)
    n = copy(a)
    for k in keys(b)
        setkey!(n, k, b[k])
    end
    return n
end

# --- 長さと単位 ---------------------------------------------------------------
# 数のすぐ隣に名前を置くと掛け算になる(Julia の juxtaposition)ので、`40px` は
# `40 * px`。掛け算を一つ教えれば、それが単位になる。計算したものは `w * px`
# と書く(Julia でも `(3*w)px` とは書けない)。
#
# 単位の名前は短いので、`s`(秒)のように、うっかり変数名とぶつかりやすいものも
# ある。関数の中の `s = ...` は外を覆うので、そこは困らない。

"数と単位。style の値として、殻が CSS に印字する"
struct Len
    n
    unit
end

function *(n::Int, u::Symbol)
    return Len(n, u)
end

function *(n::Float64, u::Symbol)
    return Len(n, u)
end

px = :px
pct = :pct
em = :em
rem = :rem
fr = :fr
s = :s
ms = :ms
deg = :deg

# --- 色の棚 --------------------------------------------------------------------
# `var(--toolbar-bgcolor, var(--toolbar-background-color))` を毎回書かないための棚。
# 名前は「どこの色か」ではなく **何の色か**(bg / text / hover)にしてある --
# drop が窓の中に居ても about: の頁に居ても、同じ名前で呼べるように。
#
# **fallback は必ず system color で終わる**(Canvas / CanvasText / Menu / MenuText /
# Field / FieldText / AccentColor / ButtonBorder)。その文書に Firefox の変数が
# 無くても色は出るし、明るい / 暗いの切り替えは Firefox と OS がしてくれる --
# drop は何もしなくていい。**自分で色を決めないほうが、ブラウザに似る。**
#
# 一行で一つ、名前と値が隣に並ぶ形。値は文字列なので、style を Dict で書いても、
# 字を連ねる書きかたでも、どちらでも置ける。

module Theme
    # `--toolbar-bgcolor` は **toolbox の中でしか定義されていない**。#browser の中に
    # 置いたもの(タブの横の帯など)からは届かないので、その次に
    # `--toolbar-background-color`(browser.xhtml の root に居る)を置く。
    # 実機で確かめた: 帯の位置で --toolbar-bgcolor は無し、--toolbar-background-color は
    # light-dark(#f9f9fb, rgb(43, 42, 51))。ここを飛ばすと Canvas まで落ちて、色が変わる。
    const bg = "var(--toolbar-bgcolor, var(--toolbar-background-color, var(--in-content-page-background, Canvas)))"
    const text = "var(--toolbar-color, var(--in-content-page-color, CanvasText))"
    const muted = "color-mix(in srgb, currentColor 60%, transparent)"
    const separator = "var(--chrome-content-separator-color, var(--in-content-border-color, ButtonBorder))"
    const hover = "var(--toolbarbutton-hover-background, color-mix(in srgb, currentColor 12%, transparent))"
    const active = "var(--toolbarbutton-active-background, color-mix(in srgb, currentColor 20%, transparent))"
    const accent = "var(--focus-outline-color, AccentColor)"
    const panel_bg = "var(--arrowpanel-background, Menu)"
    const panel_text = "var(--arrowpanel-color, MenuText)"
    const field_bg = "var(--in-content-box-background, Field)"
    const field_text = "var(--in-content-box-text-color, FieldText)"
end

# --- actor を Tsubaki で書くときの言葉 -----------------------------------------
# 殻(_shared/tsubakiActor.ts)と、ここで形を合わせている。struct は JS 側へ
# `{__type => 名前, ...}` の形で渡るので、effect はそのまま「何をするか」の名前になる。

"view の一節。tag と props と子。DOM も preact も知らない"
struct VNode
    tag
    props
    kids
end

# props はそのまま要素に渡る。"on:command" => Action の値は closure ではなく
# 「起こす action」そのもの(closure は postMessage を越えない)。
# "style" => Dict("width" => "320px") のように Dict で書けば inline style になる
# (字を連ねると `;` の混入を自分で見張ることになるので、Dict のほうを)。

el(tag, props, kids) = VNode(tag, props, kids)
el(tag, props) = VNode(tag, props, [])
el(tag) = VNode(tag, Dict(), [])

# --- 言葉 ---------------------------------------------------------------------
# 字を直に書かず、鍵で書く。どのロケールのどの字になるかを決めるのは殻で、
# logic はロケールを知らない -- style と同じ筋(データを渡して、印字は殻)。
#
#     "tooltiptext" => t(:add)
#     menu_item(t(:reload), Reload(id))
#
# 字は drop の strings.toml に並べる。build がそれを畳んで、殻が
# Services.locale で選ぶ。並べていない鍵は、鍵そのものが出る(黙って消えない)。
struct T
    key
end
t(key) = T(key)

# --- 三つの door の、二つの答えの形 ---------------------------------------------
#
#     setup()           Setup(...)   どこに置くか / style / 見ていてほしい pref
#     start(facts)      Frame(...)   最初の一枚
#     dispatch(action)  Frame(...)   次の一枚と、してほしいこと

# 置き場所ひとつ。**書かなかったところは、殻が決める** -- selector は "body"、
# tag は XUL の文書なら "vbox"、そうでなければ "html:div"、at は「その中」。
# ここで同じ既定を書き写すと、殻と二か所で決めることになるので、書かない。
#
#     Anchor(selector = "#urlbar", at = "after")
#     Anchor(name = "menu", selector = "#mainPopupSet", at = "parent", tag = "menupopup")
#
# 一つだけなら name は要らない(殻が "main" と呼ぶ)。二つ以上なら、それぞれに
# name を書いて、view はその名前で答える。
"置き場所ひとつ。書かなかったところは、殻が決める"
struct Anchor
    name
    at
    selector
    tag
    id
    area
    menu
    function Anchor(; name = nothing, at = nothing, selector = nothing, tag = nothing, id = nothing, area = nothing, menu = nothing)
        return new(name, at, selector, tag, id, area, menu)
    end
end

# anchors は「置き場所」の並び。
#
#     Setup(anchors = [Anchor(selector = "#urlbar", at = "after")],
#           style = Style.sheet(), prefs = ["nora.drop.example.on"])
#
# 既定が **空ではなく一つ**なのは、殻が「anchors が空の並び」を「どこにも置かない」
# と読むから -- `Setup()` と書いた人が待っているのは、それではない。
# 見ていてほしい出来事。いまタブだけ。並べられる名前は abi/v1.json の tab_events
# ("open" / "close" / "select" / "move" / "attr" / "restore" / "dblclick")で、
# どれか一つでも並べたら drop.toml に `tabs = "read"` が要る。
#
#     watch = Watch(tabs = ["open", "restore", "close"])
#
# 出来事は一つの action にまとまって来る:
#   Dict("__type" => "TabsChanged", "kind" => …, "tab" => そのタブ, "tabs" => 一覧)
"見ていてほしい出来事"
struct Watch
    tabs
    function Watch(; tabs = [])
        return new(tabs)
    end
end

# 前回から戻ってくるタブを、どう迎えるか。
#
#     restore = Restore(marks = ["ws"], hide_unless = "ws")
#
# `marks` は「その覚書を持って戻ってきたタブに、同じ名前の目印を、同じ値で」。
# `hide_unless` は「その覚書が、**窓の覚書**と違うタブは、仕舞ったまま戻して」。
#
# どちらも **logic には訊かれない**。殻が戻すタブをまとめて挿す、その同じ turn の
# 中で焼くので、worker の返事を待つ時間が無いのです。だから「どう迎えるか」だけを
# 先にデータで渡しておく -- style や言葉と同じ筋で、決めるのはこちら、するのは殻。
#
# 戻ってきてから目印を付け直すのでも動きはするけれど、そのときは **一瞬ぜんぶ
# 見える**。束で仕舞う drop には、それがいちばん困る一瞬なので。
"前回から戻ってくるタブを、どう迎えるか"
struct Restore
    marks
    hide_unless
    function Restore(; marks = [], hide_unless = nothing)
        return new(marks, hide_unless)
    end
end

"setup() の答え。どこに置くか、その style、見ていてほしい pref と出来事"
struct Setup
    anchors
    style
    prefs
    prefs_json
    watch
    tab_values
    window_values
    restore
    function Setup(; anchors = [Anchor()], style = nothing, prefs = [], prefs_json = [],
                     watch = nothing, tab_values = [], window_values = [], restore = nothing)
        return new(anchors, style, prefs, prefs_json, watch, tab_values, window_values, restore)
    end
end

# 一枚 = 描くものと、してほしいこと。
# view は VNode 一つ(置き場所も一つのとき)か、Dict(anchor の名前 => VNode)。
# 窓の中の離れた二か所(タブの横の列と、#mainPopupSet の下の menupopup)は
# 一本の木にできないので、setup が "anchors" で名前をつけて、ここでその名前に答える。
"start と dispatch の答え。描くものと、してほしいこと"
struct Frame
    view
    effects
    function Frame(view, effects = [])
        return new(view, effects)
    end
end

# 前からの綴り。`frame(view)` と書いてある drop は、そのまま動く
frame(view) = Frame(view)
frame(view, effects) = Frame(view, effects)

# 殻が carry out できることの全部(これ以上が要る drop は actor.ts を書く)
struct SetPref
    name
    value
end

struct OpenURL
    url
end

# ブラウザ自身の命令を一つ。名前は abi/v1.json の commands にある綴りで、
# drop.toml の [permissions] の commands にも同じ名前が並ぶ。
#
#     DoCommand("back")
struct DoCommand
    name
end

struct Log
    text
end

# 自分が置いたものの中の menupopup を、画面のこの場所で開く
struct OpenPopup
    selector
    x
    y
end

# 自分が置いた <browser> の、いま読んでいるページを読み直す
struct ReloadFrame
    selector
end

# --- 事実を「訊く」--------------------------------------------------------------
# logic は事実を作らない。新しい uuid も、いま見ているタブの URL も、drag のあとに
# 箱が実際になった幅も、こちらでは分からない。action に穴を開けて殻に埋めさせるので
# はなく、**訊いて、名前をつけた action で返してもらう**。返事は普通の dispatch で
# 来るので、「外から来るものは action に詰めてから投げる」の形のまま。

# 集まったら Dict("__type" => action, 名前 => 値) が dispatch される。
# いま訊ける事実は二つ:
#   "uuid"  新しい uuid
#   "url"   いま見ているタブの URL(http/https のときだけ。ほかは "")
#
#     Ask(["uuid", "url"], "AddPanel")
"殻に事実を訊く。返事は、その名前の action で来る"
struct Ask
    fields
    action
end

# 返事は Dict("__type" => action, "width" => …, "height" => …)。
# selector は **この drop が置いた host とその中**だけを探す(ブラウザの他の場所は
# 見ない)。view が id を書いているなら、その id をそのまま渡せる。
#
#     Measure("#nora-webpanel-box", "SetWidth")
"自分が置いたものの中の、この selector に当たる要素の実測を訊く"
struct Measure
    selector
    action
end

# --- タブ ----------------------------------------------------------------------
# タブは、この drop が置いたものではない。だから触れるのは **自分の付けたもの**
# だけ: 目印(attr)と覚書(value)、そして名札のところに出す入力欄。
#
# 目印は見えるもの(CSS で拾って、タブの見た目を変えられる)、覚書は残るもの
# (閉じて開き直しても、そのタブに付いてくる)。名前は短く書いて、
# `data-nora-<uuid>-name` / `nora.<uuid>.name` に伸ばすのは殻。生の CSS の中の
# `{attr}` も、殻が接頭辞に書き換える -- drop は自分の uuid を一度も綴らない。
#
#     SetTabAttr(id, "name", "ねこ")        → .tabbrowser-tab[{attr}name]
#     SetTabValue(id, "name", "ねこ")       → 再起動しても、そのタブに

"タブに、この drop の目印を一つ(見えるもの)"
struct SetTabAttr
    tab
    name
    value
end

"その目印を外す"
struct ClearTabAttr
    tab
    name
end

"タブに、この drop の覚書を一つ(残るもの)"
struct SetTabValue
    tab
    key
    value
end

"その覚書を消す"
struct ClearTabValue
    tab
    key
end

# 打ち終わったら Dict("__type" => action, "tab" => …, "value" => 打たれた字)が
# dispatch される。Escape で閉じたときは **何も来ない**(状態はこちらに無いので、
# 何もしないでいい)。
"そのタブの名札のところに、字を打つ欄を出す"
struct Prompt
    tab
    action
    value
    placeholder
end

# 仕舞う・見せる・選ぶ。ここは `tabs = "write"` の段。
#
# 束を切り替えるときは、**先に選んで、あとで仕舞う**。選ばれているタブは本体が
# 仕舞わないので、順が逆だと一枚だけ残ります。
"そのタブを、タブ帯から仕舞う"
struct HideTab
    tab
end

"仕舞ったタブを、また見せる"
struct ShowTab
    tab
end

"そのタブを選ぶ"
struct SelectTab
    tab
end

# 窓の覚書。タブのものと同じ作法(鍵は殻が長くする)で、置き場所が窓なだけ。
# 読み戻す鍵は setup() の `window_values` に並べる。
"この窓に、この drop の覚書を一つ(残るもの)"
struct SetWindowValue
    key
    value
end

"その覚書を消す"
struct ClearWindowValue
    key
end

What a drop is, how it reaches you, and what it is not: f3liz.casa/noraneko/drops/. Served by https://dl.f3liz.casa/drop, signed by https://github.com/f3liz-casa/noraneko-registry/.github/workflows/verify-and-sign.yml@refs/heads/main.

This page is built when you ask for it, out of what https://dl.f3liz.casa/drop is serving right now (2026-09-23T08:38:28.032Z). Drafted by Shiro, an AI assistant, sitting next to nyanrus.