Files
Warp/apps/desktop/internal/shell/links.go
T
tajniak81andClaude Opus 5.5 b6bf3ce741
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
Desktop client (warp-desktop) with Wails v3
apps/desktop is the web client in a native window. It embeds the same
build warp-web does and serves it through the same Go package, so every
screen is shared. Unlike warp-web it proxies nothing: the client calls the
homeserver directly, and the CSP allows the homeserver plus any https:
and wss: origin for the call backend.

- First run asks for the homeserver: a server name or address, with
  .well-known discovery and a /versions check; File > Change homeserver
  asks again. Signed-in sessions keep their own base URL, so a change
  never sends one server's token to another.
- Links to other sites open in the system browser (/_desktop/links.js,
  added to index.html); only http, https and mailto are opened.
- One instance at a time, since sessions and keys live in the webview's
  storage.
- Dev builds open the DevTools Protocol on Windows when
  WARP_DESKTOP_REMOTE_DEBUGGING_PORT is set, for scripted testing.
- Packaging: icon, version info and manifest in the Windows exe, a macOS
  .app with camera and microphone descriptions, Linux packages via nfpm.
  Make targets: desktop, desktop-run, desktop-test, desktop-resources,
  desktop-macos, desktop-linux.

Web client: host and client move out of internal/ so the desktop app can
import them, and host gains AnyCallOrigin. The Dockerfile, .dockerignore
and the vite output path follow the move.

Pinned to Wails v3.0.0-beta.27.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:43:54 +02:00

106 lines
3.5 KiB
Go

package shell
import (
"bytes"
"io/fs"
"time"
)
// linkScriptPath serves linkScript, which withLinkScript adds to the
// client's index.html. A script of the page's own, rather than one the
// webview injects, runs before the app on every platform and is allowed by
// the client's Content-Security-Policy (script-src 'self').
const linkScriptPath = "/_desktop/links.js"
// linkScript makes links to other sites open in the system browser. A URL
// in a message would otherwise replace the app in its own window, or open a
// bare webview window for target="_blank"; this sends those to openPath
// instead. Links within the app, and blob: URLs for downloads, are left
// alone.
const linkScript = `(() => {
if (window.__warpDesktopLinks) return
window.__warpDesktopLinks = true
const external = (href) => {
try {
const u = new URL(href, location.href)
return u.origin !== location.origin && ['http:', 'https:', 'mailto:'].includes(u.protocol) ? u.href : null
} catch { return null }
}
const open = (href) => fetch('/_desktop/open', {
method: 'POST', credentials: 'omit',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: href }),
}).catch(() => {})
const onClick = (e) => {
if (e.defaultPrevented || e.button > 1) return
const a = e.target instanceof Element ? e.target.closest('a[href]') : null
const href = a && external(a.href)
if (!href) return
e.preventDefault()
open(href)
}
// auxclick is the middle button, which would open a new window.
document.addEventListener('click', onClick, true)
document.addEventListener('auxclick', onClick, true)
const windowOpen = window.open
window.open = function (url, ...rest) {
const href = url == null ? null : external(String(url))
if (href) { open(href); return null }
return windowOpen.call(window, url, ...rest)
}
})()
`
// withLinkScript returns the client with a script tag for linkScript at the
// top of index.html's head. ok is false when index.html has no <head> to
// add it to; the client is then returned unchanged.
func withLinkScript(client fs.FS) (_ fs.FS, ok bool, _ error) {
index, err := fs.ReadFile(client, "index.html")
if err != nil {
return nil, false, err
}
tag := []byte(`<head>` + "\n " + `<script src="` + linkScriptPath + `"></script>`)
patched := bytes.Replace(index, []byte("<head>"), tag, 1)
if bytes.Equal(patched, index) {
return client, false, nil
}
return indexFS{FS: client, index: patched}, true, nil
}
// indexFS is a client whose index.html is replaced.
type indexFS struct {
fs.FS
index []byte
}
func (f indexFS) Open(name string) (fs.File, error) {
if name != "index.html" {
return f.FS.Open(name)
}
return &memFile{Reader: bytes.NewReader(f.index), size: int64(len(f.index))}, nil
}
func (f indexFS) ReadFile(name string) ([]byte, error) {
if name != "index.html" {
return fs.ReadFile(f.FS, name)
}
return bytes.Clone(f.index), nil
}
type memFile struct {
*bytes.Reader
size int64
}
func (m *memFile) Stat() (fs.FileInfo, error) { return memInfo{m.size}, nil }
func (m *memFile) Close() error { return nil }
type memInfo struct{ size int64 }
func (i memInfo) Name() string { return "index.html" }
func (i memInfo) Size() int64 { return i.size }
func (i memInfo) Mode() fs.FileMode { return 0o444 }
func (i memInfo) ModTime() time.Time { return time.Time{} }
func (i memInfo) IsDir() bool { return false }
func (i memInfo) Sys() any { return nil }