on master help sign in
43 files changed+2235−267
since splitsnapshots
unifiedsplit
.gitignore+4 INSTRUCTIONS.md+150 LICENSE+7 NOTICE+39 README.md+307 dev/elm-live.sh+19−2 docs/screenshot.png flake.lock+45 flake.nix+114 frontend/airlock.patch+16 frontend/assets/css/fonts.css+10 frontend/assets/css/main.css+45−9 frontend/assets/img/tile.png frontend/assets/js/tilt.js−11 frontend/elm-srcs.nix+267 frontend/elm.json+9−2 frontend/index.html+2−2 frontend/registry.dat frontend/script.js+3−2 frontend/src/Main.elm+135−47 zod/seax/app/seax.hoon+103−80 zod/seax/desk.docket-0+10 zod/seax/lib/dbug.hoon+9−3 zod/seax/lib/docket.hoon+223 zod/seax/lib/engines.hoon+2−3 zod/seax/lib/engines/alexandria.hoon−32 zod/seax/lib/engines/crowdview.hoon+3−5 zod/seax/lib/engines/urbit-dev.hoon+36 zod/seax/lib/engines/wiby.hoon+3−5 zod/seax/lib/engines/yep.hoon−30 zod/seax/lib/server.hoon+159 zod/seax/mar/css.hoon+25 zod/seax/mar/docket-0.hoon+25 zod/seax/mar/hoon.hoon+285−31 zod/seax/mar/html.hoon+22 zod/seax/mar/js.hoon+25 zod/seax/mar/png.hoon+12 zod/seax/mar/txt-diff.hoon+16 zod/seax/mar/txt.hoon−1 zod/seax/mar/woff2.hoon+12 zod/seax/sur/docket.hoon+82 zod/seax/sur/seax.hoon+10−1 zod/seax/sys.kelvin+1−1
.gitignore+4
@@ -6,3 +6,7 @@ zod/.http.ports
66zod/.run
77zod/.urb
88zod/.vere.lock
9+result
10+result-*
11+frontend/assets/fonts
12+frontend/airlock
INSTRUCTIONS.mdnew file+150
@@ -0,0 +1,150 @@
1+# Installation Instructions
2+
3+You need [Nix](https://nixos.org/download/) with flakes enabled to build the desk, and a ship on Urbit OS 408k to run it.
4+If flakes aren't enabled, add `--extra-experimental-features 'nix-command flakes'` after `nix` in each command.
5+The flake builds on x86_64-linux, aarch64-linux and aarch64-darwin; nixpkgs no longer supports Intel Macs.
6+On an Intel Mac, build the desk on one of those systems and copy the files in `result` over, e.g. with `scp -r` or `cp -RL` (a plain `cp -R` copies only the `result` symlink).
7+
8+## On a fake ship
9+
10+To try %seax out, run it on a fake ~zod. This also needs the Urbit runtime (Vere) 4.4 or newer, or Nix to run it. These steps were tested with Vere 4.6.
11+
12+1. Clone the **seax** project and build the desk. The build ends up in `result`.
13+
14+```bash
15+$ git clone https://github.com/iko-soy/seax
16+$ nix build ./seax
17+```
18+
19+2. Download the v4.6 pill (Urbit OS 408k) and start your fake zod using the pill.
20+
21+```bash
22+$ curl -LO https://bootstrap.urbit.org/urbit-v4.6.pill
23+$ urbit -B urbit-v4.6.pill -F zod --http-port 8080
24+```
25+
26+Without Vere installed, you can run the one from the nixpkgs that the flake pins instead:
27+
28+```bash
29+$ nix run --inputs-from ./seax nixpkgs#urbit -- -B urbit-v4.6.pill -F zod --http-port 8080
30+```
31+
32+On an Intel Mac, download [Vere's macOS x86_64 build](https://bootstrap.urbit.org/vere/live/v4.6/vere-v4.6-macos-x86_64), make it executable with `chmod +x vere-v4.6-macos-x86_64`, and run `./vere-v4.6-macos-x86_64` in place of `urbit`.
33+
34+`--http-port 8080` puts the web interface on port 8080. Without it, Vere tries port 80 first, which works on macOS.
35+If port 8080 is taken, Vere stops; choose another port, and use it in step 6.
36+
37+Booting takes about a minute.
38+The fake ship can't download apps such as Landscape, so it prints errors like `http: fail (..., 504)` and `docket: thread failed`; they don't affect %seax.
39+
40+3. Now, inside dojo run the following:
41+```bash
42+dojo> |new-desk %seax
43+dojo> |mount %seax
44+```
45+
46+4. Back in your shell, in the directory where you ran step 1, replace the mounted desk with the build.
47+Don't do this inside `seax`: its own `zod/seax` holds the desk's source.
48+```bash
49+$ rm -r zod/seax/*
50+$ cp -r result/* zod/seax/
51+$ chmod -R u+w zod/seax
52+```
53+
54+The files Nix builds are read-only, and `chmod` makes the copies writable, so that the next update can replace them.
55+
56+5. In the dojo install the seax agent.
57+```bash
58+dojo> |commit %seax
59+dojo> |install our %seax
60+```
61+
62+6. Go to `http://localhost:8080/apps/seax/` in your browser.
63+Log in with the code that `+code` prints in the dojo.
64+If the page only says "bad session auth", your browser still has a cookie from an earlier fake ship; reload the page.
65+
66+To update the desk later, run `git -C seax pull` and `nix build ./seax`, repeat step 4 and run `|commit %seax`.
67+
68+## On your own ship
69+
70+1. Build the desk as in step 1 above.
71+
72+2. In your ship's dojo, create the desk and mount it. `|mount` puts its files in your pier, at `<pier>/seax`.
73+```bash
74+dojo> |new-desk %seax
75+dojo> |mount %seax
76+```
77+
78+3. Replace the mounted desk with the build:
79+```bash
80+$ rm -r <pier>/seax/*
81+$ cp -r result/* <pier>/seax/
82+$ chmod -R u+w <pier>/seax
83+```
84+
85+4. Install it:
86+```bash
87+dojo> |commit %seax
88+dojo> |install our %seax
89+```
90+
91+5. Open `/apps/seax/` at your ship's web address, or click the %seax tile in Landscape.
92+
93+To update it later, run `git -C seax pull` and `nix build ./seax`, then repeat steps 3 and 4.
94+
95+### Upgrading from the 2023 version
96+
97+The 2023 desk was made for Urbit OS 413k, so it was suspended before your ship could update to 408k.
98+Upgrade it with the steps above, without `|new-desk`: run `|mount %seax` if the desk isn't mounted, then replace its files and run `|commit %seax` and `|install our %seax`.
99+
100+The new agent drops the old searches and closes their subscriptions.
101+It also drops the copy of your Tlon notes, if the old version made one, and the list of peers, and calls off the pings that are still waiting for an answer; see [Peers (disabled)](README.md#peers-disabled).
102+
103+Because the desk dates from before Urbit OS 410k, your ship counts it as essential: a later Urbit OS update waits until %seax supports it, instead of suspending %seax.
104+To have it suspended like other apps, run:
105+```bash
106+dojo> |essential-desk %seax |
107+```
108+`+vats %seax` shows which it is, under `essential desk`.
109+
110+### Removing it
111+
112+- `|uninstall %seax` stops the agent and keeps its data. `/apps/seax/` then answers with an error.
113+- `|nuke %seax` deletes the agent's data. It asks for confirmation first.
114+- `|unmount %seax` deletes `<pier>/seax`. The desk itself stays in your ship, because no command deletes desks.
115+
116+# Development
117+
118+With %seax installed on a running ship (see above), run this in another terminal:
119+```bash
120+$ ./seax/dev/elm-live.sh
121+```
122+
123+This serves the frontend at `http://localhost:8000`, with Elm's debugger, and rebuilds it when the code changes.
124+Requests to `/~/` are passed on to the ship at `http://localhost:8080`; set `SHIP_URL` if your ship is elsewhere.
125+Log in at `http://localhost:8000/~/login` first (reload on "bad session auth"), then open `http://localhost:8000/`.
126+
127+With Nix, the script gets Elm, elm-live, the web font and a patched [airlock](https://github.com/iko-soy/airlock) by itself.
128+Without Nix, you need `elm` 0.19.2 and `elm-live`, and airlock at commit 7e85737 (as pinned in `flake.lock`) in `frontend/airlock`, with `frontend/airlock.patch` applied:
129+```bash
130+$ git clone https://github.com/iko-soy/airlock seax/frontend/airlock
131+$ git -C seax/frontend/airlock checkout 7e85737
132+$ git -C seax/frontend/airlock apply ../airlock.patch
133+```
134+There is no `frontend/assets/fonts/` then, so the browser uses another font wherever the page would use Roboto.
135+
136+To build the desk without Nix, set up airlock as above and run `elm make src/Main.elm --optimize --output elm.js` in `seax/frontend`.
137+Then, in step 4, copy `seax/zod/seax/*` instead of `result/*`, and copy `index.html`, `elm.js`, `script.js`, `fetch-event-source.js` and `assets/` from `seax/frontend` into the mounted desk's `web/` directory.
138+
139+The Nix build pins the Elm packages in `frontend/elm-srcs.nix` and `frontend/registry.dat`.
140+After changing `frontend/elm.json`, regenerate them:
141+```bash
142+$ cd seax/frontend
143+$ nix develop .. --command sh -c 'elm2nix convert > elm-srcs.nix && elm2nix snapshot'
144+```
145+
146+# Usage Instructions
147+
148+Type a query, then press Enter or click "search": %seax collects results from its search sources.
149+On the results page, click a source to show only its results; click more sources to add theirs, and click a source again to remove it.
150+The README's [Usage](README.md#usage) section has more.
LICENSEnew file+7
@@ -0,0 +1,7 @@
1+Copyright 2023 Ilia Kostiuchenko and Evangelos Lamprou
2+
3+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
NOTICEnew file+39
@@ -0,0 +1,39 @@
1+%seax is under the MIT License in LICENSE, except for these files from other
2+projects:
3+
4+- zod/seax/lib/dbug.hoon, zod/seax/lib/server.hoon and the marks in
5+ zod/seax/mar/ other than docket-0.hoon are copied from pkg/base-dev in
6+ urbit/urbit 408k-2 (https://github.com/urbit/urbit).
7+ MIT License, Copyright (c) 2015 Urbit.
8+
9+- zod/seax/lib/docket.hoon, zod/seax/sur/docket.hoon and
10+ zod/seax/mar/docket-0.hoon are copied from pkg/landscape in urbit/urbit
11+ 408k-2. MIT License, Copyright (c) 2015 Urbit.
12+
13+- zod/seax/lib/de-html.hoon is adapted from ++de-xml in
14+ pkg/arvo/sys/zuse.hoon of urbit/urbit, as of July 2023.
15+ MIT License, Copyright (c) 2017 Urbit.
16+
17+- frontend/fetch-event-source.js is the bundle from airlock's example
18+ (https://github.com/iko-soy/airlock), a modified build of
19+ @microsoft/fetch-event-source (https://github.com/Azure/fetch-event-source).
20+ MIT License, Copyright (c) Microsoft Corporation, and
21+ MIT License, Copyright 2023 Ilia Kostiuchenko.
22+
23+- The built frontend includes the Roboto font, which is under the SIL Open
24+ Font License 1.1. Its license is next to it in web/assets/fonts/.
25+
26+- The built frontend's web/elm.js includes compiled code from airlock
27+ (MIT License, Copyright 2023 Ilia Kostiuchenko) and from some of the Elm
28+ packages in frontend/elm.json, all of them under the BSD-3-Clause, MIT or
29+ Apache-2.0 license. elm.json also lists MPL-2.0 packages by ianmackenzie,
30+ such as elm-geometry and elm-3d-scene, but no code from them is compiled
31+ in. Each package's license comes with its source.
32+
33+The MIT License, under the copyright lines above:
34+
35+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
36+
37+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
38+
39+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
README.mdnew file+307
@@ -0,0 +1,307 @@
1+<p align="center">
2+ <img src="frontend/assets/img/logo.png" alt="%seax" width="510">
3+</p>
4+
5+The [Urbit](https://urbit.org/) meta-search engine.
6+
7+<p align="center">
8+ <img src="docs/screenshot.png" alt="%seax showing results for &quot;urbit&quot; from %wiby and %urbit-dev" width="700">
9+</p>
10+
11+## About
12+
13+It's time Urbit got some searching capabilities.
14+
15+For most people, their computer is a bootloader to the closest search engine. Searching is a central part of everyone's computing workflow.
16+
17+The `%seax` meta-search engine is able to aggregate results from open search APIs and bring them inside the comfort of your Urbit ship. No longer do you have to keep a second tab open.
18+
19+Let's make searching fun again. :)
20+
21+## Contents
22+
23+- [Features](#features)
24+- [Search sources](#search-sources)
25+- [Requirements](#requirements)
26+- [Installation](#installation)
27+- [Usage](#usage)
28+- [How it works](#how-it-works)
29+- [Ranking](#ranking)
30+- [Privacy and network traffic](#privacy-and-network-traffic)
31+- [Peers (disabled)](#peers-disabled)
32+- [Debugging](#debugging)
33+- [Adding a search source](#adding-a-search-source)
34+- [Development](#development)
35+- [Project layout](#project-layout)
36+- [Known issues](#known-issues)
37+- [History](#history)
38+- [Credits](#credits)
39+- [License](#license)
40+
41+## Features
42+
43+- One search box for several search engines. Your ship sends the query to all of them and shows their results on one page.
44+- Results appear as each source answers, without waiting for the slowest.
45+- A page that more than one source returns is shown once and ranked higher.
46+- Click a source's name to see only its results.
47+- Runs on your own ship at `/apps/seax/`, with a tile in Landscape. Only you can search: visitors who aren't logged in are sent to the login page.
48+- Nothing runs in the background: %seax sends requests only when you search.
49+
50+## Search sources
51+
52+| Source | Shown as | What it is | Request your ship sends | Status (October 2026) |
53+|---|---|---|---|---|
54+| [wiby](https://wiby.me/) | `%wiby` | "Search Engine for the Classic Web", an index of simple, older-style pages | `GET https://wiby.me/json/?q=<query>`, wiby's public JSON API | Works |
55+| [Urbit docs](https://docs.urbit.org/) | `%urbit-dev` | The site search of docs.urbit.org, which is built on GitBook | `POST https://docs.urbit.org/~gitbook/search` with `{"query": "<query>", "scope": {"mode": "all"}}` | Works |
56+| [CrowdView](https://crowdview.ai/) | `%crowdview` | A search engine for forum discussions (Reddit, Hacker News, Stack Exchange and others) | `GET https://crowdview-next-js.onrender.com/api/search-v3?query=<query>`, the endpoint crowdview.ai's own page uses | Works, but has no results for many queries, such as "urbit" |
57+
58+The query is URL-encoded in the GET requests, and sent as a JSON string to the Urbit docs, whose request also has a `content-type: application/json` header. Your ship adds no user agent, cookies or other headers: Vere sends only `Host`, and `Content-Length` when there's a body.
59+
60+The page's sources row shows each source that is still loading or found something. A source that fails, or has no results, is hidden.
61+
62+Until October 2026, %seax also searched [Yep](https://yep.com/). By then Yep blocked requests that don't come from a browser, so it returned no results, but every search still sent it your query. Its official API needs an API key and is paid after 1,000 requests.
63+
64+## Requirements
65+
66+- A ship running Urbit OS 408k, which needs Vere 4.4 or newer (4.6 is current). The desk declares `[%zuse 408]` in `zod/seax/sys.kelvin`. When your ship moves to a newer Urbit OS, it suspends %seax until you install a version of the desk that declares the new one. A desk upgraded from the 2023 version is the exception; see [Installation](#installation).
67+- To build the desk: [Nix](https://nixos.org/download/) with flakes enabled, on x86_64-linux, aarch64-linux or aarch64-darwin. Nix fetches Elm and the other build tools already built from cache.nixos.org, so the build only compiles the frontend and copies files.
68+
69+## Installation
70+
71+[INSTRUCTIONS.md](INSTRUCTIONS.md) has the full steps: on a fake ship to try %seax out, on your own ship, upgrading the 2023 version, and removing %seax. In short:
72+
73+1. Build the desk: `nix build` in a clone of this repository puts it in `result/`.
74+2. In your ship's dojo, create and mount a desk: `|new-desk %seax`, then `|mount %seax`.
75+3. Replace the files of the mounted desk, `<pier>/seax/`, with those in `result/`. Nix's files are read-only, so make the copies writable with `chmod -R u+w <pier>/seax`, or the next update can't replace them.
76+4. In the dojo: `|commit %seax`, then `|install our %seax`.
77+
78+If you installed the 2023 version, which was made for Urbit OS 413k, it was suspended before your ship could update to 408k. Upgrade it the same way, without `|new-desk`. The new agent drops the old searches, the list of peers, and the copy of your Tlon notes if the old version made one. Because the desk dates from before Urbit OS 410k, your ship counts it as essential: a later Urbit OS update waits until %seax supports it, instead of suspending %seax. Run `|essential-desk %seax |` to have it suspended like other apps.
79+
80+To remove %seax, run `|uninstall %seax`, which stops it and keeps its data. `|nuke %seax` then deletes the data.
81+
82+## Usage
83+
84+Open `/apps/seax/` at your ship's web address, e.g. `http://localhost:8080/apps/seax/`, or click the %seax tile in Landscape. If you aren't logged in, you'll be asked for your `+code` first.
85+
86+- Type a query, then press Enter or click **search**.
87+- The **Sources** row names each source that is still loading or has results. Sources that are loading pulse between grey and black, and can't be clicked until they finish.
88+- Click a source to show only its results. Click more sources to add theirs, and click a source again to remove it. With no source selected, all results are shown. The selection stays when you search again.
89+- Each result shows an emoji, the title and the address. The emoji is picked from a hash of the title and is only decoration. Titles are shown without HTML tags, and with HTML entities and `%` escapes decoded.
90+- Click a title to open the result in a new tab. The address under it isn't a link.
91+- While no source has results, the page says "No results found... :)". It also says so while the sources are still loading.
92+- The logo on the start page tilts as you move the mouse over it.
93+- The page's address doesn't change when you search, so you can't bookmark a search. The back button leaves the app, and reloading the page brings back the start page.
94+- The **[About]** link at the bottom leads to this README, and **[Code]** to this repository.
95+
96+## How it works
97+
98+%seax is a Gall agent, [`zod/seax/app/seax.hoon`](zod/seax/app/seax.hoon), and an Elm app, [`frontend/`](frontend). The agent serves the built Elm app from the desk's `web/` directory.
99+
100+```mermaid
101+sequenceDiagram
102+ participant B as Browser (Elm app)
103+ participant E as Eyre
104+ participant S as %seax
105+ participant I as Iris
106+ participant W as wiby, Urbit docs, CrowdView
107+ B->>E: subscribe to /search/<query> on %seax
108+ E->>S: watch /search/<query>
109+ S-->>B: %flush: every source is loading
110+ S->>I: one HTTP request per source
111+ I->>W: requests from your ship
112+ W-->>I: responses
113+ I-->>S: a response
114+ S-->>B: %drain: what changed in the sources and ranked results
115+```
116+
117+- **Serving the app.** The agent binds `/apps/seax` in Eyre and serves the files in the desk's `web/` directory to logged-in users. Everyone else is redirected to `/~/login`. The Landscape tile comes from `zod/seax/desk.docket-0`.
118+- **Searching.** The Elm app subscribes to `/search/<query>` on the agent through an Eyre channel, using [airlock](https://github.com/iko-soy/airlock). The agent accepts the subscription only from the ship itself. For a query that isn't open yet, it sends one HTTP request per source through Iris. Every new subscriber gets the current state right away.
119+- **Streaming.** Each time a source answers, the agent parses the answer, ranks all the results so far and sends the change. The first message is the whole state (`%flush`), and later ones are diffs of it (`%drain`), made by [`lib/sink.hoon`](zod/seax/lib/sink.hoon) and [`lib/noun-diff.hoon`](zod/seax/lib/noun-diff.hoon). airlock's `Ur.Sub.sink` applies them in the browser.
120+- **Sharing searches.** While a query has subscribers, its results stay in the agent's state. Another tab that searches for the same query gets them right away, without new requests. Queries have to match exactly, including case and spaces. When the last subscriber of a query leaves, the agent forgets the query. A new search in the same tab leaves the old query, but closing or reloading a tab doesn't: Eyre ends the tab's subscriptions when it closes the tab's channel, 12 hours after the tab disconnects. Results aren't refreshed while a query stays open.
121+- **Failures.** A source fails when its response has no body, or the body isn't the JSON the source's parser expects. The agent doesn't look at the HTTP status. When Vere can't make a request (DNS, connection or TLS errors, or a connection that stalls for 5 minutes), Iris returns status 504 with no body, so that source fails too. Iris follows redirects with status 301, 303 and 307; other redirects make the source fail. Failed requests aren't retried.
122+
123+## Ranking
124+
125+Each source has a weight in [`zod/seax/lib/engines.hoon`](zod/seax/lib/engines.hoon), currently `.2` (2.0) for all of them. [`zod/seax/lib/rank.hoon`](zod/seax/lib/rank.hoon) merges the results that have exactly the same URL. Each URL gets a score, which is the product of three numbers:
126+
127+- the weights of the sources that returned it, multiplied together,
128+- the number of times it was returned,
129+- the sum of 1/position for each time, counting positions from 1.
130+
131+Results are shown with the highest score first. For a URL that only one source returns, the score is the source's weight divided by the position: a first result scores 2, a second one 1, a third one 0.67. Since the weights are all the same, the first results of all sources come first, then their second results, and so on. Results with the same score come in no particular order.
132+
133+URLs that differ only by a trailing slash or by `http`/`https` count as different URLs. A merged result keeps the title from one of its sources. See [Known issues](#known-issues) for a bug in the scores of merged results.
134+
135+## Privacy and network traffic
136+
137+- **Searches.** Your query goes from your ship to wiby.me, docs.urbit.org and crowdview-next-js.onrender.com, over HTTPS. Those services see the query and your ship's IP address.
138+- **Nothing else.** %seax makes requests only when you search for a query that isn't already open. It has no timers, and it doesn't contact other ships, except once: an upgrade from an older version calls off that version's pings to its peers that are still waiting for an answer. See [Peers (disabled)](#peers-disabled).
139+- **The page.** It loads nothing from other sites: the fonts, scripts and images all come from your ship. airlock normally pokes ~zod when it opens a channel, and [`frontend/airlock.patch`](frontend/airlock.patch) removes that poke. The bug is reported at [iko-soy/airlock#15](https://github.com/iko-soy/airlock/issues/15). Eyre's login page, which comes with Urbit, loads its fonts from rsms.me and storage.googleapis.com.
140+- **Stored data.** Search results stay in the agent's state while their query is open, which can be up to 12 hours after you close its tab. As with everything your ship does, the requests and responses are also recorded in your pier's event log. The agent also prints your queries and their results in the dojo.
141+- **Other ships.** Only you can search. %seax refuses other ships' pokes and searches, so they can't use it through your ship.
142+
143+## Peers (disabled)
144+
145+From August 2023 until this version, %seax kept a list of peers: other ships that might run %seax. The list started with ~zod and ~racfer-hattes. Every 30 seconds, %seax pinged its peers by subscribing to `/ping` on their %seax, and told them which peers had answered, so that ships running %seax could find each other.
146+
147+Nothing ever used the list, and neither the code nor the commit messages say what it was for. The 2023 code didn't work well either: a peer was dropped after missing one ping, every other check emptied the list, and from October 2023 ships refused the peer lists that other ships sent them, because `+on-poke` couldn't read pokes from other ships.
148+
149+This version disables the peer network:
150+
151+- It doesn't ping other ships or send them anything, and it has no timers.
152+- An upgrade drops the list and calls off the pings that are still waiting for an answer.
153+- It refuses pings and peer lists from other ships.
154+
155+## Debugging
156+
157+%seax prints a lot to the dojo:
158+
159+- the wire of every response it gets from Arvo, e.g. `/search-result/<query>/<source>` for each HTTP response,
160+- each source's parsed results, followed by the state of every source,
161+- the path of every search that closes,
162+- the stack trace of every crash while it handles a response, such as the late answers in [Known issues](#known-issues).
163+
164+A few dojo commands help:
165+
166+- `:seax +dbug` prints the agent's state: each open query, with its number of subscribers and the results of each source. A source that is still loading has no entry, `~` means that it failed, and `[~ ~]` that it found nothing.
167+- `:seax +dbug [%incoming ~]` lists the subscriptions to the agent, i.e. open searches.
168+- `:seax +dbug [%outgoing ~]` lists the agent's own subscriptions. It should print "no matching subscriptions", also right after an upgrade from an older version.
169+- `:seax +dbug [%state '~(key by search-subscriptions)']` evaluates Hoon against the state, here listing the open queries.
170+- `+vats %seax` shows whether the desk is running or suspended, e.g. after an Urbit OS update, and which Urbit OS version it declares in `/sys/kelvin`.
171+
172+When Vere can't make a request, it prints `http: fail (<n>, 504): <reason>`, e.g. `unknown node or service` when a DNS lookup fails, and that source counts as failed.
173+
174+## Adding a search source
175+
176+1. Create `zod/seax/lib/engines/<name>.hoon`. It holds an `engine` core: `++request` builds the HTTP request for a query, and `++results` parses the response into a list of `[title url]`, or `~` if it can't. `++get` in [`zod/seax/sur/seax.hoon`](zod/seax/sur/seax.hoon) makes a GET request with the query appended to a URL. It escapes the query twice, because Vere unescapes the URL once before sending it. For example, for an API that returns `[{"title": ..., "url": ...}, ...]`:
177+
178+ ```hoon
179+ /- *seax
180+ ^- engine
181+ =<
182+ |%
183+ ++ request
184+ |= query=tape
185+ ^- request:http
186+ (get "https://example.com/search?q=" query)
187+ ++ results
188+ |= response=mime-data:iris
189+ ^- (unit (list search-result))
190+ ;< jon=json _biff (de:json:html q.data.response)
191+ (apex jon)
192+ --
193+ |%
194+ ++ apex
195+ =, dejs-soft:format
196+ (ar (ot ~[title+so url+so]))
197+ --
198+ ```
199+
200+2. In [`zod/seax/lib/engines.hoon`](zod/seax/lib/engines.hoon), import it (`/+ engines-<name>`) and add `[name=%<name> engine=engines-<name> weight=.2]` to the list. The weight is a floating-point literal: `.2` is 2.0.
201+3. Build and commit the desk again. Nix only sees files that git tracks, so `git add` the new file before building. The Elm app shows every source the agent has, so it doesn't need any change.
202+
203+A source counts as failed if `++results` returns `~` or an empty list. Note that `ar` from `dejs-soft` gives `~` if even one result doesn't parse.
204+
205+## Development
206+
207+[INSTRUCTIONS.md](INSTRUCTIONS.md#development) explains how to work on the frontend. `dev/elm-live.sh` serves it at `http://localhost:8000` with Elm's debugger, rebuilds it when the code changes, and passes requests for `/~/` on to a running ship.
208+
209+The flake has these outputs:
210+
211+- `packages.<system>.desk`, the default: the desk, with the built frontend in `web/`.
212+- `packages.<system>.frontend`: the built frontend.
213+- `packages.<system>.fonts`: the Roboto Light web font, converted to WOFF2 from nixpkgs' Roboto, and its license.
214+- `packages.<system>.airlock-src`: airlock at the commit pinned in `flake.lock`, with `frontend/airlock.patch` applied.
215+- `devShells.<system>.default`: a shell with `elm` 0.19.2, `elm-live` and `elm2nix`.
216+
217+To change the agent, edit `zod/seax`, rebuild the desk and copy it into your mounted desk, as in the installation steps, then `|commit %seax`. The agent reloads with its state. Nix only sees files that git tracks, so `git add` new files before building.
218+
219+## Project layout
220+
221+```
222+README.md, INSTRUCTIONS.md this file, and how to install and develop
223+LICENSE, NOTICE the MIT License, and the files from other projects
224+flake.nix, flake.lock the Nix build: desk, frontend, font and airlock
225+dev/elm-live.sh frontend development server, with a proxy to a ship
226+docs/screenshot.png the screenshot above
227+zod/seax/ the %seax desk
228+ app/seax.hoon the agent
229+ sur/seax.hoon the search-result and engine types, and ++get
230+ lib/engines.hoon the sources and their weights
231+ lib/engines/*.hoon one file per source
232+ lib/rank.hoon merges and ranks results
233+ lib/sink.hoon sends state as a %flush and then %drain diffs
234+ lib/noun-diff.hoon diffs of nouns, for lib/sink.hoon
235+ lib/dbug.hoon, lib/server.hoon from Urbit's base-dev files
236+ lib/docket.hoon, sur/docket.hoon for the Landscape tile
237+ lib/de-html.hoon, lib/extract-links.hoon, lib/extract-text.hoon, lib/words.hoon
238+ HTML and text helpers from the first, crawler
239+ version, which nothing uses now
240+ mar/ marks for the desk's files
241+ desk.docket-0 the tile: title, description, colour, image, URL,
242+ version, website and license
243+ desk.bill the agent to start: %seax
244+ sys.kelvin [%zuse 408]
245+frontend/ the Elm app
246+ src/Main.elm the whole app
247+ src/Route.elm, src/Pages/Search.elm unused leftovers of an early version;
248+ Pages/Search.elm doesn't compile
249+ index.html, script.js the page, and the glue between Elm and the event stream
250+ fetch-event-source.js event-stream client, from airlock's example
251+ assets/ CSS, the logo and tile images, the logo tilt script
252+ elm.json Elm 0.19.2 and its packages; airlock's code comes from airlock/src
253+ elm-srcs.nix, registry.dat pinned Elm packages, for the Nix build
254+ airlock.patch stops airlock from poking ~zod when it opens a channel
255+```
256+
257+## Known issues
258+
259+The search sources:
260+
261+- **CrowdView often has no results,** and %seax shows a source without results the same way as a failed one: it's hidden.
262+- **One malformed result fails a whole source.** If any result in a source's response doesn't parse, the whole source counts as failed.
263+
264+The agent:
265+
266+- **Scores of merged results are wrong.** `lib/rank.hoon` adds the 1/position terms of a merged URL, and compares scores, as integers instead of floating-point numbers. As a result:
267+ - URLs that 2 sources return always come before URLs that only 1 returns. Among themselves, they come roughly in order of the product of their two positions, smallest first, instead of by their scores, and those whose positions multiply to 4 or less tie at the top.
268+ - URLs that all 3 sources return come before those.
269+ - Single-source results are ranked correctly.
270+- **Late answers crash the agent.** If you search for something else before every source has answered, the late answers to the old query crash the agent's `+on-arvo`, and the dojo prints a stack trace for each one. Nothing else is affected. The agent never cancels its requests.
271+- **Restarting the ship during a search leaves sources loading.** When Vere restarts, Iris cancels the requests that are still open. The agent crashes on each cancellation, and those sources keep loading until the query closes, which can be 12 hours after you close its tab.
272+- **A source that stalls keeps loading** until Vere's HTTP client gives up after 5 minutes.
273+- **Results are never refreshed.** A query that is open in any tab, or in a tab closed less than 12 hours ago, gives the results it already has.
274+
275+The page:
276+
277+- **A `%` in a title can hide it.** Titles are `%`-decoded, and when that fails, as on a lone `%`, the title disappears and the result can't be clicked. Escapes that do decode are decoded even when they are part of the real title.
278+- **Narrow screens are cut off.** The logo and the search box have fixed widths, so on a phone the search box and the search button are cut off, and on the start page the logo too. Titles and addresses never wrap, so long ones are cut off as well. The page can't scroll sideways.
279+- **The source filter stays for the next search.** If the selected sources have no results for the new query, the list is empty without the "No results found" message, and a selected source that is hidden can't be unselected.
280+- **"Come back later" doesn't help.** The no-results message says "We'll try and find you something. Just come back later.", but nothing searches again.
281+- **Searching for the same query again does nothing,** so the page can't refresh its results.
282+- **Searches can't be bookmarked,** because the page's address doesn't change.
283+- **Smaller things:** only the title of a result is a link, although the whole result looks clickable; the logo tilts only on the start page; the page has no favicon; the search box isn't focused when the page opens; and the page logs every event from the ship to the browser console.
284+
285+## History
286+
287+- **July 2023.** iko created the repository on 23 July and started %seax on 27 July, as a web crawler with a reverse word index. On 29 July it switched to searching other search engines, with Yep, CrowdView, wiby and Alexandria as sources, and from 31 July it sent each query to all of them. Streaming results through `%sink` and noun diffs, ranking and the first Elm frontend were in place by 2 August, and filtering by source and animations by 5 August.
288+- **August 2023.** iko added liveness checks between %seax ships on 9 August, and "bad peer discovery" (gossip) on 27 August.
289+- **October 2023.** Evangelos Lamprou added the logo and its tilt effect, improvements to the results page including the emoji, the Urbit developer docs as a source, and the first INSTRUCTIONS.md. He also added a `%get-notes` poke that copied the ship's Tlon notebook notes into the agent's state. The notes were never searched.
290+- **Upstream.** iko also contributed `%sink` and the noun-diff library to urbit/urbit in July 2023 ([#6712](https://github.com/urbit/urbit/pull/6712), [#6681](https://github.com/urbit/urbit/pull/6681)), where they live in `pkg/base-dev`.
291+- **2026.** %seax was ported to Urbit OS 408k:
292+ - It is built with a Nix flake, served from the ship, and has a Landscape tile.
293+ - The docs.urbit.org search replaced the old urbit.org developer search.
294+ - Alexandria, whose API is gone, and the Tlon notes were removed.
295+ - The peer network, which nothing used, was disabled.
296+ - Yep, which blocks requests that don't come from a browser, was dropped.
297+
298+## Credits
299+
300+Created by [~racfer-hattes](https://iko.soy) and [~dopnyr-figbud](https://vagos.lamprou.xyz/).
301+
302+- Results come from [wiby](https://wiby.me/), the [Urbit docs](https://docs.urbit.org/) and [CrowdView](https://crowdview.ai/).
303+- The frontend is written in [Elm](https://elm-lang.org/) with [airlock](https://github.com/iko-soy/airlock), [elm-ui](https://github.com/mdgriffith/elm-ui) and [elm-animator](https://github.com/mdgriffith/elm-animator), and includes the [Roboto](https://github.com/googlefonts/roboto-3-classic) font.
304+
305+## License
306+
307+MIT, see [LICENSE](LICENSE). Files from other projects are listed in [NOTICE](NOTICE).
dev/elm-live.sh+19−2
@@ -1,7 +1,24 @@
1−#!/bin/sh
1+#!/usr/bin/env bash
2+# Serves the frontend at http://localhost:8000 and rebuilds it on changes.
3+# Requests to /~/ are passed on to the ship at $SHIP_URL, so log in at
4+# http://localhost:8000/~/login first. Extra arguments go to `elm make`.
25
36set -e
47
58cd "$(dirname "$0")/../frontend"
69
7−nix run nixpkgs#elmPackages.elm-live -- src/Main.elm --start-page=index.html -- --debug --output=elm.js "$@"
10+ship=${SHIP_URL:-http://localhost:8080}
11+args=(src/Main.elm --start-page=index.html --proxy-prefix=/~ "--proxy-host=$ship/~" -- --debug --output=elm.js "$@")
12+
13+if command -v nix >/dev/null; then
14+ nix build ..#airlock-src --out-link airlock
15+ nix build ..#fonts --out-link assets/fonts
16+ exec nix develop .. --command elm-live "${args[@]}"
17+elif command -v elm-live >/dev/null; then
18+ if [ ! -d airlock ]; then
19+ echo "Please clone https://github.com/iko-soy/airlock into frontend/airlock and apply frontend/airlock.patch" && exit 1
20+ fi
21+ exec elm-live "${args[@]}"
22+else
23+ echo "Please install either 'nix' or 'elm-live'" && exit 1
24+fi
docs/screenshot.pngnew file

Binary file not shown.

flake.locknew file+45
@@ -0,0 +1,45 @@
1+{
2+ "nodes": {
3+ "airlock": {
4+ "flake": false,
5+ "locked": {
6+ "lastModified": 1692952961,
7+ "narHash": "sha256-xz4esBLKyzcBp0rNkne26lrhIB993gLRJqxg217d8Yk=",
8+ "owner": "iko-soy",
9+ "repo": "airlock",
10+ "rev": "7e857379f10b362a9ee9e271312a4deb1fe65b66",
11+ "type": "github"
12+ },
13+ "original": {
14+ "owner": "iko-soy",
15+ "repo": "airlock",
16+ "rev": "7e857379f10b362a9ee9e271312a4deb1fe65b66",
17+ "type": "github"
18+ }
19+ },
20+ "nixpkgs": {
21+ "locked": {
22+ "lastModified": 1792210252,
23+ "narHash": "sha256-Mh0PUaIv4nFY+8wAGonaEh8iBzwsmdNlwjVHmFg1Biw=",
24+ "owner": "NixOS",
25+ "repo": "nixpkgs",
26+ "rev": "09063bd1f2b36fd9fd793c36dfade7bd40a93dfc",
27+ "type": "github"
28+ },
29+ "original": {
30+ "owner": "NixOS",
31+ "ref": "nixos-unstable",
32+ "repo": "nixpkgs",
33+ "type": "github"
34+ }
35+ },
36+ "root": {
37+ "inputs": {
38+ "airlock": "airlock",
39+ "nixpkgs": "nixpkgs"
40+ }
41+ }
42+ },
43+ "root": "root",
44+ "version": 7
45+}
flake.nixnew file+114
@@ -0,0 +1,114 @@
1+{
2+ description = "seax: an Urbit search agent";
3+
4+ inputs = {
5+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6+ # The Elm library the frontend talks to the ship with.
7+ airlock = {
8+ url = "github:iko-soy/airlock/7e857379f10b362a9ee9e271312a4deb1fe65b66";
9+ flake = false;
10+ };
11+ };
12+
13+ outputs =
14+ {
15+ self,
16+ nixpkgs,
17+ airlock,
18+ }:
19+ let
20+ # nixpkgs 26.11 dropped x86_64-darwin.
21+ forAllSystems = nixpkgs.lib.genAttrs [
22+ "x86_64-linux"
23+ "aarch64-linux"
24+ "aarch64-darwin"
25+ ];
26+ in
27+ {
28+ packages = forAllSystems (
29+ system:
30+ let
31+ pkgs = nixpkgs.legacyPackages.${system};
32+ inherit (self.packages.${system}) airlock-src fonts frontend;
33+ in
34+ {
35+ # The %seax desk, ready to be copied into a mounted desk directory
36+ # and committed with `|commit %seax`. The agent serves the frontend
37+ # from web/ at /apps/seax/.
38+ desk = pkgs.runCommand "seax-desk" { } ''
39+ cp -r --no-preserve=mode ${./zod/seax} $out
40+ cp -r --no-preserve=mode ${frontend} $out/web
41+ '';
42+
43+ frontend = pkgs.stdenv.mkDerivation {
44+ name = "seax-frontend";
45+ src = ./frontend;
46+ nativeBuildInputs = [ pkgs.elmPackages.elm ];
47+ # elm-srcs.nix and registry.dat come from `elm2nix convert` and
48+ # `elm2nix snapshot`; regenerate them when elm.json changes.
49+ configurePhase =
50+ pkgs.elmPackages.fetchElmDeps {
51+ elmPackages = import ./frontend/elm-srcs.nix;
52+ elmVersion = "0.19.2";
53+ registryDat = ./frontend/registry.dat;
54+ }
55+ + ''
56+ # A checkout without .git can still contain dev/elm-live.sh's link.
57+ rm -rf airlock
58+ ln -s ${airlock-src} airlock
59+ '';
60+ buildPhase = ''
61+ elm make src/Main.elm --optimize --output elm.js
62+ '';
63+ installPhase = ''
64+ mkdir -p $out/assets
65+ cp index.html elm.js script.js fetch-event-source.js $out/
66+ cp -r assets/css assets/img assets/js $out/assets/
67+ cp -r ${fonts} $out/assets/fonts
68+ '';
69+ };
70+
71+ # airlock, patched to not poke ~zod when it opens a channel.
72+ airlock-src = pkgs.applyPatches {
73+ name = "airlock";
74+ src = airlock;
75+ patches = [ ./frontend/airlock.patch ];
76+ };
77+
78+ # The web font used by the frontend, served from assets/fonts/.
79+ fonts =
80+ let
81+ robotoLicense = pkgs.fetchurl {
82+ url = "https://raw.githubusercontent.com/googlefonts/roboto-3-classic/v3.016/OFL.txt";
83+ hash = "sha256-BhQCMnqWqtsL+2lKlg7SiezTjTg+OWJDgxq4H+sQnEE=";
84+ };
85+ in
86+ pkgs.runCommand "seax-fonts" { nativeBuildInputs = [ pkgs.woff2 ]; } ''
87+ mkdir -p $out
88+ cp ${pkgs.roboto.src}/web/static/Roboto-Light.ttf roboto-light.ttf
89+ woff2_compress roboto-light.ttf
90+ cp roboto-light.woff2 $out/roboto-light.woff2
91+ cp ${robotoLicense} $out/roboto-license.txt
92+ '';
93+
94+ default = self.packages.${system}.desk;
95+ }
96+ );
97+
98+ devShells = forAllSystems (
99+ system:
100+ let
101+ pkgs = nixpkgs.legacyPackages.${system};
102+ in
103+ {
104+ default = pkgs.mkShell {
105+ packages = [
106+ pkgs.elmPackages.elm
107+ pkgs.elmPackages.elm-live
108+ pkgs.elm2nix
109+ ];
110+ };
111+ }
112+ );
113+ };
114+}
frontend/airlock.patchnew file+16
@@ -0,0 +1,16 @@
1+diff --git a/src/Ur/Run.elm b/src/Ur/Run.elm
2+index 4802e86..fe67d11 100644
3+--- a/src/Ur/Run.elm
4++++ b/src/Ur/Run.elm
5+@@ -312,8 +312,10 @@ update inp msg model =
6+
7+ NeedsActivation ->
8+ let
9++ -- Any PUT creates the channel. An ack of nothing does so
10++ -- without poking ~zod (`ship = ""`) over the network.
11+ ( eventId, reqs ) =
12+- [ Poke { ship = "", agent = "hood", mark = "helm-hi", noun = C.cord "Opening airlock!" } ]
13++ [ Ack 0 ]
14+ |> tag model.eventId
15+ in
16+ ( { model | eventId = eventId }
frontend/assets/css/fonts.cssnew file+10
@@ -0,0 +1,10 @@
1+/* The font file is taken from nixpkgs at build time (`nix build .#fonts`),
2+ see flake.nix. Its license is next to it in ../fonts/. */
3+
4+@font-face {
5+ font-family: 'Roboto';
6+ font-style: normal;
7+ font-weight: 300;
8+ font-display: swap;
9+ src: url('../fonts/roboto-light.woff2') format('woff2');
10+}
frontend/assets/css/main.css+45−9
@@ -1,13 +1,6 @@
1−@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200&family=Roboto+Mono:wght@200&family=Roboto:wght@300&display=swap');
2−
3−#tilt:hover {
4− /* box-shadow: 0px 0px 30px rgba(0,0,0, 0.6); */
5− /* cursor: pointer; */
6− /* transition: all 0.1s ease-in-out; */
7−}
1+@import url('fonts.css');
82
93.button {
10− background-color: #ff7600;
114 border: none;
125 color: white;
136 padding: 15px 32px;
@@ -22,6 +15,49 @@
2215}
2316
2417.shadow {
25− box-shadow: 0 2px 2px #888888;
18+ shadow: 0 2px 2px #888888;
19+}
20+
21+.result {
22+}
23+
24+.result:hover {
25+ text-decoration: underline;
26+}
27+
28+.search:hover {
29+ text-decoration: underline;
2630}
2731
32+.wavy {
33+ animation-name: wavy;
34+ animation-duration: 4.2s;
35+ animation-timing-function: ease-in-out;
36+ animation-iteration-count: infinite;
37+ position: relative;
38+ top: 0;
39+ left: 0;
40+}
41+@keyframes wavy {
42+ 0% {
43+ top: 0px;
44+ }
45+ 50% {
46+ top: -15px;
47+ }
48+ 100% {
49+ top: 0px;
50+ }
51+}
52+
53+.result-link {
54+ color: #000;
55+ text-decoration: none;
56+ font-size: 12px !important;
57+}
58+
59+.result-title {
60+ color: #000;
61+ text-decoration: none;
62+ font-size: 16px;
63+}
frontend/assets/img/tile.pngnew file

Binary file not shown.

frontend/assets/js/tilt.js−11
@@ -43,17 +43,6 @@ el.addEventListener('mouseout', function() {
4343 el.style.transform = 'perspective(500px) scale(1) rotateX(0) rotateY(0)'
4444})
4545
46−/* Add listener for mousedown event, to simulate click */
47−el.addEventListener('mousedown', function() {
48− el.style.transform = 'perspective(500px) scale(0.9) rotateX(0) rotateY(0)'
49−})
50−
51−/* Add listener for mouseup, simulate release of mouse click */
52−el.addEventListener('mouseup', function() {
53− el.style.transform = 'perspective(500px) scale(1.1) rotateX(0) rotateY(0)'
54−})
55−
56−
5746/*
5847 * Add a listener for mousemove event
5948 * Which will trigger function 'handleMove'
frontend/elm-srcs.nixnew file+267
@@ -0,0 +1,267 @@
1+{
2+
3+ "Evelios/elm-hash" = {
4+ sha256 = "0xiypbcqd8bfv0lzpna6is5a63vf91jy8xa2350asi5y5c5fzrkq";
5+ version = "1.0.0";
6+ };
7+
8+ "TSFoster/elm-bytes-extra" = {
9+ sha256 = "1ip5br0kszhr8kmk19jsqqqhgna89drn3cwb6zn8ncwfrxc8xvs9";
10+ version = "1.3.0";
11+ };
12+
13+ "avh4/elm-color" = {
14+ sha256 = "0n16wnvp87x9az3m5qjrl6smsg7051m719xn5d244painx8xmpzq";
15+ version = "1.0.0";
16+ };
17+
18+ "cmditch/elm-bigint" = {
19+ sha256 = "00wfcvl3kg8l85cyljvgal51qksnqfqkc4g4diljggz6dcl155w2";
20+ version = "2.0.1";
21+ };
22+
23+ "elm-community/list-extra" = {
24+ sha256 = "02grd0p5hc2gvdy4n723d1s28pm1grn95jrzic6jcgb26qh16vcc";
25+ version = "8.7.0";
26+ };
27+
28+ "elm-community/maybe-extra" = {
29+ sha256 = "185jy9jxx3bqf0xl3rmdxfqqmxzcr084llf32glr6hgl5agshywk";
30+ version = "5.3.0";
31+ };
32+
33+ "elm-community/string-extra" = {
34+ sha256 = "014l3lkglaniizwvr5pqbi4z7bb0piq0pp3fdifyd4rdp53ac23f";
35+ version = "4.0.1";
36+ };
37+
38+ "elm/browser" = {
39+ sha256 = "0nagb9ajacxbbg985r4k9h0jadqpp0gp84nm94kcgbr5sf8i9x13";
40+ version = "1.0.2";
41+ };
42+
43+ "elm/bytes" = {
44+ sha256 = "02ywbf52akvxclpxwj9n04jydajcbsbcbsnjs53yjc5lwck3abwj";
45+ version = "1.0.8";
46+ };
47+
48+ "elm/core" = {
49+ sha256 = "19w0iisdd66ywjayyga4kv2p1v9rxzqjaxhckp8ni6n8i0fb2dvf";
50+ version = "1.0.5";
51+ };
52+
53+ "elm/html" = {
54+ sha256 = "1n3gpzmpqqdsldys4ipgyl1zacn0kbpc3g4v3hdpiyfjlgh8bf3k";
55+ version = "1.0.0";
56+ };
57+
58+ "elm/http" = {
59+ sha256 = "008bs76mnp48b4dw8qwjj4fyvzbxvlrl4xpa2qh1gg2kfwyw56v1";
60+ version = "2.0.0";
61+ };
62+
63+ "elm/json" = {
64+ sha256 = "0kjwrz195z84kwywaxhhlnpl3p251qlbm5iz6byd6jky2crmyqyh";
65+ version = "1.1.3";
66+ };
67+
68+ "elm/svg" = {
69+ sha256 = "1cwcj73p61q45wqwgqvrvz3aypjyy3fw732xyxdyj6s256hwkn0k";
70+ version = "1.0.1";
71+ };
72+
73+ "elm/time" = {
74+ sha256 = "0vch7i86vn0x8b850w1p69vplll1bnbkp8s383z7pinyg94cm2z1";
75+ version = "1.0.0";
76+ };
77+
78+ "elm/url" = {
79+ sha256 = "0av8x5syid40sgpl5vd7pry2rq0q4pga28b4yykn9gd9v12rs3l4";
80+ version = "1.0.0";
81+ };
82+
83+ "figbus/elm-urbit-api" = {
84+ sha256 = "1qpb8gv2giqp5rysmh0ar66j341bra6wzqka8qcr6xk82ad6afch";
85+ version = "4.0.1";
86+ };
87+
88+ "hecrj/html-parser" = {
89+ sha256 = "19rr5w9lca7akwyvq1fbi5vrib4x2b9sf1p76dxhf6vmiw71xrxr";
90+ version = "2.4.0";
91+ };
92+
93+ "ianmackenzie/elm-1d-parameter" = {
94+ sha256 = "08r1q80k7w2kyasqk54k1xzd5d1607wj4ahx66jdh7gkrni9m8n5";
95+ version = "1.0.1";
96+ };
97+
98+ "ianmackenzie/elm-3d-camera" = {
99+ sha256 = "1cp7ws6kn4sakzk0hpwy83qyz4mr6xfsk2jb4vbf97wjc1fs1gf7";
100+ version = "3.1.0";
101+ };
102+
103+ "ianmackenzie/elm-3d-scene" = {
104+ sha256 = "0h1yhxdycyayxp31g7dsj2iarhfzb6gls2rbzqydmd1v4px4rafr";
105+ version = "1.0.1";
106+ };
107+
108+ "ianmackenzie/elm-geometry" = {
109+ sha256 = "11vjnr9h1742pn9z55bclsxm5hal1f1v4dd055zd6z14fr91pysn";
110+ version = "3.11.0";
111+ };
112+
113+ "ianmackenzie/elm-triangular-mesh" = {
114+ sha256 = "00n08cc3qj610g2f7f77qpl6b6pwr456czf3prkz6iddmi52h3gp";
115+ version = "1.1.0";
116+ };
117+
118+ "ianmackenzie/elm-units" = {
119+ sha256 = "053nibhpkf906c7g14z48pvinzzfhnrnk5hkj4mjvjnv3r457h97";
120+ version = "2.10.0";
121+ };
122+
123+ "jasonliang-dev/elm-heroicons" = {
124+ sha256 = "038j32fniz3q9kpm7mcjz053fycs2nl7pnishgrrs5djjc6djw1z";
125+ version = "2.0.0";
126+ };
127+
128+ "jxxcarlson/hex" = {
129+ sha256 = "0dd0f2lfny4qwzkqbvixkcgprza2n289y46y1y2mgiay8iisfkx9";
130+ version = "4.0.0";
131+ };
132+
133+ "marcosh/elm-html-to-unicode" = {
134+ sha256 = "1f1b32qkqxl9lqc4g5737kw4yrz32jxcylgbsbmq871jwv4gwdp8";
135+ version = "1.0.4";
136+ };
137+
138+ "mdgriffith/elm-animator" = {
139+ sha256 = "0rbsycykmcjgjlm5r48ycbr9halv2big52ifhs7wpslz0df3hhmh";
140+ version = "1.1.1";
141+ };
142+
143+ "mdgriffith/elm-ui" = {
144+ sha256 = "0ffcqv4a4ad400hwp824m3qq4jy82cqp5ghmhp1m0q7n004z6kgv";
145+ version = "1.1.8";
146+ };
147+
148+ "rtfeldman/elm-css" = {
149+ sha256 = "1gwhgqwclc7clk1ns4qqzyn7b4wvcmccg9qavvb8m694qmwlkzjy";
150+ version = "18.0.0";
151+ };
152+
153+ "rtfeldman/elm-hex" = {
154+ sha256 = "1y0aa16asvwdqmgbskh5iba6psp43lkcjjw9mgzj3gsrg33lp00d";
155+ version = "1.0.0";
156+ };
157+
158+ "toastal/either" = {
159+ sha256 = "1yfgvw81szsipibafnllik65vhki9n1lxdylbyy6fsmj6406g1yv";
160+ version = "3.6.3";
161+ };
162+
163+ "zwilias/elm-html-string" = {
164+ sha256 = "0pfqbva3rawly0cr448lvajgx2lzqq6avb8wgrp67i6sh9zd06f7";
165+ version = "2.0.2";
166+ };
167+
168+ "AdrianRibao/elm-derberos-date" = {
169+ sha256 = "0w4ld89akhhr0zr17vnwcwdiwygmnqdf9nb2g2f3w1b2p3p73acp";
170+ version = "1.2.3";
171+ };
172+
173+ "NoRedInk/elm-string-conversions" = {
174+ sha256 = "0xhjyypf5jmnjih9m3rr2ccyf15i403yvpv5xmknqmhmnnhrkf63";
175+ version = "1.0.1";
176+ };
177+
178+ "bitsoflogic/elm-radixint" = {
179+ sha256 = "0625xbi9bw3xdixhids82gcx7bhmrcd9q0cxpc6rpiy8br7d2lfn";
180+ version = "2.0.0";
181+ };
182+
183+ "bonzaico/murmur3" = {
184+ sha256 = "1ccyaz8n2c9szv25y5pfdp62pdmm81n9h6yhr1r26hab9c15gcxx";
185+ version = "1.0.0";
186+ };
187+
188+ "chain-partners/elm-bignum" = {
189+ sha256 = "17fqqmm83r905zw03gjc0dif3ach4d1aar3cv7dlcn2szp0qpgfs";
190+ version = "1.0.1";
191+ };
192+
193+ "elm-explorations/linear-algebra" = {
194+ sha256 = "19zf3dgrx5bvhybjwq3bp171wzsb4bfk5rrfjv234b2f8q3yi59g";
195+ version = "1.0.3";
196+ };
197+
198+ "elm-explorations/webgl" = {
199+ sha256 = "0ma7laa3ax8zd41s838px1y5s1dsmlqzp4j321lwngxjzvxkm3zy";
200+ version = "1.1.3";
201+ };
202+
203+ "elm/file" = {
204+ sha256 = "1rljcb41dl97myidyjih2yliyzddkr2m7n74x7gg46rcw4jl0ny8";
205+ version = "1.0.5";
206+ };
207+
208+ "elm/parser" = {
209+ sha256 = "0a3cxrvbm7mwg9ykynhp7vjid58zsw03r63qxipxp3z09qks7512";
210+ version = "1.1.0";
211+ };
212+
213+ "elm/random" = {
214+ sha256 = "138n2455wdjwa657w6sjq18wx2r0k60ibpc4frhbqr50sncxrfdl";
215+ version = "1.0.0";
216+ };
217+
218+ "elm/regex" = {
219+ sha256 = "0lijsp50w7n1n57mjg6clpn9phly8vvs07h0qh2rqcs0f1jqvsa2";
220+ version = "1.0.0";
221+ };
222+
223+ "elm/virtual-dom" = {
224+ sha256 = "1yvb8px2z62xd578ag2q0r5hd1vkz9y7dfkx05355iiy1d7jwq4v";
225+ version = "1.0.3";
226+ };
227+
228+ "ianmackenzie/elm-float-extra" = {
229+ sha256 = "0l499d14iabyigdysvx3xa572izmqqki7b1ym2gc7kv4sprrq194";
230+ version = "1.1.0";
231+ };
232+
233+ "ianmackenzie/elm-geometry-linear-algebra-interop" = {
234+ sha256 = "15gjd1h0jv3d35p0hyv4k8z1gpag23dip8yacclajjrnkdxmcpgj";
235+ version = "2.0.2";
236+ };
237+
238+ "ianmackenzie/elm-interval" = {
239+ sha256 = "086x40s9gnl2r7700ff75w0p3wfrxh144ks24ni5cwhirxwwf0pq";
240+ version = "3.1.0";
241+ };
242+
243+ "ianmackenzie/elm-units-interval" = {
244+ sha256 = "0xwb5vj8jcnn88cnwniibgncfvyaxnmwwkrbk1n7ji3aj6w8f8dj";
245+ version = "3.2.0";
246+ };
247+
248+ "ktonon/elm-word" = {
249+ sha256 = "055rs4aacvhl4m25lj70wlk71f4yinwgd74wa4nx36mpj2m743b6";
250+ version = "2.1.2";
251+ };
252+
253+ "myrho/elm-round" = {
254+ sha256 = "0zv0a60wzkx4xib7h07ijcg72mcyb3vb914hk5pjp6rf4k2lv9kj";
255+ version = "1.0.5";
256+ };
257+
258+ "robinheghan/murmur3" = {
259+ sha256 = "15asmgr2zqh7rkywrg5647rpdqkpzxk02v5qc6ndj60jza3gsmjk";
260+ version = "1.0.0";
261+ };
262+
263+ "chelovek0v/bbase64" = {
264+ sha256 = "01fj6lf2xpc3byw89zivh11i6kl5bimlsdjd6m3gsi1v88nfj17d";
265+ version = "1.0.1";
266+ };
267+}
frontend/elm.json+9−2
@@ -2,11 +2,12 @@
22 "type": "application",
33 "source-directories": [
44 "src",
5− "../../airlock/src"
5+ "airlock/src"
66 ],
7− "elm-version": "0.19.1",
7+ "elm-version": "0.19.2",
88 "dependencies": {
99 "direct": {
10+ "Evelios/elm-hash": "1.0.0",
1011 "TSFoster/elm-bytes-extra": "1.3.0",
1112 "avh4/elm-color": "1.0.0",
1213 "cmditch/elm-bigint": "2.0.1",
@@ -21,7 +22,9 @@
2122 "elm/url": "1.0.0",
2223 "elm-community/list-extra": "8.7.0",
2324 "elm-community/maybe-extra": "5.3.0",
25+ "elm-community/string-extra": "4.0.1",
2426 "figbus/elm-urbit-api": "4.0.1",
27+ "hecrj/html-parser": "2.4.0",
2528 "ianmackenzie/elm-1d-parameter": "1.0.1",
2629 "ianmackenzie/elm-3d-camera": "3.1.0",
2730 "ianmackenzie/elm-3d-scene": "1.0.1",
@@ -30,6 +33,7 @@
3033 "ianmackenzie/elm-units": "2.10.0",
3134 "jasonliang-dev/elm-heroicons": "2.0.0",
3235 "jxxcarlson/hex": "4.0.0",
36+ "marcosh/elm-html-to-unicode": "1.0.4",
3337 "mdgriffith/elm-animator": "1.1.1",
3438 "mdgriffith/elm-ui": "1.1.8",
3539 "rtfeldman/elm-css": "18.0.0",
@@ -41,6 +45,8 @@
4145 "AdrianRibao/elm-derberos-date": "1.2.3",
4246 "NoRedInk/elm-string-conversions": "1.0.1",
4347 "bitsoflogic/elm-radixint": "2.0.0",
48+ "bonzaico/murmur3": "1.0.0",
49+ "chain-partners/elm-bignum": "1.0.1",
4450 "elm/file": "1.0.5",
4551 "elm/parser": "1.1.0",
4652 "elm/random": "1.0.0",
@@ -53,6 +59,7 @@
5359 "ianmackenzie/elm-interval": "3.1.0",
5460 "ianmackenzie/elm-units-interval": "3.2.0",
5561 "ktonon/elm-word": "2.1.2",
62+ "myrho/elm-round": "1.0.5",
5663 "robinheghan/murmur3": "1.0.0"
5764 }
5865 },
frontend/index.html+2−2
@@ -28,8 +28,8 @@
2828 </head>
2929 <body>
3030 <div id="elm"></div>
31− <script src="/elm.js"></script>
32− <script type="module" src="/script.js"></script>
31+ <script src="elm.js"></script>
32+ <script type="module" src="script.js"></script>
3333 </body>
3434 <script type="text/javascript" src="assets/js/tilt.js" defer></script>
3535</html>
frontend/registry.datnew file

Binary file not shown.

frontend/script.js+3−2
@@ -1,6 +1,7 @@
1−import { fetchEventSource } from "/fetch-event-source.js"
1+import { fetchEventSource } from "./fetch-event-source.js"
22
3−const uid = `${Math.floor(Date.now() / 1000)}-${Math.random()}`;
3+// No dots: Eyre would read anything after a dot as a file extension.
4+const uid = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
45
56function searchForInit(obj) {
67 if (obj.init) {
frontend/src/Main.elm+135−47
@@ -10,10 +10,20 @@ import Element.Events as Events exposing (onClick)
1010import Element.Font as Font
1111import Element.Input as Input
1212import Element.Keyed as Keyed
13+import Html exposing (Html, hr)
1314import Html.Attributes
15+import Html.Parser
16+import Html.String
17+import Html.String
18+import Html.Styled.Attributes exposing (css)
19+import Html.Styled.Attributes exposing (css)
20+import Html.Events exposing (on)
21+import Http
1422import Json.Decode as JD
23+import List
1524import List.Extra
1625import Set exposing (Set)
26+import String.Extra exposing (stripTags)
1727import Time exposing (Posix)
1828import Ur
1929import Ur.Cmd
@@ -22,15 +32,16 @@ import Ur.Jam exposing (isSig)
2232import Ur.Run
2333import Ur.Sub
2434import Ur.Types exposing (Noun(..))
25−import Html.String
26−import List
27−import Css
28−import Html.Styled.Attributes exposing (css)
29−
35+import Url
36+import ElmEscapeHtml
37+import Json.Encode
38+import Hash
3039
40+{-| The frontend is served by the ship itself, so Eyre is on the same origin.
41+-}
3142url : String
3243url =
33− "http://localhost:8080"
44+ ""
3445
3546
3647type SearchEngineState
@@ -88,11 +99,10 @@ main =
8899 , engines = []
89100 , selectedEngines = Set.empty
90101 }
91− , Cmd.batch
92− [ Ur.logIn url "lidlut-tabwed-pillex-ridrup"
93− |> Cmd.map (always Noop)
94− , Ur.getShipName url |> Cmd.map (result (always Noop) GotShipName)
95− ]
102+ , Http.get
103+ { url = url ++ "/~/host"
104+ , expect = Http.expectString (result (always Noop) GotShipName)
105+ }
96106 |> Ur.Cmd.cmd
97107 )
98108 , update = update
@@ -102,12 +112,19 @@ main =
102112 , body =
103113 [ view model
104114 |> layout
105− [ Font.family
106− [ Font.typeface "Inter"
107− , Font.typeface "Apple Color Emoji"
115+ [ Font.family
116+ [ Font.typeface "monospace"
117+ , Font.typeface "system-ui"
118+ , Font.typeface "-apple-system"
119+ , Font.typeface "BlinkMacSystemFont"
120+ , Font.typeface "Segoe UI"
121+ , Font.typeface "Roboto"
122+ , Font.typeface "Helvetica"
123+ , Font.typeface "Arial"
124+ , Font.sansSerif
108125 ]
109126 , clipX
110− ]
127+ ]
111128 ]
112129 }
113130 , subscriptions = \model -> Animator.toSubscription Tick model (animator model)
@@ -146,19 +163,59 @@ main =
146163
147164logoView : List (Attribute Msg) -> Element Msg
148165logoView attributes =
149− Element.image ( List.append [ Html.Attributes.id "tilt" |> htmlAttribute ] attributes )
150− { src = "assets/img/logo.png", description = "The logo" }
166+ Element.image (List.append [ Html.Attributes.id "tilt" |> htmlAttribute ] attributes)
167+ { src = "assets/img/logo.png", description = "%seax logo" }
168+
169+noSearchResultsView : Element Msg
170+noSearchResultsView = Element.el
171+ [ Html.Attributes.style "padding" "1em" |> htmlAttribute
172+ , width (px 500)
173+ , Font.size 15
174+ , Html.Attributes.class "wavy" |> htmlAttribute
175+ ]
176+ (Element.text """No results found... :)
177+We'll try and find you something.
178+Just come back later.""")
179+
180+subtleLinkView : String -> String -> Element Msg
181+subtleLinkView text href =
182+ Element.el
183+ [ Font.size 15
184+ , pointer
185+ , mouseOver
186+ [ Font.color (rgb 0.1 0.7 0.7)
187+ ]
188+ ]
189+ (Element.html (Html.a [ Html.Attributes.href href ] [ Html.text text ]))
190+
191+aboutView : Element Msg
192+aboutView = row [ centerX, padding 10, spacing 10]
193+ [ subtleLinkView "[About]" "https://github.com/iko-soy/seax#readme"
194+ , subtleLinkView "[Code]" "https://github.com/iko-soy/seax"
195+ ]
196+
197+titleEmoji : String -> String
198+titleEmoji title =
199+ let maxUnicode = 0x1F3F0
200+ minUnicode = 0x1F300
201+ in title |> Hash.fromString
202+ |> Hash.toString
203+ |> String.toInt
204+ |> Maybe.withDefault 0
205+ |> (\x -> ((modBy (maxUnicode - minUnicode) x) + minUnicode))
206+ |> Char.fromCode
207+ |> String.fromChar
208+ |> (\x -> x ++ " ")
209+
151210
152211view : Model -> Element Msg
153212view model =
154213 case model.initiatedSearch of
155214 Nothing ->
156215 column [ centerX, centerY, padding 8, spacing 16 ]
157− [
158− -- el [ centerX, Font.size 67, Font.heavy] (text "%seax") ,
159− -- Element.image [ centerX, width (px 510), height (px 161), Html.Attributes.id "tilt" |> htmlAttribute ] { src = "assets/img/logo.png", description = "The logo" },
160− logoView [centerX, width (px 510), height (px 161) ],
161− searchView model
216+ [ logoView [ centerX, width (px 510), height (px 161) ]
217+ , searchView model
218+ , aboutView
162219 ]
163220
164221 Just _ ->
@@ -166,6 +223,9 @@ view model =
166223 [ column [ spacing 16, width (minimum 0 fill), padding 8]
167224 [ row [] [ logoView [height (px 40)], searchView model |> el [] ]
168225 , row [ spacing 8, paddingEach { top = 16, bottom = 16, left = 0, right = 0 } ]
226+ (if List.isEmpty model.engines then
227+ []
228+ else
169229 [ text "Sources: "
170230 , model.engines
171231 |> List.filter (\( _, state ) -> state /= Failed)
@@ -205,12 +265,14 @@ view model =
205265 |> wrappedRow
206266 [ spacing 8
207267 , width fill
208−
209− -- , padding 16
210268 ]
211269 ]
270+ )
212271 ]
213− , Keyed.column [ width (minimum 0 fill), padding 8 ]
272+ , (if List.isEmpty model.searchResults then
273+ noSearchResultsView
274+ else
275+ Keyed.column [ width (minimum 0 fill), padding 8 ]
214276 (model.searchResults
215277 |> List.filter
216278 (\{ engines } ->
@@ -247,35 +309,70 @@ view model =
247309 |> Maybe.map Tuple.first
248310 |> Maybe.withDefault (Animator.init 0)
249311 )
250− (\i -> toFloat i * 47 |> Animator.at)
312+ (\i -> toFloat i * 0 |> Animator.at)
251313 |> String.fromFloat
252314 )
253315 ++ "px"
254316 )
255317 |> htmlAttribute
256− , Html.Attributes.style "position" "absolute" |> htmlAttribute
318+ , Html.Attributes.style "margin-bottom" "1em" |> htmlAttribute
319+ , Html.Attributes.style "padding" "0.5em" |> htmlAttribute
320+ , Html.Attributes.class "result" |> htmlAttribute
257321 ]
258− [ text title
259− |> el
260− [ Font.size 16
261− , width fill
322+ [ (Element.html
323+ (Html.a
324+ [ Html.Attributes.href link
325+ , Html.Attributes.class "result-title"
326+ , Html.Attributes.target "_blank"
262327 ]
328+ [ Html.text
329+ (Maybe.withDefault ""
330+ (Url.percentDecode
331+ (ElmEscapeHtml.unescape
332+ (stripTags
333+ ((titleEmoji title) ++ title)
334+ ))))
335+ ]
336+ )
337+ )
263338 , text link
264339 |> el
265− [ Font.size 12
266− , Font.color (rgb 0.7 0.7 0.7)
340+ [ Font.color (rgb 0.7 0.7 0.7)
341+ , Html.Attributes.class "result-link" |> htmlAttribute
267342 ]
268343 ]
269344 )
270345 )
271− )
346+ ))
347+ , Element.html (Html.hr [Html.Attributes.style "width" "95%"] [])
348+ , aboutView
272349 ]
273350
274351
352+onEnter : msg -> Element.Attribute msg
353+onEnter msg =
354+ Element.htmlAttribute
355+ (Html.Events.on "keyup"
356+ (JD.field "key" JD.string
357+ |> JD.andThen
358+ (\key ->
359+ if key == "Enter" then
360+ JD.succeed msg
361+
362+ else
363+ JD.fail "Not the enter key"
364+ )
365+ )
366+ )
367+
368+
275369searchView : { a | search : String } -> Element Msg
276370searchView model =
277371 row [ spacing 8 ]
278− [ Input.text [ Html.Attributes.class "shadow" |> htmlAttribute, width (px 400)]
372+ [ Input.text
373+ [ Html.Attributes.class "shadow" |> htmlAttribute, width (px 400)
374+ , onEnter (Search model.search)
375+ ]
279376 { onChange = UpdateSearch
280377 , placeholder = Nothing
281378 , text = model.search
@@ -285,24 +382,15 @@ searchView model =
285382 { label =
286383 text "search"
287384 |> el
288− [ Html.Attributes.class "button" |> htmlAttribute
289− , Border.width 4
290− , padding 4
385+ [ Html.Attributes.class "search" |> htmlAttribute
386+ , Border.width 2
387+ , padding 6
291388 , Font.size 29
292− , Font.bold
293− , Border.color (rgb 0 0 0)
294− , Background.color (rgb 0 0 0)
295− , Font.color (rgb 1 1 1)
296− , mouseOver
297− [ Font.color (rgb 0 0 0)
298− , Background.color (rgb 1 1 1)
299− ]
300389 ]
301390 , onPress = Just (Search model.search)
302391 }
303392 ]
304393
305−
306394update : Msg -> Model -> ( Model, Ur.Cmd.Cmd Msg )
307395update msg model =
308396 case msg of
zod/seax/app/seax.hoon+103−80
@@ -1,6 +1,5 @@
11/- *seax
2−/+ dbug, engines, rank, sink
3−
2+/+ dbug, engines, rank, server, sink
43|%
54+$ query cord
65+$ search-state
@@ -8,16 +7,16 @@ $:
87 number-of-subscriptions=@u
98 search-results=(map term (unit (list search-result)))
109==
11−+$ state-0
10+:: state-0 (2023), and state-1 and state-2 (only in development builds of
11+:: the 408k port), also kept peers: other ships that may run %seax. They
12+:: were pinged and told about each other, but never searched, so they are
13+:: dropped on load. state-0 also kept notes from Tlon's %diary agent.
14+::
15++$ state-3
1216$:
17+ %3
1318 search-subscriptions=(map query search-state)
14− peers=_(silt ~[~zod ~racfer-hattes])
15− alive-peers=(set @p)
1619==
17−+$ poke
18− $%
19− [%add-peers (set @p)]
20− ==
2120++ rank-results
2221 |= search-results=(map term (unit (list search-result)))
2322 %- rank
@@ -39,13 +38,41 @@ $:
3938 |= [engine-name=term engine=engine *]
4039 =/ req
4140 ^- task:iris
42− [%request [%'GET' (crip (url.engine (trip query))) ~ ~] *outbound-config:iris]
41+ [%request (request.engine (trip query)) *outbound-config:iris]
4342 [%pass ~[%search-result query engine-name] %arvo %i req]
44− ++ liveness-interval ~s30
45− ++ gossip-interval ~s30
43+:: +serve: the frontend, served from /web in this desk at /apps/seax/
44+::
45+++ serve
46+ |= [=bowl:gall url=@t]
47+ ^- simple-payload:http
48+ =/ [[ext=(unit @ta) site=(list @t)] *] (parse-request-line:server url)
49+ ?+ site not-found:gen:server
50+ [%apps %seax ~]
51+ (redirect:gen:server '/apps/seax/')
52+ ::
53+ [%apps %seax %$ ~]
54+ (serve-file bowl /index/html)
55+ ::
56+ [%apps %seax *]
57+ ?~ ext not-found:gen:server
58+ =/ pax=(list @t) (snoc t.t.site u.ext)
59+ ?. (levy pax (sane %ta)) not-found:gen:server
60+ (serve-file bowl ;;(path pax))
61+ ==
62+++ serve-file
63+ |= [=bowl:gall pax=path]
64+ ^- simple-payload:http
65+ =/ root=path /(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)
66+ =/ file=path (weld root (welp /web pax))
67+ ?. .^(? %cu file) not-found:gen:server
68+ =/ =mark ;;(mark (rear pax))
69+ =/ =dais:clay .^(dais:clay %cb (snoc root mark))
70+ =/ =tube:clay .^(tube:clay %cc (weld root /[mark]/mime))
71+ =/ =mime !<(mime (tube (vale:dais .^(* %cx file))))
72+ [[200 ['content-type' (rsh [3 1] (spat p.mime))]~] `q.mime]
4673--
4774
48−=/ state *state-0
75+=/ state *state-3
4976=* current-engines
5077 |= =query
5178 ^- (list [term ?(%loading %failed %completed)])
@@ -75,32 +102,59 @@ $:
75102%- agent:dbug
76103|_ =bowl:gall
77104+* this .
78− liveness-behn-card [%pass /check-liveness %arvo %b %wait (add now.bowl liveness-interval)]
79− gossip-behn-card [%pass /gossip %arvo %b %wait (add now.bowl gossip-interval)]
105+ eyre-connect-card [%pass /eyre/connect %arvo %e %connect [~ /apps/seax] dap.bowl]
80106++ on-init
81− [~[liveness-behn-card gossip-behn-card] this]
107+ [~[eyre-connect-card] this]
82108++ on-save !>(state)
83−++ on-load
109+++ on-load
84110 |= =vase
85− =/ state !<(state-0 vase)
86− :- ~
87− %= this
88− state state
89− ==
90−++ on-poke
91− |= [* * pk=*]
92111 ^- (quip card:agent:gall _this)
93− =/ poke (poke pk)
94− ~& poke
95− ?- -.poke
96− %add-peers
97− [~ this(peers.state (~(put in (~(uni in peers.state) +.poke)) src.bowl))]
98− ==
99−
100−++ on-watch
101− |= =path
112+ ?: ?=([%3 *] q.vase)
113+ [~ this(state !<(state-3 vase))]
114+ :: an older state. its searches are dropped, but gall still has their
115+ :: subscribers, so kick them, or they would leave a new search for the
116+ :: same query. a state-0 agent stayed suspended until this upgrade, so
117+ :: its searches are stale anyway
118+ ::
119+ =/ stale=(list path)
120+ %~ tap in
121+ %- silt
122+ %+ murn ~(val by sup.bowl)
123+ |= [ship =path]
124+ ?.(?=([%search *] path) ~ `path)
125+ =/ kick=(list card:agent:gall)
126+ ?~(stale ~ [%give %kick stale ~]~)
127+ :: call off the pings to peers that are still waiting for an answer
128+ ::
129+ =/ leaves=(list card:agent:gall)
130+ %+ murn ~(tap by wex.bowl)
131+ |= [[=wire =ship =term] *]
132+ ^- (unit card:agent:gall)
133+ ?. ?=([%alive ~] wire) ~
134+ `[%pass wire %agent [ship term] %leave ~]
135+ :_ this(state [%3 ~])
136+ [eyre-connect-card (weld kick leaves)]
137+++ on-poke
138+ |= [=mark =vase]
139+ ^- (quip card:agent:gall _this)
140+ :: only eyre pokes us. older versions also took peers from other ships
141+ ::
142+ ?> ?=(%handle-http-request mark)
143+ =/ [eyre-id=@ta =inbound-request:eyre] !<([@ta inbound-request:eyre] vase)
144+ :_ this
145+ %+ give-simple-payload:app:server eyre-id
146+ %+ require-authorization:app:server inbound-request
147+ |= =inbound-request:eyre
148+ (serve bowl url.request.inbound-request)
149+++ on-watch
150+ |= =path
102151 ?+ path !!
152+ [%http-response *]
153+ `this
103154 [%search query ~]
155+ :: searching makes requests to every engine, so only we can do it
156+ ::
157+ ?> =(our src):bowl
104158 =/ query +<.path
105159 =/ subscription=(unit search-state)
106160 (~(get by search-subscriptions.state) query)
@@ -115,49 +169,38 @@ $:
115169 =. search-subscriptions.state
116170 (~(put by search-subscriptions.state) query subscription)
117171 [~[flush:(sink query)] this]
118− [%ping ~]
119− ~& path
120− :-
121− :~
122− [%give %fact ~ %noun !>(%pong)]
123− [%give %kick ~ ~]
124− ==
125− this
126−
127172 ==
128173
129−:: This was not tested because it requires waiting 12 hours.
130174++ on-leave
131175 |= =path
132176 ?+ path `this
133177 [%search query ~]
134178 =/ query +<.path
135179 ~& path
136− =/ search-state (~(got by search-subscriptions.state) query)
137− =/ number-of-subscriptions (dec number-of-subscriptions.search-state)
180+ =/ search-state (~(get by search-subscriptions.state) query)
181+ :: searches from before an upgrade are dropped in +on-load
182+ ::
183+ ?~ search-state `this
184+ =/ number-of-subscriptions (dec number-of-subscriptions.u.search-state)
138185 =. search-subscriptions.state
139− ?~ number-of-subscriptions
186+ ?~ number-of-subscriptions
140187 (~(del by search-subscriptions.state) query)
141− (~(put by search-subscriptions.state) query search-state(number-of-subscriptions number-of-subscriptions))
188+ (~(put by search-subscriptions.state) query u.search-state(number-of-subscriptions number-of-subscriptions))
142189 `this
143190 ==
144191
145−++ on-peek
192+++ on-peek
146193 |= path
147194 ~
148−++ on-agent
195+:: older versions poked their peers with peer lists, and the acks to
196+:: those pokes can still arrive after an upgrade
197+::
198+++ on-agent
149199 |= [=wire =sign:agent:gall]
150− ?+ wire `this
151− [%alive ~]
152− ?+ -.sign `this
153− %fact
154− ~& "{<src.bowl>} is alive"
155− =. alive-peers.state (~(put in alive-peers.state) src.bowl)
200+ ^- (quip card:agent:gall _this)
156201 `this
157− ==
158− ==
159202++ on-arvo
160− |= [=wire =sign-arvo]
203+ |= [=wire =sign-arvo]
161204 ^- (quip card:agent:gall _this)
162205 ~& wire
163206 ?+ wire `this
@@ -175,32 +218,12 @@ $:
175218 =. search-results.search-subscription
176219 (~(put by search-results.search-subscription) engine-name results)
177220 =. search-subscriptions.state
178− (~(put by search-subscriptions.state) query search-subscription)
221+ (~(put by search-subscriptions.state) query search-subscription)
179222 =^ card sink (sync:sink (sink-for-query query))
180223 ~& (current-engines query)
181224 [~[card] this]
182− [%check-liveness ~]
183− =/ checks
184− %+ turn ~(tap in peers.state)
185− |= peer=@p
186− [%pass /alive %agent [peer %seax] %watch /ping]
187− =. peers.state alive-peers.state
188− =. alive-peers.state *(set @p)
189− =/ timer
190− liveness-behn-card
191− :-
192− [timer checks]
193− this
194− [%gossip ~]
195− :-
196− :- gossip-behn-card
197− %+ turn ~(tap in peers.state)
198− |= peer=@p
199− [%pass / %agent [peer %seax] %poke %noun !>([%add-peers peers.state])]
200− this
201225 ==
202226++ on-fail
203− |= [term =tang]
227+ |= [term =tang]
204228 ((slog tang) `this)
205229--
206−
zod/seax/desk.docket-0new file+10
@@ -0,0 +1,10 @@
1+:~
2+ title+'%seax'
3+ info+'The Urbit meta-search engine. Sends your query to several search engines from your ship and merges their results.'
4+ color+0xff.ffff
5+ image+'/apps/seax/assets/img/tile.png'
6+ site+/apps/seax
7+ version+[0 1 0]
8+ website+'https://github.com/iko-soy/seax'
9+ license+'MIT'
10+==
zod/seax/lib/dbug.hoon+9−3
@@ -4,7 +4,8 @@
44::
55|%
66+$ poke
7− $% [%bowl ~]
7+ $% [%skip depth=@ud =poke]
8+ [%bowl ~]
89 [%state grab=cord]
910 [%incoming =about]
1011 [%outgoing =about]
@@ -32,8 +33,13 @@
3233 ?. ?=(%dbug mark)
3334 =^ cards agent (on-poke:ag mark vase)
3435 [cards this]
35− =/ dbug
36− !<(poke vase)
36+ =/ dbug !<(poke vase)
37+ =? dbug ?=([%skip %0 *] dbug) poke.dbug
38+ ?: ?=(%skip -.dbug)
39+ =^ cards agent
40+ %+ on-poke:ag %dbug
41+ !>(`poke`dbug(depth (dec depth.dbug)))
42+ [cards this]
3743 =; =tang
3844 ((%*(. slog pri 1) tang) [~ this])
3945 ?- -.dbug
zod/seax/lib/docket.hoonnew file+223
@@ -0,0 +1,223 @@
1+/- *docket
2+|%
3+::
4+++ mime
5+ |%
6+ +$ draft
7+ $: title=(unit @t)
8+ info=(unit @t)
9+ color=(unit @ux)
10+ glob-http=(unit [=url hash=@uvH])
11+ glob-ames=(unit [=ship hash=@uvH])
12+ base=(unit term)
13+ site=(unit path)
14+ image=(unit url)
15+ version=(unit version)
16+ website=(unit url)
17+ license=(unit cord)
18+ ==
19+ ::
20+ ++ finalize
21+ |= =draft
22+ ^- (unit docket)
23+ ?~ title.draft ~
24+ ?~ info.draft ~
25+ ?~ color.draft ~
26+ ?~ version.draft ~
27+ ?~ website.draft ~
28+ ?~ license.draft ~
29+ =/ href=(unit href)
30+ ?^ site.draft `[%site u.site.draft]
31+ ?~ base.draft ~
32+ ?^ glob-http.draft
33+ `[%glob u.base hash.u.glob-http %http url.u.glob-http]:draft
34+ ?~ glob-ames.draft
35+ ~
36+ `[%glob u.base hash.u.glob-ames %ames ship.u.glob-ames]:draft
37+ ?~ href ~
38+ =, draft
39+ :- ~
40+ :* %1
41+ u.title
42+ u.info
43+ u.color
44+ u.href
45+ image
46+ u.version
47+ u.website
48+ u.license
49+ ==
50+ ::
51+ ++ from-clauses
52+ =| =draft
53+ |= cls=(list clause)
54+ ^- (unit docket)
55+ =* loop $
56+ ?~ cls (finalize draft)
57+ =* clause i.cls
58+ =. draft
59+ ?- -.clause
60+ %title draft(title `title.clause)
61+ %info draft(info `info.clause)
62+ %color draft(color `color.clause)
63+ %glob-http draft(glob-http `[url hash]:clause)
64+ %glob-ames draft(glob-ames `[ship hash]:clause)
65+ %base draft(base `base.clause)
66+ %site draft(site `path.clause)
67+ %image draft(image `url.clause)
68+ %version draft(version `version.clause)
69+ %website draft(website `website.clause)
70+ %license draft(license `license.clause)
71+ ==
72+ loop(cls t.cls)
73+ ::
74+ ++ to-clauses
75+ |= d=docket
76+ ^- (list clause)
77+ %- zing
78+ :~ :~ title+title.d
79+ info+info.d
80+ color+color.d
81+ version+version.d
82+ website+website.d
83+ license+license.d
84+ ==
85+ ?~ image.d ~ ~[image+u.image.d]
86+ ?: ?=(%site -.href.d) ~[site+path.href.d]
87+ =/ ref=glob-reference glob-reference.href.d
88+ :~ base+base.href.d
89+ ?- -.location.ref
90+ %http [%glob-http url.location.ref hash.ref]
91+ %ames [%glob-ames ship.location.ref hash.ref]
92+ == == ==
93+ ::
94+ ++ spit-clause
95+ |= =clause
96+ ^- tape
97+ %+ weld " {(trip -.clause)}+"
98+ ?+ -.clause "'{(trip +.clause)}'"
99+ %color (scow %ux color.clause)
100+ %site (spud path.clause)
101+ ::
102+ %glob-http
103+ "['{(trip url.clause)}' {(scow %uv hash.clause)}]"
104+ ::
105+ %glob-ames
106+ "[{(scow %p ship.clause)} {(scow %uv hash.clause)}]"
107+ ::
108+ %version
109+ =, version.clause
110+ "[{(scow %ud major)} {(scow %ud minor)} {(scow %ud patch)}]"
111+ ==
112+ ::
113+ ++ spit-docket
114+ |= dock=docket
115+ ^- tape
116+ ;: welp
117+ ":~\0a"
118+ `tape`(zing (join "\0a" (turn (to-clauses dock) spit-clause)))
119+ "\0a=="
120+ ==
121+ --
122+::
123+++ enjs
124+ =, enjs:format
125+ |%
126+ ::
127+ ++ charge-update
128+ |= u=^charge-update
129+ ^- json
130+ %+ frond -.u
131+ ^- json
132+ ?- -.u
133+ %del-charge s+desk.u
134+ ::
135+ %initial
136+ %- pairs
137+ %+ turn ~(tap by initial.u)
138+ |=([=desk c=^charge] [desk (charge c)])
139+ ::
140+ %add-charge
141+ %- pairs
142+ :~ desk+s+desk.u
143+ charge+(charge charge.u)
144+ ==
145+ ==
146+ ::
147+ ++ num
148+ |= a=@u
149+ ^- ^tape
150+ =/ p=json (numb a)
151+ ?> ?=(%n -.p)
152+ (trip p.p)
153+ ::
154+ ++ version
155+ |= v=^version
156+ ^- json
157+ :- %s
158+ %- crip
159+ "{(num major.v)}.{(num minor.v)}.{(num patch.v)}"
160+ ::
161+ ++ merge
162+ |= [a=json b=json]
163+ ^- json
164+ ?> &(?=(%o -.a) ?=(%o -.b))
165+ [%o (~(uni by p.a) p.b)]
166+ ::
167+ ++ href
168+ |= h=^href
169+ %+ frond -.h
170+ ?- -.h
171+ %site s+(spat path.h)
172+ %glob
173+ %- pairs
174+ :~ base+s+base.h
175+ glob-reference+(glob-reference glob-reference.h)
176+ ==
177+ ==
178+ ::
179+ ++ glob-reference
180+ |= ref=^glob-reference
181+ %- pairs
182+ :~ hash+s+(scot %uv hash.ref)
183+ location+(glob-location location.ref)
184+ ==
185+ ::
186+ ++ glob-location
187+ |= loc=^glob-location
188+ ^- json
189+ %+ frond -.loc
190+ ?- -.loc
191+ %http s+url.loc
192+ %ames s+(scot %p ship.loc)
193+ ==
194+ ::
195+ ++ charge
196+ |= c=^charge
197+ %+ merge (docket docket.c)
198+ %- pairs
199+ :~ chad+(chad chad.c)
200+ ==
201+ ::
202+ ++ docket
203+ |= d=^docket
204+ ^- json
205+ %- pairs
206+ :~ title+s+title.d
207+ info+s+info.d
208+ color+s+(scot %ux color.d)
209+ href+(href href.d)
210+ image+?~(image.d ~ s+u.image.d)
211+ version+(version version.d)
212+ license+s+license.d
213+ website+s+website.d
214+ ==
215+ ::
216+ ++ chad
217+ |= c=^chad
218+ %+ frond -.c
219+ ?+ -.c ~
220+ %hung s+err.c
221+ ==
222+ --
223+--
zod/seax/lib/engines.hoon+2−3
@@ -1,14 +1,13 @@
11/- *seax
2−/+ engines-yep, engines-crowdview, engines-wiby, engines-alexandria
2+/+ engines-crowdview, engines-wiby, engines-urbit-dev
33
44^- engines
55
66%- malt
77%- limo
88:~
9− [name=%yep engine=engines-yep weight=.1]
109 [name=%crowdview engine=engines-crowdview weight=.2]
1110 [name=%wiby engine=engines-wiby weight=.2]
12− [name=%alexandria engine=engines-alexandria weight=.2]
11+ [name=%urbit-dev engine=engines-urbit-dev weight=.2]
1312==
1413
zod/seax/lib/engines/alexandria.hoondeleted−32
@@ -1,32 +0,0 @@
1−/- *seax
2−
3−^- engine
4−=<
5−|%
6−++ url
7− |= query=tape
8− ^- tape
9− %+ weld
10− "https://api.alexandria.org/?a=1&q="
11− (en-urlt:html query)
12−++ results
13− |= response=mime-data:iris
14− ^- (unit (list search-result))
15− =/ body q.data.response
16− ;< jon=json _biff (de:json:html body)
17− (apex jon)
18−--
19−::
20−|%
21−++ apex
22− =, dejs-soft:format
23− %- ot
24− ~[results+(ar parse-search-result)]
25−++ parse-search-result
26− =, dejs-soft:format
27− %- ot
28− ~[title+so url+so]
29−--
30−
31−
32−
zod/seax/lib/engines/crowdview.hoon+3−5
@@ -3,12 +3,10 @@
33^- engine
44=<
55|%
6−++ url
6+++ request
77 |= query=tape
8− ^- tape
9− %+ weld
10− "https://crowdview-next-js.onrender.com/api/search-v3?query="
11− (en-urlt:html query)
8+ ^- request:http
9+ (get "https://crowdview-next-js.onrender.com/api/search-v3?query=" query)
1210++ results
1311 |= response=mime-data:iris
1412 ^- (unit (list search-result))
zod/seax/lib/engines/urbit-dev.hoonnew file+36
@@ -0,0 +1,36 @@
1+/- *seax
2+
3+^- engine
4+=<
5+|%
6+++ request
7+ |= query=tape
8+ ^- request:http
9+ :^ %'POST' 'https://docs.urbit.org/~gitbook/search'
10+ ~[['content-type' 'application/json']]
11+ %- some
12+ %- as-octs:mimes:html
13+ %- en:json:html
14+ %- pairs:enjs:format
15+ :~ query+s+(crip query)
16+ scope+(pairs:enjs:format ~[mode+s+'all'])
17+ ==
18+++ results
19+ |= response=mime-data:iris
20+ ^- (unit (list search-result))
21+ =/ body q.data.response
22+ ;< jon=json _biff (de:json:html body)
23+ (apex jon)
24+--
25+::
26+|%
27+++ apex
28+ =, dejs-soft:format
29+ (ar parse-search-result)
30+++ parse-search-result
31+ |= jon=json
32+ ^- (unit search-result)
33+ =, dejs-soft:format
34+ ;< [title=@t href=@t] _biff ((ot ~[title+so href+so]) jon)
35+ `[title (cat 3 'https://docs.urbit.org' href)]
36+--
zod/seax/lib/engines/wiby.hoon+3−5
@@ -3,12 +3,10 @@
33^- engine
44=<
55|%
6−++ url
6+++ request
77 |= query=tape
8− ^- tape
9− %+ weld
10− "https://wiby.me/json/?q="
11− (en-urlt:html query)
8+ ^- request:http
9+ (get "https://wiby.me/json/?q=" query)
1210++ results
1311 |= response=mime-data:iris
1412 ^- (unit (list search-result))
zod/seax/lib/engines/yep.hoondeleted−30
@@ -1,30 +0,0 @@
1−/- *seax
2−
3−^- engine
4−=<
5−|%
6−++ url
7− |= query=tape
8− ^- tape
9− %+ weld
10− "https://api.yep.com/fs/1/?type=web&no_correct=false&limit=100&q="
11− (en-urlt:html query)
12−++ results
13− |= response=mime-data:iris
14− =/ body q.data.response
15− ;< jon=json _biff (de:json:html body)
16− (apex jon)
17−--
18−::
19−|%
20−++ apex
21− |= jon=json
22− ^- (unit (list search-result))
23− =, dejs-soft:format
24− ;< [* results=(list search-result)] _biff %- (at ~[so (ot ~[results+(ar parse-search-result)])]) jon
25− `results
26−++ parse-search-result
27− =, dejs-soft:format
28− %- ot
29− ~[title+so url+so]
30−--
zod/seax/lib/server.hoonnew file+159
@@ -0,0 +1,159 @@
1+=, eyre
2+|%
3++$ request-line
4+ $: [ext=(unit @ta) site=(list @t)]
5+ args=(list [key=@t value=@t])
6+ ==
7+:: +parse-request-line: take a cord and parse out a url
8+::
9+++ parse-request-line
10+ |= url=@t
11+ ^- request-line
12+ (fall (rush url ;~(plug apat:de-purl:html yque:de-purl:html)) [[~ ~] ~])
13+::
14+++ manx-to-octs
15+ |= man=manx
16+ ^- octs
17+ (as-octt:mimes:html (en-xml:html man))
18+::
19+++ json-to-octs
20+ |= jon=json
21+ ^- octs
22+ (as-octs:mimes:html (en:json:html jon))
23+::
24+++ app
25+ |%
26+ ::
27+ :: +require-authorization:
28+ :: redirect to the login page when unauthenticated
29+ :: otherwise call handler on inbound request
30+ ::
31+ ++ require-authorization
32+ |= $: =inbound-request:eyre
33+ handler=$-(inbound-request:eyre simple-payload:http)
34+ ==
35+ ^- simple-payload:http
36+ ::
37+ ?: authenticated.inbound-request
38+ ~! this
39+ ~! +:*handler
40+ (handler inbound-request)
41+ ::
42+ =- [[307 ['location' -]~] ~]
43+ %^ cat 3
44+ '/~/login?redirect='
45+ url.request.inbound-request
46+ ::
47+ :: +require-authorization-simple:
48+ :: redirect to the login page when unauthenticated
49+ :: otherwise pass through simple-paylod
50+ ::
51+ ++ require-authorization-simple
52+ |= [=inbound-request:eyre =simple-payload:http]
53+ ^- simple-payload:http
54+ ::
55+ ?: authenticated.inbound-request
56+ ~! this
57+ simple-payload
58+ ::
59+ =- [[307 ['location' -]~] ~]
60+ %^ cat 3
61+ '/~/login?redirect='
62+ url.request.inbound-request
63+ ::
64+ ++ give-simple-payload
65+ |= [eyre-id=@ta =simple-payload:http]
66+ ^- (list card:agent:gall)
67+ =/ header-cage
68+ [%http-response-header !>(response-header.simple-payload)]
69+ =/ data-cage
70+ [%http-response-data !>(data.simple-payload)]
71+ :~ [%give %fact ~[/http-response/[eyre-id]] header-cage]
72+ [%give %fact ~[/http-response/[eyre-id]] data-cage]
73+ [%give %kick ~[/http-response/[eyre-id]] ~]
74+ ==
75+ --
76+++ gen
77+ |%
78+ ::
79+ ++ max-1-da ['cache-control' 'max-age=86400']
80+ ++ max-1-wk ['cache-control' 'max-age=604800']
81+ ::
82+ ++ html-response
83+ =| cache=?
84+ |= =octs
85+ ^- simple-payload:http
86+ :_ `octs
87+ [200 [['content-type' 'text/html'] ?:(cache [max-1-wk ~] ~)]]
88+ ::
89+ ++ css-response
90+ =| cache=?
91+ |= =octs
92+ ^- simple-payload:http
93+ :_ `octs
94+ [200 [['content-type' 'text/css'] ?:(cache [max-1-wk ~] ~)]]
95+ ::
96+ ++ js-response
97+ =| cache=?
98+ |= =octs
99+ ^- simple-payload:http
100+ :_ `octs
101+ [200 [['content-type' 'text/javascript'] ?:(cache [max-1-wk ~] ~)]]
102+ ::
103+ ++ png-response
104+ =| cache=?
105+ |= =octs
106+ ^- simple-payload:http
107+ :_ `octs
108+ [200 [['content-type' 'image/png'] ?:(cache [max-1-wk ~] ~)]]
109+ ::
110+ ++ svg-response
111+ =| cache=?
112+ |= =octs
113+ ^- simple-payload:http
114+ :_ `octs
115+ [200 [['content-type' 'image/svg+xml'] ?:(cache [max-1-wk ~] ~)]]
116+ ::
117+ ++ ico-response
118+ |= =octs
119+ ^- simple-payload:http
120+ [[200 [['content-type' 'image/x-icon'] max-1-wk ~]] `octs]
121+ ::
122+ ++ woff2-response
123+ =| cache=?
124+ |= =octs
125+ ^- simple-payload:http
126+ [[200 [['content-type' 'font/woff2'] max-1-wk ~]] `octs]
127+ ::
128+ ++ json-response
129+ =| cache=_|
130+ |= =json
131+ ^- simple-payload:http
132+ :_ `(json-to-octs json)
133+ [200 [['content-type' 'application/json'] ?:(cache [max-1-da ~] ~)]]
134+ ::
135+ ++ manx-response
136+ =| cache=_|
137+ |= man=manx
138+ ^- simple-payload:http
139+ :_ `(manx-to-octs man)
140+ [200 [['content-type' 'text/html'] ?:(cache [max-1-da ~] ~)]]
141+ ::
142+ ++ not-found
143+ ^- simple-payload:http
144+ [[404 ~] ~]
145+ ::
146+ ++ login-redirect
147+ |= =request:http
148+ ^- simple-payload:http
149+ =- [[307 ['location' -]~] ~]
150+ %^ cat 3
151+ '/~/login?redirect='
152+ url.request
153+ ::
154+ ++ redirect
155+ |= redirect=cord
156+ ^- simple-payload:http
157+ [[307 ['location' redirect]~] ~]
158+ --
159+--
zod/seax/mar/css.hoonnew file+25
@@ -0,0 +1,25 @@
1+::
2+:::: /hoon/css/mar
3+ ::
4+/? 310
5+=, eyre
6+=, mimes:html
7+|_ mud=@t
8+++ grow :: convert to
9+ |%
10+ ++ mime [/text/css (as-octs mud)] :: convert to %mime
11+ ++ hymn :: convert to %hymn
12+ |^ html
13+ ++ style ;style
14+ ;- (trip mud)
15+ ==
16+ ++ html ;html:(head:"{style}" body)
17+ --
18+ --
19+++ grab
20+ |% :: convert from
21+ ++ mime |=([p=mite q=octs] (@t q.q))
22+ ++ noun @t :: clam from %noun
23+ --
24+++ grad %mime
25+--
zod/seax/mar/docket-0.hoonnew file+25
@@ -0,0 +1,25 @@
1+/+ dock=docket
2+|_ =docket:dock
3+++ grow
4+ |%
5+ ++ mime
6+ ^- ^mime
7+ [/text/x-docket (as-octt:mimes:html (spit-docket:mime:dock docket))]
8+ ++ noun docket
9+ ++ json (docket:enjs:dock docket)
10+ --
11+++ grab
12+ |%
13+ ::
14+ ++ mime
15+ |= [=mite len=@ud tex=@]
16+ ^- docket:dock
17+ %- need
18+ %- from-clauses:mime:dock
19+ !<((list clause:dock) (slap !>(~) (ream tex)))
20+
21+ ::
22+ ++ noun docket:dock
23+ --
24+++ grad %noun
25+--
zod/seax/mar/hoon.hoon+285−31
@@ -1,41 +1,295 @@
1−:::: /hoon/hoon/mar
2− ::
3−/? 310
4−::
5−=, eyre
61|_ own=@t
72::
83++ grow :: convert to
94 |%
105 ++ mime `^mime`[/text/x-hoon (as-octs:mimes:html own)] :: convert to %mime
11− ++ elem :: convert to %html
12− ;div:pre(urb_codemirror "", mode "hoon"):"{(trip own)}"
13− :: =+ gen-id="src-{<`@ui`(mug own)>}"
14− :: ;div
15− :: ;textarea(id "{gen-id}"):"{(trip own)}"
16− :: ;script:"""
17− :: CodeMirror.fromTextArea(
18− :: window[{<gen-id>}],
19− :: \{lineNumbers:true, readOnly:true}
20− :: )
21− :: """
22− :: ==
236 ++ hymn
24− :: ;html:(head:title:"Source" "+{elem}")
25− ;html
26− ;head
27− ;title:"Source"
28− ;script@"//cdnjs.cloudflare.com/ajax/libs/codemirror/4.3.0/codemirror.js";
29− ;script@"/lib/syntax/hoon.js";
30− ;link(rel "stylesheet", href "//cdnjs.cloudflare.com/ajax/libs/".
31− "codemirror/4.3.0/codemirror.min.css");
32− ;link/"/lib/syntax/codemirror.css"(rel "stylesheet");
33− ==
34− ;body
35− ;textarea#src:"{(trip own)}"
36− ;script:'CodeMirror.fromTextArea(src, {lineNumbers:true, readOnly:true})'
7+ ^- manx
8+ =/ to-wall
9+ |= =tape
10+ ^- wall
11+ %+ roll (flop tape)
12+ |= [char=@tD =wall]
13+ ?~ wall
14+ [[char ~] ~]
15+ ?: =('\0a' char)
16+ [~ wall]
17+ [[char i.wall] t.wall]
18+ ::
19+ =/ src=tape (trip own)
20+ =/ count=@ud (lent (to-wall src))
21+ =; [style=tape script=tape]
22+ ;html
23+ ;head
24+ ;title: source
25+ ;meta(charset "utf-8");
26+ ;meta(name "viewport", content "width=device-width, initial-scale=1");
27+ ;style: {style}
28+ ==
29+ ;body
30+ ;div.viewer
31+ :: ;div.viewer-bar
32+ :: ;span.viewer-name: {(scow %ud count)} lines
33+ :: ;button.copy-link(id "copy-link", hidden ""): copy link
34+ :: ==
35+ ;div.code
36+ ;pre.gutter(id "gutter");
37+ ;div.code-body
38+ ;div(id "hl");
39+ ;pre.code-pre(id "code"): {src}
40+ ==
41+ ==
42+ ==
43+ ;script: {script}
44+ ==
3745 ==
38− ==
46+ ::
47+ :-
48+ ^~ ^= style
49+ ^- tape
50+ %- trip
51+ '''
52+ :root {
53+ --bg: #fbfbf9;
54+ --panel: #ffffff;
55+ --gutter-bg: #f3f3ef;
56+ --line: #e4e4dd;
57+ --fg: #2b2b2b;
58+ --fg-dim: #9a9a90;
59+ --sel: #fff7d6;
60+ --sel-edge: #f1e4a8;
61+ --accent: #4a6da7;
62+ --lh: 20px;
63+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
64+ "Liberation Mono", "Courier New", monospace;
65+ }
66+ * { box-sizing: border-box; }
67+ html, body {
68+ margin: 0;
69+ height: 100%;
70+ background: var(--bg);
71+ color: var(--fg);
72+ font-family: var(--mono);
73+ -webkit-font-smoothing: antialiased;
74+ }
75+ .viewer {
76+ height: 100vh;
77+ display: flex;
78+ flex-direction: column;
79+ padding: 16px;
80+ gap: 12px;
81+ }
82+ .viewer-bar {
83+ flex: 0 0 auto;
84+ display: flex;
85+ align-items: center;
86+ gap: 12px;
87+ font-size: 13px;
88+ color: var(--fg-dim);
89+ }
90+ .viewer-name {
91+ flex: 1 1 auto;
92+ min-width: 0;
93+ overflow: hidden;
94+ text-overflow: ellipsis;
95+ white-space: nowrap;
96+ color: var(--fg);
97+ }
98+ .copy-link {
99+ flex: 0 0 auto;
100+ padding: 4px 12px;
101+ border: 1px solid var(--line);
102+ border-radius: 6px;
103+ background: var(--panel);
104+ color: var(--fg-dim);
105+ font: inherit;
106+ font-size: 12px;
107+ cursor: pointer;
108+ transition: color .12s, border-color .12s;
109+ }
110+ .copy-link:hover { color: var(--accent); border-color: var(--accent); }
111+ .copy-link[hidden] { display: none; }
112+ .code {
113+ flex: 1 1 auto;
114+ min-height: 0;
115+ display: flex;
116+ align-items: flex-start;
117+ overflow: auto;
118+ background: var(--panel);
119+ border: 1px solid var(--line);
120+ border-radius: 8px;
121+ }
122+ .code-body {
123+ position: relative;
124+ flex: 1 1 auto;
125+ min-width: 0;
126+ width: max-content;
127+ }
128+ #gutter, #code {
129+ margin: 0;
130+ padding: 14px 0;
131+ font-family: var(--mono);
132+ font-size: 13px;
133+ line-height: var(--lh);
134+ white-space: pre;
135+ tab-size: 2;
136+ }
137+ #gutter {
138+ position: sticky;
139+ left: 0;
140+ z-index: 2;
141+ flex: 0 0 auto;
142+ padding-left: 16px;
143+ padding-right: 12px;
144+ text-align: right;
145+ color: var(--fg-dim);
146+ background: var(--gutter-bg);
147+ border-right: 1px solid var(--line);
148+ user-select: none;
149+ -webkit-user-select: none;
150+ cursor: pointer;
151+ }
152+ #code {
153+ display: block;
154+ padding-left: 14px;
155+ padding-right: 18px;
156+ color: var(--fg);
157+ }
158+ #hl {
159+ position: absolute;
160+ left: 0;
161+ right: 0;
162+ z-index: 0; /* under the text */
163+ display: none;
164+ background: var(--sel);
165+ box-shadow: inset 2px 0 0 var(--sel-edge);
166+ pointer-events: none;
167+ }
168+ #code { position: relative; z-index: 1; } /* text above the bar */
169+ :focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
170+ '''
171+ ^~ ^= code
172+ ^- tape
173+ %- trip
174+ '''
175+ (function () {
176+ "use strict";
177+ const panel = document.querySelector(".code"); // the scroll container
178+ const code = document.getElementById("code");
179+ const gutter = document.getElementById("gutter");
180+ const hl = document.getElementById("hl");
181+ if (!panel || !code || !gutter || !hl) return;
182+ const text = code.textContent;
183+ let lineCount = text.length ? text.split("\n").length : 1;
184+ if (text.endsWith("\n")) lineCount -= 1;
185+ if (lineCount < 1) lineCount = 1;
186+ (function fillGutter() {
187+ let s = "";
188+ for (let i = 1; i <= lineCount; i++) s += i + "\n";
189+ gutter.textContent = s;
190+ })();
191+ let lineH = 0;
192+ let codeTop = 0; // code's content-box top padding, the y-origin of line 1
193+ function measure() {
194+ const cs = getComputedStyle(code);
195+ const lh = parseFloat(cs.lineHeight);
196+ lineH = isNaN(lh) ? parseFloat(cs.fontSize) * 1.55 : lh;
197+ codeTop = parseFloat(cs.paddingTop) || 0;
198+ positionHighlight(); // keep the bar correct after a re-measure
199+ }
200+ let anchor = null;
201+ let selLo = 0;
202+ let selHi = 0;
203+ function positionHighlight() {
204+ if (!selLo) { hl.style.display = "none"; return; }
205+ hl.style.display = "block";
206+ hl.style.top = (codeTop + (selLo - 1) * lineH) + "px";
207+ hl.style.height = ((selHi - selLo + 1) * lineH) + "px";
208+ }
209+ function applySel(lo, hi) {
210+ selLo = lo;
211+ selHi = hi;
212+ positionHighlight();
213+ }
214+ function deselect() {
215+ selLo = selHi = 0;
216+ anchor = null;
217+ positionHighlight();
218+ history.replaceState(null, "", location.pathname + location.search);
219+ }
220+ function parseHash(hash) {
221+ const m = /^#L(\d+)(?:-L(\d+))?$/.exec(hash || "");
222+ if (!m) return null;
223+ let lo = parseInt(m[1], 10);
224+ let hi = m[2] ? parseInt(m[2], 10) : lo;
225+ if (hi < lo) { const t = lo; lo = hi; hi = t; }
226+ lo = Math.max(1, Math.min(lo, lineCount));
227+ hi = Math.max(1, Math.min(hi, lineCount));
228+ return [lo, hi];
229+ }
230+ function setHash(lo, hi) {
231+ const frag = lo === hi ? "L" + lo : "L" + lo + "-L" + hi;
232+ history.replaceState(null, "", "#" + frag);
233+ }
234+ function scrollToLine(n) {
235+ const y = codeTop + (n - 1) * lineH;
236+ if (y < panel.scrollTop || y > panel.scrollTop + panel.clientHeight) {
237+ panel.scrollTop = Math.max(0, y - panel.clientHeight / 2);
238+ }
239+ }
240+ function selectFromHash(scroll) {
241+ const r = parseHash(location.hash);
242+ if (!r) return false;
243+ anchor = r[0];
244+ applySel(r[0], r[1]);
245+ if (scroll) scrollToLine(r[0]);
246+ return true;
247+ }
248+ function lineFromEvent(e) {
249+ // y within the code content: pointer relative to the code pre's rendered
250+ // top, which already accounts for scroll since getBoundingClientRect is
251+ // viewport-relative and the pre moves as the panel scrolls.
252+ const rect = code.getBoundingClientRect();
253+ const y = e.clientY - rect.top - codeTop;
254+ let n = Math.floor(y / lineH) + 1;
255+ if (n < 1) n = 1;
256+ if (n > lineCount) n = lineCount;
257+ return n;
258+ }
259+ function onClick(e) {
260+ const inGutter = e.target === gutter;
261+ if (!inGutter && !e.shiftKey) return; // normal text interaction in code
262+ const n = lineFromEvent(e);
263+ if (!e.shiftKey && selLo === n && selHi === n) {
264+ deselect();
265+ return;
266+ }
267+ if (e.shiftKey && anchor !== null) {
268+ applySel(Math.min(anchor, n), Math.max(anchor, n));
269+ setHash(selLo, selHi);
270+ } else {
271+ anchor = n;
272+ applySel(n, n);
273+ setHash(n, n);
274+ }
275+ }
276+ gutter.addEventListener("click", onClick);
277+ code.addEventListener("click", onClick);
278+ document.addEventListener("keydown", function (e) {
279+ if (e.key === "Escape") deselect();
280+ });
281+ window.addEventListener("hashchange", function () {
282+ if (!selectFromHash(true)) deselect();
283+ });
284+ let rz = null;
285+ window.addEventListener("resize", function () {
286+ clearTimeout(rz);
287+ rz = setTimeout(measure, 100);
288+ });
289+ measure();
290+ selectFromHash(true);
291+ })();
292+ '''
39293 ++ txt
40294 (to-wain:format own)
41295 --
zod/seax/mar/html.hoonnew file+22
@@ -0,0 +1,22 @@
1+::
2+:::: /hoon/html/mar
3+ ::
4+/? 310
5+ ::
6+:::: compute
7+ ::
8+=, html
9+|_ htm=@t
10+++ grow :: convert to
11+ ^?
12+ |% ::
13+ ++ mime [/text/html (met 3 htm) htm] :: to %mime
14+ ++ hymn (need (de-xml htm)) :: to %hymn
15+ -- ::
16+++ grab ^?
17+ |% :: convert from
18+ ++ noun @t :: clam from %noun
19+ ++ mime |=([p=mite q=octs] q.q) :: retrieve form %mime
20+ --
21+++ grad %mime
22+--
zod/seax/mar/js.hoonnew file+25
@@ -0,0 +1,25 @@
1+::
2+:::: /hoon/js/mar
3+ ::
4+/? 310
5+::
6+=, eyre
7+|_ mud=@
8+++ grow
9+ |%
10+ ++ mime [/application/javascript (as-octs:mimes:html (@t mud))]
11+ ++ hymn :: convert to %hymn
12+ |^ html
13+ ++ script ;script
14+ ;- (trip (@t mud))
15+ ==
16+ ++ html ;html:(head:"{script}" body)
17+ --
18+ --
19+++ grab
20+ |% :: convert from
21+ ++ mime |=([p=mite q=octs] (@t q.q))
22+ ++ noun cord :: clam from %noun
23+ --
24+++ grad %mime
25+--
zod/seax/mar/png.hoonnew file+12
@@ -0,0 +1,12 @@
1+|_ dat=@
2+++ grow
3+ |%
4+ ++ mime [/image/png (as-octs:mimes:html dat)]
5+ --
6+++ grab
7+ |%
8+ ++ mime |=([p=mite q=octs] q.q)
9+ ++ noun @
10+ --
11+++ grad %mime
12+--
zod/seax/mar/txt-diff.hoonnew file+16
@@ -0,0 +1,16 @@
1+::
2+:::: /hoon/txt-diff/mar
3+ ::
4+/? 310
5+|_ txt-diff=(urge:clay cord)
6+::
7+++ grad %noun
8+++ grow
9+ |%
10+ ++ noun txt-diff
11+ --
12+++ grab :: convert from
13+ |%
14+ ++ noun (urge:clay cord) :: make from %noun
15+ --
16+--
zod/seax/mar/txt.hoon−1
@@ -18,7 +18,6 @@
1818 => v=.
1919 |%
2020 ++ mime => v [/text/plain (as-octs (of-wain txt))]
21− ++ elem => v ;pre: {(trip (of-wain txt))}
2221 --
2322++ grad
2423 |%
zod/seax/mar/woff2.hoonnew file+12
@@ -0,0 +1,12 @@
1+|_ dat=octs
2+++ grow
3+ |%
4+ ++ mime [/font/woff2 dat]
5+ --
6+++ grab
7+ |%
8+ ++ mime |=([=mite =octs] octs)
9+ ++ noun octs
10+ --
11+++ grad %mime
12+--
zod/seax/sur/docket.hoonnew file+82
@@ -0,0 +1,82 @@
1+|%
2+::
3++$ version
4+ [major=@ud minor=@ud patch=@ud]
5+::
6++$ glob (map path mime)
7+::
8++$ url cord
9+:: $glob-location: How to retrieve a glob
10+::
11++$ glob-reference
12+ [hash=@uvH location=glob-location]
13+::
14++$ glob-location
15+ $% [%http =url]
16+ [%ames =ship]
17+ ==
18+:: $href: Where a tile links to
19+::
20++$ href
21+ $% [%glob base=term =glob-reference]
22+ [%site =path]
23+ ==
24+:: $chad: State of a docket
25+::
26++$ chad
27+ $~ [%install ~]
28+ $% :: Done
29+ [%glob =glob]
30+ [%site ~]
31+ :: Waiting
32+ [%install ~]
33+ [%suspend glob=(unit glob)]
34+ :: Error
35+ [%hung err=cord]
36+ ==
37+::
38+:: $charge: A realized $docket
39+::
40++$ charge
41+ $: =docket
42+ =chad
43+ ==
44+::
45+:: $clause: A key and value, as part of a docket
46+::
47+:: Only used to parse $docket
48+::
49++$ clause
50+ $% [%title title=@t]
51+ [%info info=@t]
52+ [%color color=@ux]
53+ [%glob-http url=cord hash=@uvH]
54+ [%glob-ames =ship hash=@uvH]
55+ [%image =url]
56+ [%site =path]
57+ [%base base=term]
58+ [%version =version]
59+ [%website website=url]
60+ [%license license=cord]
61+ ==
62+::
63+:: $docket: A description of JS bundles for a desk
64+::
65++$ docket
66+ $: %1
67+ title=@t
68+ info=@t
69+ color=@ux
70+ =href
71+ image=(unit url)
72+ =version
73+ website=url
74+ license=cord
75+ ==
76+::
77++$ charge-update
78+ $% [%initial initial=(map desk charge)]
79+ [%add-charge =desk =charge]
80+ [%del-charge =desk]
81+ ==
82+--
zod/seax/sur/seax.hoon+10−1
@@ -7,8 +7,17 @@
77++ engine
88 $_ ^|
99 |%
10− ++ url |~ tape *tape
10+ ++ request |~ tape *request:http
1111 ++ results |~ mime-data:iris *(unit (list search-result))
1212 --
1313+$ engines (map name=term [=engine weight=@rs])
14+:: +get: a GET request to url with the query appended
15+::
16+:: The runtime unescapes the query string once before sending the
17+:: request, so the query is escaped twice to stay escaped on the wire.
18+::
19+++ get
20+ |= [url=tape query=tape]
21+ ^- request:http
22+ [%'GET' (crip (weld url (en-urlt:html (en-urlt:html query)))) ~ ~]
1423--
zod/seax/sys.kelvin+1−1
@@ -1 +1 @@
1−[%zuse 413]
1+[%zuse 408]