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.
about:nora:settings in noraneko instead.
who
gh/nyanruswhat 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.