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
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>
106 lines
3.5 KiB
Go
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 }
|