forked from joejulian/gio
app: add custom scheme support
Now, it's possible to launch one Gio app using a custom URI scheme, such as `gio://some/data`.
This feature is supported on Android, iOS, macOS and Windows, issuing a new transfer.URLEvent,
containing the URL launched. If the program is already open, one transfer.URLEvent will be
sent to the current app.
Limitations:
On Windows, if the program listen to schemes (compiled with `-schemes`), then just a single
instance of the app can be open. In other words, just a single `myprogram.exe` can
be active.
Security:
Deeplinking have the same level of security of clipboard. Any other software can send such
information and read the content, without any restriction. That should not be used to transfer
sensible data, and can't be fully trusted.
Setup/Compiling:
In order to set the custom scheme, you need to use the new `-schemes` flag in `gogio`, using
as `-schemes gio` will listen to `gio://`.
If you are not using gogio you need to defined some values, which varies for each OS:
macOS/iOS - You need to define the following Properly List:
```
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>yourCustomScheme</string>
</array>
</dict>
</array>
```
Windows - You need to compiling using -X argument:
```
-ldflags="-X "gioui.org/app.schemesURI=yourCustomScheme" -H=windowsgui"
```
Android - You need to add IntentFilter in GioActivity:
```
<intent-filter>
<action android:name="android.intent.action.VIEW"></action>
<category android:name="android.intent.category.DEFAULT"></category>
<category android:name="android.intent.category.BROWSABLE"></category>
<data android:scheme="yourCustomScheme"></data>
</intent-filter>
```
That assumes that you still using GioActivity and GioAppDelegate, otherwise more
changes are required.
Events are routed to a new app.Events, which are not linked to a specific window.
Signed-off-by: inkeliz <inkeliz@inkeliz.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
@@ -4,6 +4,7 @@ package org.gioui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.View;
|
||||
@@ -29,6 +30,7 @@ public final class GioActivity extends Activity {
|
||||
|
||||
layer.addView(view);
|
||||
setContentView(layer);
|
||||
onNewIntent(this.getIntent());
|
||||
}
|
||||
|
||||
@Override public void onDestroy() {
|
||||
@@ -60,4 +62,9 @@ public final class GioActivity extends Activity {
|
||||
if (!view.backPressed())
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
view.onIntentEvent(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.app.Fragment;
|
||||
import android.app.FragmentManager;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
@@ -315,6 +316,15 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
|
||||
window.setAttributes(layoutParams);
|
||||
}
|
||||
|
||||
protected void onIntentEvent(Intent intent) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
if (intent.getData() != null) {
|
||||
this.onOpenURI(nhandle, intent.getData().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected boolean dispatchHoverEvent(MotionEvent event) {
|
||||
if (!accessManager.isTouchExplorationEnabled()) {
|
||||
return super.dispatchHoverEvent(event);
|
||||
@@ -553,6 +563,7 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
|
||||
static private native void onExitTouchExploration(long handle);
|
||||
static private native void onA11yFocus(long handle, int viewId);
|
||||
static private native void onClearA11yFocus(long handle, int viewId);
|
||||
static private native void onOpenURI(long handle, String uri);
|
||||
static private native void imeSetSnippet(long handle, int start, int end);
|
||||
static private native String imeSnippet(long handle);
|
||||
static private native int imeSnippetStart(long handle);
|
||||
|
||||
+51
@@ -3,7 +3,10 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"gioui.org/io/event"
|
||||
"golang.org/x/net/idna"
|
||||
"image"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -56,6 +59,15 @@ type FrameEvent struct {
|
||||
Source input.Source
|
||||
}
|
||||
|
||||
// URLEvent is generated for external requests to open a URL. Unlike window specific events,
|
||||
// it is delivered through the [Events] iterator.
|
||||
//
|
||||
// In order to receive URLEvents the program must register one or more URL schemes. A scheme can
|
||||
// be registered using gogio, with the `-schemes` flag.
|
||||
type URLEvent struct {
|
||||
URL *url.URL
|
||||
}
|
||||
|
||||
// ViewEvent provides handles to the underlying window objects for the
|
||||
// current display protocol.
|
||||
type ViewEvent interface {
|
||||
@@ -136,7 +148,29 @@ func Main() {
|
||||
osMain()
|
||||
}
|
||||
|
||||
// Events is an iterator that yields events that are not specific to any window,
|
||||
// such as [URLEvent]. It never returns.
|
||||
//
|
||||
// Events must be called by the main goroutine, and replaces the
|
||||
// call to [Main].
|
||||
func Events(yield func(event.Event) bool) {
|
||||
yieldGlobalEvent = yield
|
||||
osMain()
|
||||
}
|
||||
|
||||
var yieldGlobalEvent func(evt event.Event) bool
|
||||
|
||||
func processGlobalEvent(evt event.Event) {
|
||||
if yieldGlobalEvent == nil {
|
||||
return
|
||||
}
|
||||
if !yieldGlobalEvent(evt) {
|
||||
yieldGlobalEvent = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (FrameEvent) ImplementsEvent() {}
|
||||
func (URLEvent) ImplementsEvent() {}
|
||||
|
||||
func init() {
|
||||
if extraArgs != "" {
|
||||
@@ -147,3 +181,20 @@ func init() {
|
||||
ID = filepath.Base(os.Args[0])
|
||||
}
|
||||
}
|
||||
|
||||
// newURLEvent creates a URLEvent from a raw URL string, handling Punycode decoding.
|
||||
func newURLEvent(rawurl string) (URLEvent, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return URLEvent{}, err
|
||||
}
|
||||
u.Host, err = idna.Punycode.ToUnicode(u.Hostname())
|
||||
if err != nil {
|
||||
return URLEvent{}, err
|
||||
}
|
||||
u, err = url.Parse(u.String())
|
||||
if err != nil {
|
||||
return URLEvent{}, err
|
||||
}
|
||||
return URLEvent{URL: u}, nil
|
||||
}
|
||||
|
||||
@@ -56,6 +56,11 @@ For example, to display a blank but otherwise functional window:
|
||||
app.Main()
|
||||
}
|
||||
|
||||
# Events
|
||||
|
||||
The [Events] iterator yields app-specific events such as [URLEvent]. [Window.Event]
|
||||
yields events that target a particular window.
|
||||
|
||||
# Permissions
|
||||
|
||||
The packages under gioui.org/app/permission should be imported
|
||||
|
||||
+10
-1
@@ -121,6 +121,7 @@ import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"gioui.org/io/transfer"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
@@ -146,7 +147,6 @@ import (
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/semantic"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
@@ -664,6 +664,15 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view
|
||||
}
|
||||
}
|
||||
|
||||
//export Java_org_gioui_GioView_onOpenURI
|
||||
func Java_org_gioui_GioView_onOpenURI(env *C.JNIEnv, class C.jclass, view C.jlong, uri C.jstring) {
|
||||
evt, err := newURLEvent(goString(env, uri))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
processGlobalEvent(evt)
|
||||
}
|
||||
|
||||
func (w *window) ProcessEvent(e event.Event) {
|
||||
w.processEvent(e)
|
||||
}
|
||||
|
||||
@@ -427,6 +427,15 @@ func osMain() {
|
||||
select {}
|
||||
}
|
||||
|
||||
//export gio_onOpenURI
|
||||
func gio_onOpenURI(uri C.CFTypeRef) {
|
||||
evt, err := newURLEvent(nsstringToString(uri))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
processGlobalEvent(evt)
|
||||
}
|
||||
|
||||
//export gio_runMain
|
||||
func gio_runMain() {
|
||||
if !isMainThread() {
|
||||
|
||||
@@ -293,6 +293,10 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
|
||||
[self.window makeKeyAndVisible];
|
||||
return YES;
|
||||
}
|
||||
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
|
||||
gio_onOpenURI((__bridge CFTypeRef)url.absoluteString);
|
||||
return YES;
|
||||
}
|
||||
@end
|
||||
|
||||
int gio_applicationMain(int argc, char *argv[]) {
|
||||
|
||||
@@ -997,6 +997,16 @@ func gio_onFinishLaunching() {
|
||||
close(launched)
|
||||
}
|
||||
|
||||
//export gio_onOpenURI
|
||||
func gio_onOpenURI(uri C.CFTypeRef) {
|
||||
evt, err := newURLEvent(nsstringToString(uri))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
processGlobalEvent(evt)
|
||||
}
|
||||
|
||||
func newWindow(win *callbacks, options []Option) {
|
||||
<-launched
|
||||
res := make(chan struct{})
|
||||
|
||||
@@ -421,6 +421,11 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
}
|
||||
- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
|
||||
for (NSURL *url in urls) {
|
||||
gio_onOpenURI((__bridge CFTypeRef)url.absoluteString);
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
void gio_main() {
|
||||
|
||||
+233
-3
@@ -5,8 +5,12 @@ package app
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"gioui.org/io/transfer"
|
||||
syscall "golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"image"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -16,8 +20,6 @@ import (
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
|
||||
syscall "golang.org/x/sys/windows"
|
||||
|
||||
"gioui.org/app/internal/windows"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
@@ -28,7 +30,6 @@ import (
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
)
|
||||
|
||||
type Win32ViewEvent struct {
|
||||
@@ -56,6 +57,8 @@ type window struct {
|
||||
|
||||
const _WM_WAKEUP = windows.WM_USER + iota
|
||||
|
||||
const copyDataURLType = 0xffffff00
|
||||
|
||||
type gpuAPI struct {
|
||||
priority int
|
||||
initializer func(w *window) (context, error)
|
||||
@@ -81,6 +84,7 @@ var resources struct {
|
||||
}
|
||||
|
||||
func osMain() {
|
||||
processURLEvent(startupURI())
|
||||
select {}
|
||||
}
|
||||
|
||||
@@ -452,6 +456,20 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
||||
case windows.WM_IME_ENDCOMPOSITION:
|
||||
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
|
||||
return windows.TRUE
|
||||
case windows.WM_COPYDATA:
|
||||
data := (*windows.CopyDataStruct)(unsafe.Pointer(lParam))
|
||||
switch data.DwData {
|
||||
case copyDataURLType:
|
||||
if schemesURI == "" {
|
||||
return windows.TRUE
|
||||
}
|
||||
|
||||
uri := syscall.UTF16PtrToString((*uint16)(unsafe.Pointer(data.LpData)))
|
||||
if processURLEvent(uri) {
|
||||
w.Perform(system.ActionRaise)
|
||||
}
|
||||
return windows.TRUE
|
||||
}
|
||||
}
|
||||
|
||||
return windows.DefWindowProc(hwnd, msg, wParam, lParam)
|
||||
@@ -1058,3 +1076,215 @@ func getPointerButtons(pi windows.PointerInfo) pointer.Buttons {
|
||||
|
||||
return btns
|
||||
}
|
||||
|
||||
// schemesURI is a list of schemes, comma separated, that must be
|
||||
// defined using -X compiler ldflag, that used in gogio.
|
||||
var schemesURI string
|
||||
|
||||
func init() {
|
||||
if schemesURI == "" {
|
||||
return
|
||||
}
|
||||
|
||||
currentSchemes := strings.Split(schemesURI, ",")
|
||||
oldSchemes := registeredSchemes(ID)
|
||||
|
||||
for _, s := range currentSchemes {
|
||||
for i, o := range oldSchemes {
|
||||
if s == o {
|
||||
oldSchemes = append(oldSchemes[:i], oldSchemes[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(oldSchemes) > 0 {
|
||||
go unregisterSchemes(ID, oldSchemes)
|
||||
}
|
||||
|
||||
if len(currentSchemes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// On Windows, launching the app using a URI will start a new instance of the app,
|
||||
// a new window. That behavior, by default, doesn't align with iOS/Android/macOS, where
|
||||
// the deeplink sends the event to the running app (if any). We are emulating it.
|
||||
if hwnd, _ := windows.FindWindow(ID); hwnd != 0 {
|
||||
if u := startupURI(); u != "" {
|
||||
broadcastURI(hwnd, u)
|
||||
}
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
|
||||
go registerSchemes(ID, currentSchemes)
|
||||
}
|
||||
|
||||
func startupURI() string {
|
||||
if len(os.Args) == 3 && os.Args[1] == "-gio_launch_url" {
|
||||
return os.Args[2]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func processURLEvent(rawurl string) bool {
|
||||
if rawurl == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
evt, err := newURLEvent(rawurl)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, scheme := range strings.Split(schemesURI, ",") {
|
||||
if strings.EqualFold(scheme, evt.URL.Scheme) {
|
||||
processGlobalEvent(evt)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func broadcastURI(hwnd syscall.Handle, uri string) {
|
||||
data, err := syscall.UTF16FromString(uri)
|
||||
if err != nil {
|
||||
return // Only happens if uri contains NULL character.
|
||||
}
|
||||
|
||||
pinner := new(runtime.Pinner)
|
||||
defer pinner.Unpin()
|
||||
pinner.Pin(unsafe.Pointer(&data[0]))
|
||||
|
||||
msg := &windows.CopyDataStruct{
|
||||
DwData: copyDataURLType,
|
||||
CbData: uint32(len(data) * int(unsafe.Sizeof(data[0]))),
|
||||
LpData: uintptr(unsafe.Pointer(unsafe.SliceData(data))),
|
||||
}
|
||||
pinner.Pin(unsafe.Pointer(msg))
|
||||
|
||||
// SendMessage blocks until the message is processed.
|
||||
windows.SendMessage(hwnd, windows.WM_COPYDATA, 0, uintptr(unsafe.Pointer(msg)))
|
||||
}
|
||||
|
||||
func registeredSchemes(appid string) []string {
|
||||
meta, err := registry.OpenKey(registry.CURRENT_USER, `Software\\`+appid, registry.ALL_ACCESS)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer meta.Close()
|
||||
|
||||
schemes, _, _ := meta.GetStringsValue("URISchemes")
|
||||
return schemes
|
||||
}
|
||||
|
||||
func registerSchemes(appid string, schemes []string) error {
|
||||
reg := func(scheme string) error {
|
||||
key, existent, err := registry.CreateKey(registry.CURRENT_USER, `Software\\Classes\\`+scheme, registry.ALL_ACCESS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
if existent {
|
||||
// Check if the existent key belongs to the current application
|
||||
id, _, err := key.GetStringValue("appid")
|
||||
if err != nil || id != appid {
|
||||
return fmt.Errorf("scheme %s already registered by another application", scheme)
|
||||
}
|
||||
}
|
||||
|
||||
path, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = key.SetStringValue("", "URL:"+scheme+" Protocol"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = key.SetStringValue("URL Protocol", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = key.SetStringValue("appid", appid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
icon, _, err := registry.CreateKey(key, `DefaultIcon`, registry.ALL_ACCESS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer icon.Close()
|
||||
|
||||
if err = icon.SetStringValue("", `"`+path+`",1`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, _, err := registry.CreateKey(key, `shell\\open\\command`, registry.ALL_ACCESS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cmd.Close()
|
||||
|
||||
if err = cmd.SetStringValue("", `"`+path+`" -gio_launch_url "%1"`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, scheme := range schemes {
|
||||
if scheme == "" {
|
||||
continue // just in case
|
||||
}
|
||||
if err := reg(scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
meta, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\\`+appid, registry.ALL_ACCESS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer meta.Close()
|
||||
|
||||
if err = meta.SetStringsValue("URISchemes", schemes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unregisterSchemes(appid string, schemes []string) {
|
||||
classes, err := registry.OpenKey(registry.CURRENT_USER, `Software\\Classes`, registry.ALL_ACCESS)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer classes.Close()
|
||||
|
||||
for _, scheme := range schemes {
|
||||
if scheme == "" {
|
||||
continue // just in case
|
||||
}
|
||||
|
||||
key, err := registry.OpenKey(classes, scheme, registry.ALL_ACCESS)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
id, _, err := key.GetStringValue("appid")
|
||||
if err == nil && id != appid {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, k := range []string{`DefaultIcon`, `shell\\open\\command`, `shell\\open`, `shell`} {
|
||||
registry.DeleteKey(key, k)
|
||||
}
|
||||
|
||||
if err := key.Close(); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
registry.DeleteKey(classes, scheme)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module gioui.org
|
||||
|
||||
go 1.23.8
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
|
||||
@@ -9,6 +9,8 @@ require (
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
|
||||
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0
|
||||
golang.org/x/image v0.26.0
|
||||
golang.org/x/sys v0.33.0
|
||||
golang.org/x/text v0.24.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/text v0.32.0
|
||||
)
|
||||
|
||||
require golang.org/x/net v0.48.0
|
||||
|
||||
@@ -13,7 +13,9 @@ golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfa
|
||||
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
|
||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
|
||||
Reference in New Issue
Block a user