├── .github
└── workflows
│ └── static.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── backend
├── .gitignore
├── cursors
│ ├── config.cue
│ ├── cursors.go
│ ├── db.go
│ ├── migrations
│ │ ├── 1_create_tables.up.sql
│ │ ├── 2_path_url.up.sql
│ │ └── 3_url_index.up.sql
│ ├── pubsub.go
│ ├── subscribe.go
│ └── validate.go
├── encore.app
├── go.mod
└── go.sum
├── demo
├── index.html
└── style.css
├── local-demo
├── cursors.min.js
├── index.html
└── style.css
├── logo.png
└── script
├── .prettierignore
├── .prettierrc
├── dist
└── cursors.min.js
├── package-lock.json
├── package.json
├── rollup.config.js
└── src
├── assets
├── mac.svg
├── tux.svg
└── win.svg
├── config.js
├── cursors.js
└── ws.js
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["master"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: "pages"
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Single deploy job since we're just deploying
26 | deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v4
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v5
36 | - name: Upload artifact
37 | uses: actions/upload-pages-artifact@v3
38 | with:
39 | # Upload entire repository
40 | path: './demo'
41 | - name: Deploy to GitHub Pages
42 | id: deployment
43 | uses: actions/deploy-pages@v4
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | .encore
4 | encore.gen
5 | encore.gen.go
6 | encore.gen.cue
7 |
8 | node_modules
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Ansar Smagulov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: run-backend build-script run-demo lint-script
2 |
3 | run-backend:
4 | @cd backend && encore run
5 | build-script:
6 | @cd script && npm run build && rm ../local-demo/cursors.min.js && cp dist/cursors.min.js ../local-demo/cursors.min.js
7 | run-demo:
8 | @cd local-demo && python3 -m http.server 8000
9 | lint-script:
10 | @cd script && npm run lint
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Encursors
9 |
10 |
11 | Ever feel like a lone wanderer on the digital plains of the web? Do your static websites seem just a tad too... *static*? Fear not, for Encursors is here to help! This nifty little tool transforms your website into a bustling village square. With a simple script, Encursors displays each visitor's cursor movements in real time, letting everyone see where everyone else is looking. It's like a party on your page, and everyone's invited!
12 |
13 | The backend is built and hosted with [Encore](https://encore.dev), a development platform for building event-driven and distributed systems. You can run your own instance by cloning the repostory.
14 |
15 | > [!NOTE]
16 | > Encursors does not display cursors or track users on mobile devices.
17 |
18 | ## Demo
19 | You can see Encursors in action on our [demo page](https://anfragment.github.io/encursors/). Open the page in multiple tabs or devices to see the cursors move in real time!
20 |
21 | ## Features
22 | - Displays the flag of the country the visitor is from alongside their cursor.
23 | - Custom cursors based on the visitor's operating system.
24 | - Respects the [prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) setting by not displaying the cursors when it is enabled.
25 | - No cookies or tracking of any kind. Cursor data is permanently deleted after a visitor leaves the page.
26 | - Fully open source and free to use.
27 |
28 | ## Installation
29 | To install Encursors, simply add the following script tag to your website's HTML:
30 | ```html
31 |
32 | ```
33 |
34 | ## Configuration options
35 | You can configure Encursors by passing options to the script tag as data attributes. Here are the available options:
36 | - `data-api-url`: The base URL of the API. Set if you're running your own instance. Should not include the protocol or the trailing slash.
37 | - `data-z-index`: The z-index of the cursor elements. Optional.
38 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | encore.gen.go
2 | encore.gen.cue
3 | /.encore
4 | /encore.gen
5 |
--------------------------------------------------------------------------------
/backend/cursors/config.cue:
--------------------------------------------------------------------------------
1 | AllowLocalhost: bool | *false
2 | MinEventTimeoutMs: 1500
3 |
4 | if #Meta.Environment.Type == "development" {
5 | AllowLocalhost: true
6 | }
--------------------------------------------------------------------------------
/backend/cursors/cursors.go:
--------------------------------------------------------------------------------
1 | package cursors
2 |
3 | import (
4 | "context"
5 |
6 | "encore.dev/beta/errs"
7 | "encore.dev/config"
8 | "encore.dev/rlog"
9 | )
10 |
11 | type Config struct {
12 | AllowLocalhost config.Bool
13 | MinEventTimeoutMs config.Int
14 | }
15 |
16 | var cfg = config.Load[*Config]()
17 |
18 | type CursorOS int
19 |
20 | const (
21 | CursorOSMacOS CursorOS = iota
22 | CursorOSWindows
23 | CursorOSLinux
24 | )
25 |
26 | type Cursor struct {
27 | Id string `json:"id"`
28 | Country string `json:"country"`
29 | OS CursorOS `json:"os"`
30 | URL string `json:"url"`
31 | PosX int `json:"posX"`
32 | PosY int `json:"posY"`
33 | }
34 |
35 | type GetCursors struct {
36 | Cursors []*Cursor `json:"cursors"`
37 | }
38 |
39 | type GetCursorsParams struct {
40 | URL string
41 | }
42 |
43 | // Cursors returns all cursors for a given path.
44 | //
45 | //encore:api public method=GET path=/cursors
46 | func Cursors(ctx context.Context, p *GetCursorsParams) (GetCursors, error) {
47 | if !validateURL(p.URL) {
48 | return GetCursors{}, &errs.Error{
49 | Code: errs.InvalidArgument,
50 | Message: "invalid URL",
51 | }
52 | }
53 |
54 | cursors, err := getCursorsByURLFromDB(ctx, p.URL)
55 | if err != nil {
56 | rlog.Error("failed to retrieve cursors", "error", err)
57 | return GetCursors{}, &errs.Error{
58 | Code: errs.Internal,
59 | Message: "failed to retrieve cursors",
60 | }
61 | }
62 | return GetCursors{Cursors: cursors}, nil
63 | }
64 |
--------------------------------------------------------------------------------
/backend/cursors/db.go:
--------------------------------------------------------------------------------
1 | package cursors
2 |
3 | import (
4 | "context"
5 |
6 | "encore.dev/storage/sqldb"
7 | )
8 |
9 | var db = sqldb.NewDatabase("cursors", sqldb.DatabaseConfig{
10 | Migrations: "./migrations",
11 | })
12 |
13 | func writeCursorToDB(ctx context.Context, cursor *Cursor) error {
14 | _, err := db.Exec(ctx, "INSERT INTO cursors (id, country, os, url, pos_x, pos_y) VALUES ($1, $2, $3, $4, $5, $6)",
15 | cursor.Id, cursor.Country, cursor.OS, cursor.URL, cursor.PosX, cursor.PosY)
16 | return err
17 | }
18 |
19 | func getCursorsByURLFromDB(ctx context.Context, url string) ([]*Cursor, error) {
20 | rows, err := db.Query(ctx, "SELECT id, country, os, url, pos_x, pos_y FROM cursors WHERE url = $1", url)
21 | if err != nil {
22 | return nil, err
23 | }
24 | defer rows.Close()
25 |
26 | var cursors []*Cursor
27 | for rows.Next() {
28 | var cursor Cursor
29 | if err := rows.Scan(&cursor.Id, &cursor.Country, &cursor.OS, &cursor.URL, &cursor.PosX, &cursor.PosY); err != nil {
30 | return nil, err
31 | }
32 | cursors = append(cursors, &cursor)
33 | }
34 | return cursors, nil
35 | }
36 |
37 | func updateCursorInDB(ctx context.Context, cursor *Cursor) error {
38 | _, err := db.Exec(ctx, "UPDATE cursors SET pos_x = $1, pos_y = $2 WHERE id = $3", cursor.PosX, cursor.PosY, cursor.Id)
39 | return err
40 | }
41 |
42 | func deleteCursorFromDB(ctx context.Context, id string) error {
43 | _, err := db.Exec(ctx, "DELETE FROM cursors WHERE id = $1", id)
44 | return err
45 | }
46 |
--------------------------------------------------------------------------------
/backend/cursors/migrations/1_create_tables.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE cursors (
2 | id CHAR(36) PRIMARY KEY,
3 | os INT NOT NULL,
4 | country CHAR(2) NOT NULL,
5 | path VARCHAR(2048) NOT NULL,
6 | pos_x INT NOT NULL,
7 | pos_y INT NOT NULL
8 | );
--------------------------------------------------------------------------------
/backend/cursors/migrations/2_path_url.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE cursors
2 | RENAME COLUMN path TO url;
--------------------------------------------------------------------------------
/backend/cursors/migrations/3_url_index.up.sql:
--------------------------------------------------------------------------------
1 | CREATE INDEX cursors_url_index ON cursors (url);
--------------------------------------------------------------------------------
/backend/cursors/pubsub.go:
--------------------------------------------------------------------------------
1 | package cursors
2 |
3 | import (
4 | "context"
5 | "sync"
6 |
7 | "encore.dev/pubsub"
8 | )
9 |
10 | type CursorEventType string
11 |
12 | const (
13 | CursorEventTypeEnter CursorEventType = "enter"
14 | CursorEventTypeMove CursorEventType = "move"
15 | CursorEventTypeLeave CursorEventType = "leave"
16 | )
17 |
18 | type CursorEvent struct {
19 | Type CursorEventType `json:"type"`
20 | Cursor *Cursor `json:"cursor"`
21 | }
22 |
23 | var subscribersMu sync.RWMutex
24 |
25 | // subscribers maps URLs to a map of subscriber IDs to channels.
26 | var subscribers = make(map[string]map[string]chan *CursorEvent)
27 |
28 | // fanout sends the cursor to all subscribers.
29 | func fanout(ctx context.Context, event *CursorEvent) error {
30 | subscribersMu.RLock()
31 | defer subscribersMu.RUnlock()
32 | for _, ch := range subscribers[event.Cursor.URL] {
33 | ch <- event
34 | }
35 | return nil
36 | }
37 |
38 | // subToUpdates subscribes a client to cursor updates.
39 | func subToUpdates(id string, url string, ch chan *CursorEvent, done <-chan struct{}) {
40 | subscribersMu.Lock()
41 | defer subscribersMu.Unlock()
42 | if _, ok := subscribers[url]; !ok {
43 | subscribers[url] = make(map[string]chan *CursorEvent)
44 | }
45 | subscribers[url][id] = ch
46 | go func() {
47 | <-done
48 | subscribersMu.Lock()
49 | defer subscribersMu.Unlock()
50 | delete(subscribers[url], id)
51 | }()
52 | }
53 |
54 | var CursorEvents = pubsub.NewTopic[*CursorEvent]("cursor-events", pubsub.TopicConfig{
55 | DeliveryGuarantee: pubsub.AtLeastOnce,
56 | })
57 |
58 | var _ = pubsub.NewSubscription[*CursorEvent](CursorEvents, "cursor-events-fanout", pubsub.SubscriptionConfig[*CursorEvent]{
59 | Handler: fanout,
60 | })
61 |
--------------------------------------------------------------------------------
/backend/cursors/subscribe.go:
--------------------------------------------------------------------------------
1 | package cursors
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "net/http"
7 | "strconv"
8 | "time"
9 |
10 | "encore.dev/metrics"
11 | "encore.dev/rlog"
12 | "github.com/google/uuid"
13 | "github.com/gorilla/websocket"
14 | )
15 |
16 | type CursorEnterWebsocketPayload struct {
17 | Id string `json:"id"`
18 | Country string `json:"country"`
19 | OS CursorOS `json:"os"`
20 | PosX int `json:"posX"`
21 | PosY int `json:"posY"`
22 | }
23 |
24 | type CursorMoveWebsocketPayload struct {
25 | Id string `json:"id"`
26 | PosX int `json:"posX"`
27 | PosY int `json:"posY"`
28 | }
29 |
30 | type CursorLeaveWebsocketPayload struct {
31 | Id string `json:"id"`
32 | }
33 |
34 | var upgrader = websocket.Upgrader{
35 | CheckOrigin: func(r *http.Request) bool {
36 | return true
37 | },
38 | }
39 |
40 | var TotalClients = metrics.NewCounter[uint64]("total_clients", metrics.CounterConfig{})
41 |
42 | // Subscribe subscribes to cursor updates for a given URL.
43 | //
44 | //encore:api public raw method=GET path=/subscribe
45 | func Subscribe(w http.ResponseWriter, req *http.Request) {
46 | ctx := req.Context()
47 | query := req.URL.Query()
48 |
49 | url := query.Get("url")
50 | if !validateURL(url) {
51 | http.Error(w, "specify valid url in url parameters", http.StatusBadRequest)
52 | return
53 | }
54 |
55 | var os CursorOS
56 | switch query.Get("os") {
57 | case "mac":
58 | os = CursorOSMacOS
59 | case "win":
60 | os = CursorOSWindows
61 | case "linux":
62 | os = CursorOSLinux
63 | default:
64 | http.Error(w, "specify valid os in url parameters", http.StatusBadRequest)
65 | return
66 | }
67 |
68 | country := query.Get("country")
69 | if country == "" || len(country) != 2 {
70 | http.Error(w, "specify valid country in url parameters", http.StatusBadRequest)
71 | return
72 | }
73 |
74 | var posX, posY int
75 | var err error
76 | if query.Get("posX") != "" {
77 | posX, err = strconv.Atoi(query.Get("posX"))
78 | if err != nil {
79 | http.Error(w, "specify valid posX in url parameters", http.StatusBadRequest)
80 | return
81 | }
82 | }
83 | if query.Get("posY") != "" {
84 | posY, err = strconv.Atoi(query.Get("posY"))
85 | if err != nil {
86 | http.Error(w, "specify valid posY in url parameters", http.StatusBadRequest)
87 | return
88 | }
89 | }
90 |
91 | c, err := upgrader.Upgrade(w, req, nil)
92 | if err != nil {
93 | rlog.Error("error upgrading websocket connection", "err", err)
94 | return
95 | }
96 |
97 | TotalClients.Increment()
98 |
99 | id := uuid.New().String()
100 | rlog := rlog.With("cursor_id", id)
101 |
102 | events := make(chan *CursorEvent)
103 | done := make(chan struct{})
104 | go handleIncomingPubSubEvents(ctx, rlog, id, url, events, done, c)
105 | defer handleClosure(ctx, rlog, events, done, c)
106 |
107 | cursor := &Cursor{
108 | Id: id,
109 | Country: country,
110 | OS: os,
111 | URL: url,
112 | PosX: posX,
113 | PosY: posY,
114 | }
115 | if err := writeCursorToDB(ctx, cursor); err != nil {
116 | rlog.Error("error writing cursor to db", "err", err)
117 | return
118 | }
119 | defer deleteCursorFromDB(ctx, id)
120 |
121 | handleWSComms(ctx, rlog, cursor, c)
122 | }
123 |
124 | func handleWSComms(ctx context.Context, rlog rlog.Ctx, cursor *Cursor, c *websocket.Conn) {
125 | event := &CursorEvent{
126 | Type: CursorEventTypeEnter,
127 | Cursor: cursor,
128 | }
129 | if msgId, err := CursorEvents.Publish(ctx, event); err != nil {
130 | rlog.Error("error publishing cursor event", "err", err)
131 | } else {
132 | rlog.Debug("published cursor enter event", "msg_id", msgId)
133 | }
134 |
135 | var lastMessageAt time.Time
136 |
137 | for {
138 | if ctx.Err() != nil {
139 | break
140 | }
141 |
142 | mt, message, err := c.ReadMessage()
143 | if err != nil {
144 | if !websocket.IsCloseError(err, websocket.CloseNormalClosure) {
145 | rlog.Error("error reading message", "err", err)
146 | }
147 | break
148 | }
149 | if mt != websocket.TextMessage {
150 | rlog.Error("unexpected message type", "type", mt)
151 | break
152 | }
153 |
154 | if time.Since(lastMessageAt) < time.Duration(cfg.MinEventTimeoutMs())*time.Millisecond {
155 | rlog.Error("received message too quickly, ignoring", "elapsed", time.Since(lastMessageAt))
156 | lastMessageAt = time.Now()
157 | continue
158 | }
159 | lastMessageAt = time.Now()
160 |
161 | pos := [2]int{}
162 | if err := json.Unmarshal(message, &pos); err != nil {
163 | rlog.Error("error unmarshalling message", "err", err)
164 | break
165 | }
166 |
167 | rlog.Debug("received cursor position", "pos", pos)
168 |
169 | cursor.PosX = pos[0]
170 | cursor.PosY = pos[1]
171 |
172 | if err := updateCursorInDB(ctx, cursor); err != nil {
173 | rlog.Error("error updating cursor in db", "err", err)
174 | break
175 | }
176 |
177 | event = &CursorEvent{
178 | Type: CursorEventTypeMove,
179 | Cursor: cursor,
180 | }
181 | if msgId, err := CursorEvents.Publish(ctx, event); err != nil {
182 | rlog.Error("error publishing cursor event", "err", err)
183 | } else {
184 | rlog.Debug("published cursor move event", "msg_id", msgId)
185 | }
186 | }
187 |
188 | event.Type = CursorEventTypeLeave
189 | if msgId, err := CursorEvents.Publish(ctx, event); err != nil {
190 | rlog.Error("error publishing cursor event", "err", err)
191 | } else {
192 | rlog.Debug("published cursor leave event", "msg_id", msgId)
193 | }
194 | }
195 |
196 | func handleIncomingPubSubEvents(_ context.Context, rlog rlog.Ctx, id string, url string, eventsCh chan *CursorEvent, doneCh <-chan struct{}, c *websocket.Conn) {
197 | subToUpdates(id, url, eventsCh, doneCh)
198 | for {
199 | select {
200 | case event := <-eventsCh:
201 | if event.Cursor.Id == id {
202 | continue
203 | }
204 | msg := struct {
205 | Type CursorEventType `json:"type"`
206 | Payload interface{} `json:"payload"`
207 | }{
208 | Type: event.Type,
209 | }
210 | switch event.Type {
211 | case CursorEventTypeEnter:
212 | msg.Payload = CursorEnterWebsocketPayload{
213 | Id: event.Cursor.Id,
214 | Country: event.Cursor.Country,
215 | OS: event.Cursor.OS,
216 | PosX: event.Cursor.PosX,
217 | PosY: event.Cursor.PosY,
218 | }
219 | case CursorEventTypeMove:
220 | msg.Payload = CursorMoveWebsocketPayload{
221 | Id: event.Cursor.Id,
222 | PosX: event.Cursor.PosX,
223 | PosY: event.Cursor.PosY,
224 | }
225 | case CursorEventTypeLeave:
226 | msg.Payload = CursorLeaveWebsocketPayload{
227 | Id: event.Cursor.Id,
228 | }
229 | default:
230 | rlog.Error("unknown cursor event type", "type", event.Type)
231 | continue
232 | }
233 |
234 | if err := c.WriteJSON(msg); err != nil {
235 | rlog.Error("error writing JSON", "err", err)
236 | return
237 | }
238 | case <-doneCh:
239 | return
240 | }
241 | }
242 | }
243 |
244 | func handleClosure(ctx context.Context, rlog rlog.Ctx, events chan *CursorEvent, done chan struct{}, c *websocket.Conn) {
245 | rlog.Debug("closing websocket connection")
246 | close(done)
247 | close(events)
248 | c.Close()
249 | }
250 |
--------------------------------------------------------------------------------
/backend/cursors/validate.go:
--------------------------------------------------------------------------------
1 | package cursors
2 |
3 | import (
4 | "net"
5 | "net/url"
6 | "strings"
7 |
8 | "encore.dev/rlog"
9 | )
10 |
11 | // validateURL validates the URL to ensure it can be used for fetching and subscribing to cursor updates.
12 | //
13 | // Returns true if the url is valid, false otherwise.
14 | func validateURL(urlToValidate string) bool {
15 | url, err := url.Parse(urlToValidate)
16 | if err != nil {
17 | rlog.Debug("failed to parse url", "err", err)
18 | return false
19 | }
20 |
21 | if cfg.AllowLocalhost() {
22 | if strings.HasSuffix(url.Host, "localhost") {
23 | return true
24 | }
25 | if ip := net.ParseIP(url.Host); ip != nil && ip.IsLoopback() {
26 | return true
27 | }
28 | }
29 |
30 | if url.Host == "" {
31 | return false
32 | }
33 | if url.Fragment != "" {
34 | return false
35 | }
36 | if strings.HasSuffix(url.Host, "localhost") {
37 | return false
38 | }
39 | if ip := net.ParseIP(url.Host); ip != nil && (ip.IsLoopback() || ip.IsPrivate()) {
40 | return false
41 | }
42 |
43 | return true
44 | }
45 |
--------------------------------------------------------------------------------
/backend/encore.app:
--------------------------------------------------------------------------------
1 | {
2 | "id": "encursors-ypdi",
3 | "global_cors": {
4 | "allow_origins_without_credentials": ["*"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/backend/go.mod:
--------------------------------------------------------------------------------
1 | module encore.app
2 |
3 | go 1.18
4 |
5 | require (
6 | encore.dev v1.34.3
7 | github.com/google/uuid v1.6.0
8 | github.com/gorilla/websocket v1.5.1
9 | )
10 |
11 | require (
12 | github.com/jackc/pgpassfile v1.0.0 // indirect
13 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
14 | github.com/jackc/pgx/v5 v5.5.4 // indirect
15 | github.com/jackc/puddle/v2 v2.2.1 // indirect
16 | golang.org/x/crypto v0.21.0 // indirect
17 | golang.org/x/net v0.23.0 // indirect
18 | golang.org/x/sync v0.1.0 // indirect
19 | golang.org/x/text v0.14.0 // indirect
20 | )
21 |
--------------------------------------------------------------------------------
/backend/go.sum:
--------------------------------------------------------------------------------
1 | encore.dev v1.34.3 h1:jO+8jBqq5/MShiRHSRGPBuTq003YR6KnnspgD+fkMBg=
2 | encore.dev v1.34.3/go.mod h1:XdWK6bKKAVzutmOKpC5qzalDQJLNfRCF/YCgA7OUZ3E=
3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
6 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
8 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
9 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
10 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
11 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
12 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
13 | github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=
14 | github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
15 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
16 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
20 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
22 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
23 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
24 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
25 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
26 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
27 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
28 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
29 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
30 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
32 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
33 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
34 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Encursors Demo
7 |
8 |
9 |
10 |
11 |
Encursors Demo
12 |
13 |
14 | Stopping by Woods on a Snowy Evening, by Robert Frost
15 |
16 |
17 | Whose woods these are I think I know.
18 | His house is in the village though;
19 | He will not see me stopping here
20 | To watch his woods fill up with snow.
21 |
22 | My little horse must think it queer
23 | To stop without a farmhouse near
24 | Between the woods and frozen lake
25 | The darkest evening of the year.
26 |
27 | He gives his harness bells a shake
28 | To ask if there is some mistake.
29 | The only other sound’s the sweep
30 | Of easy wind and downy flake.
31 |
32 | The woods are lovely, dark and deep,
33 | But I have promises to keep,
34 | And miles to go before I sleep,
35 | And miles to go before I sleep.
36 |
37 |
38 |
39 | Lines Written in Early Spring, by William Wordsworth
40 |
41 |
42 | I heard a thousand blended notes,
43 | While in a grove I sate reclined,
44 | In that sweet mood when pleasant thoughts
45 | Bring sad thoughts to the mind.
46 |
47 | To her fair works did Nature link
48 | The human soul that through me ran;
49 | And much it grieved my heart to think
50 | What man has made of man.
51 |
52 | Through primrose tufts, in that green bower,
53 | The periwinkle trailed its wreaths;
54 | And ’tis my faith that every flower
55 | Enjoys the air it breathes.
56 |
57 | The birds around me hopped and played,
58 | Their thoughts I cannot measure:—
59 | But the least motion which they made
60 | It seemed a thrill of pleasure.
61 |
62 | The budding twigs spread out their fan,
63 | To catch the breezy air;
64 | And I must think, do all I can,
65 | That there was pleasure there.
66 |
67 | If this belief from heaven be sent,
68 | If such be Nature’s holy plan,
69 | Have I not reason to lament
70 | What man has made of man?
71 |
72 |
73 |
74 | The Summer Day, by Mary Oliver
75 |
76 |
77 | Who made the world?
78 | Who made the swan, and the black bear?
79 | Who made the grasshopper?
80 | This grasshopper, I mean—
81 | the one who has flung herself out of the grass,
82 | the one who is eating sugar out of my hand,
83 | who is moving her jaws back and forth instead of up and down—
84 | who is gazing around with her enormous and complicated eyes.
85 | Now she lifts her pale forearms and thoroughly washes her face.
86 | Now she snaps her wings open, and floats away.
87 | I don't know exactly what a prayer is.
88 | I do know how to pay attention, how to fall down
89 | into the grass, how to kneel down in the grass,
90 | how to be idle and blessed, how to stroll through the fields,
91 | which is what I have been doing all day.
92 | Tell me, what else should I have done?
93 | Doesn't everything die at last, and too soon?
94 | Tell me, what is it you plan to do
95 | with your one wild and precious life?
96 |
97 |
98 |
99 | To Autumn, by John Keats
100 |
101 |
102 | Season of mists and mellow fruitfulness,
103 | Close bosom-friend of the maturing sun;
104 | Conspiring with him how to load and bless
105 | With fruit the vines that round the thatch-eves run;
106 | To bend with apples the moss’d cottage-trees,
107 | And fill all fruit with ripeness to the core;
108 | To swell the gourd, and plump the hazel shells
109 | With a sweet kernel; to set budding more,
110 | And still more, later flowers for the bees,
111 | Until they think warm days will never cease,
112 | For summer has o’er-brimm’d their clammy cells.
113 |
114 | Who hath not seen thee oft amid thy store?
115 | Sometimes whoever seeks abroad may find
116 | Thee sitting careless on a granary floor,
117 | Thy hair soft-lifted by the winnowing wind;
118 | Or on a half-reap’d furrow sound asleep,
119 | Drowsed with the fume of poppies, while thy hook
120 | Spares the next swath and all its twined flowers:
121 | And sometimes like a gleaner thou dost keep
122 | Steady thy laden head across a brook;
123 | Or by a cider-press, with patient look,
124 | Thou watchest the last oozings, hours by hours.
125 |
126 | Where are the songs of Spring? Ay, where are they?
127 | Think not of them, thou hast thy music too,—
128 | While barred clouds bloom the soft-dying day,
129 | And touch the stubble-plains with rosy hue;
130 | Then in a wailful choir the small gnats mourn
131 | Among the river sallows, borne aloft
132 | Or sinking as the light wind lives or dies;
133 | And full-grown lambs loud bleat from hilly bourn;
134 | Hedge-crickets sing; and now with treble soft
135 | The redbreast whistles from a garden-croft,
136 | And gathering swallows twitter in the skies.
137 |
14 | Stopping by Woods on a Snowy Evening, by Robert Frost
15 |
16 |
17 | Whose woods these are I think I know.
18 | His house is in the village though;
19 | He will not see me stopping here
20 | To watch his woods fill up with snow.
21 |
22 | My little horse must think it queer
23 | To stop without a farmhouse near
24 | Between the woods and frozen lake
25 | The darkest evening of the year.
26 |
27 | He gives his harness bells a shake
28 | To ask if there is some mistake.
29 | The only other sound’s the sweep
30 | Of easy wind and downy flake.
31 |
32 | The woods are lovely, dark and deep,
33 | But I have promises to keep,
34 | And miles to go before I sleep,
35 | And miles to go before I sleep.
36 |
37 |
38 |
39 | Lines Written in Early Spring, by William Wordsworth
40 |
41 |
42 | I heard a thousand blended notes,
43 | While in a grove I sate reclined,
44 | In that sweet mood when pleasant thoughts
45 | Bring sad thoughts to the mind.
46 |
47 | To her fair works did Nature link
48 | The human soul that through me ran;
49 | And much it grieved my heart to think
50 | What man has made of man.
51 |
52 | Through primrose tufts, in that green bower,
53 | The periwinkle trailed its wreaths;
54 | And ’tis my faith that every flower
55 | Enjoys the air it breathes.
56 |
57 | The birds around me hopped and played,
58 | Their thoughts I cannot measure:—
59 | But the least motion which they made
60 | It seemed a thrill of pleasure.
61 |
62 | The budding twigs spread out their fan,
63 | To catch the breezy air;
64 | And I must think, do all I can,
65 | That there was pleasure there.
66 |
67 | If this belief from heaven be sent,
68 | If such be Nature’s holy plan,
69 | Have I not reason to lament
70 | What man has made of man?
71 |
72 |
73 |
74 | The Summer Day, by Mary Oliver
75 |
76 |
77 | Who made the world?
78 | Who made the swan, and the black bear?
79 | Who made the grasshopper?
80 | This grasshopper, I mean—
81 | the one who has flung herself out of the grass,
82 | the one who is eating sugar out of my hand,
83 | who is moving her jaws back and forth instead of up and down—
84 | who is gazing around with her enormous and complicated eyes.
85 | Now she lifts her pale forearms and thoroughly washes her face.
86 | Now she snaps her wings open, and floats away.
87 | I don't know exactly what a prayer is.
88 | I do know how to pay attention, how to fall down
89 | into the grass, how to kneel down in the grass,
90 | how to be idle and blessed, how to stroll through the fields,
91 | which is what I have been doing all day.
92 | Tell me, what else should I have done?
93 | Doesn't everything die at last, and too soon?
94 | Tell me, what is it you plan to do
95 | with your one wild and precious life?
96 |
97 |
98 |
99 | To Autumn, by John Keats
100 |
101 |
102 | Season of mists and mellow fruitfulness,
103 | Close bosom-friend of the maturing sun;
104 | Conspiring with him how to load and bless
105 | With fruit the vines that round the thatch-eves run;
106 | To bend with apples the moss’d cottage-trees,
107 | And fill all fruit with ripeness to the core;
108 | To swell the gourd, and plump the hazel shells
109 | With a sweet kernel; to set budding more,
110 | And still more, later flowers for the bees,
111 | Until they think warm days will never cease,
112 | For summer has o’er-brimm’d their clammy cells.
113 |
114 | Who hath not seen thee oft amid thy store?
115 | Sometimes whoever seeks abroad may find
116 | Thee sitting careless on a granary floor,
117 | Thy hair soft-lifted by the winnowing wind;
118 | Or on a half-reap’d furrow sound asleep,
119 | Drowsed with the fume of poppies, while thy hook
120 | Spares the next swath and all its twined flowers:
121 | And sometimes like a gleaner thou dost keep
122 | Steady thy laden head across a brook;
123 | Or by a cider-press, with patient look,
124 | Thou watchest the last oozings, hours by hours.
125 |
126 | Where are the songs of Spring? Ay, where are they?
127 | Think not of them, thou hast thy music too,—
128 | While barred clouds bloom the soft-dying day,
129 | And touch the stubble-plains with rosy hue;
130 | Then in a wailful choir the small gnats mourn
131 | Among the river sallows, borne aloft
132 | Or sinking as the light wind lives or dies;
133 | And full-grown lambs loud bleat from hilly bourn;
134 | Hedge-crickets sing; and now with treble soft
135 | The redbreast whistles from a garden-croft,
136 | And gathering swallows twitter in the skies.
137 |