detached f8849c8

cstrings: add NSStringToString (#447)

* nsstring

* nsstrings: add NSStringToString

Closes #438

* fix panic package name

* license

* fix year

* underscore in global variables

* rename isKindOf
commit
f8849c8c937f72e5b82038afbcbb862154428fed
author
TotallyGamerJet · · 2026-05-09 18:59 UTC
committer
GitHub · · 2026-05-09 18:59 UTC
parent
fa49e74
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
2 files · +98 -0
A cstrings/nsstrings_darwin.go +44 -0
@@ -0,0 +1,44 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package cstrings
5+
6+import (
7+ "fmt"
8+
9+ "github.com/ebitengine/purego"
10+ "github.com/ebitengine/purego/internal/strings"
11+ "github.com/ebitengine/purego/objc"
12+)
13+
14+var (
15+ sel_isKindOf objc.SEL
16+ sel_UTF8String objc.SEL
17+
18+ class_NSString objc.Class
19+)
20+
21+func init() {
22+ // Must pull in Foundation to get the NSString class.
23+ _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW)
24+ if err != nil {
25+ panic(fmt.Errorf("cstrings: %w", err))
26+ }
27+ sel_isKindOf = objc.RegisterName("isKindOfClass:")
28+ sel_UTF8String = objc.RegisterName("UTF8String")
29+ class_NSString = objc.GetClass("NSString")
30+}
31+
32+// NSStringToString returns a copy of the NSString contents as a Go string.
33+// If the ID is 0 then an empty string is returned.
34+// If the ID is not an NSString class then the function panics.
35+// This function is only available on darwin.
36+func NSStringToString(str objc.ID) string {
37+ if str == 0 {
38+ return ""
39+ }
40+ if str.Send(sel_isKindOf, class_NSString) == 0 {
41+ panic("cstrings: provided ID is not an NSString")
42+ }
43+ return strings.GoString(uintptr(str.Send(sel_UTF8String)))
44+}
@@ -0,0 +1,44 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package cstrings
5+
6+import (
7+ "fmt"
8+
9+ "github.com/ebitengine/purego"
10+ "github.com/ebitengine/purego/internal/strings"
11+ "github.com/ebitengine/purego/objc"
12+)
13+
14+var (
15+ sel_isKindOf objc.SEL
16+ sel_UTF8String objc.SEL
17+
18+ class_NSString objc.Class
19+)
20+
21+func init() {
22+ // Must pull in Foundation to get the NSString class.
23+ _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW)
24+ if err != nil {
25+ panic(fmt.Errorf("cstrings: %w", err))
26+ }
27+ sel_isKindOf = objc.RegisterName("isKindOfClass:")
28+ sel_UTF8String = objc.RegisterName("UTF8String")
29+ class_NSString = objc.GetClass("NSString")
30+}
31+
32+// NSStringToString returns a copy of the NSString contents as a Go string.
33+// If the ID is 0 then an empty string is returned.
34+// If the ID is not an NSString class then the function panics.
35+// This function is only available on darwin.
36+func NSStringToString(str objc.ID) string {
37+ if str == 0 {
38+ return ""
39+ }
40+ if str.Send(sel_isKindOf, class_NSString) == 0 {
41+ panic("cstrings: provided ID is not an NSString")
42+ }
43+ return strings.GoString(uintptr(str.Send(sel_UTF8String)))
44+}
A cstrings/nsstrings_darwin_test.go +54 -0
@@ -0,0 +1,54 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package cstrings_test
5+
6+import (
7+ "testing"
8+
9+ "github.com/ebitengine/purego/cstrings"
10+ "github.com/ebitengine/purego/objc"
11+)
12+
13+func TestNSStringToString(t *testing.T) {
14+ t.Run("nil ID returns empty string", func(t *testing.T) {
15+ result := cstrings.NSStringToString(0)
16+ if result != "" {
17+ t.Errorf("expected empty string for nil ID, got %q", result)
18+ }
19+ })
20+
21+ t.Run("valid NSString returns correct string", func(t *testing.T) {
22+ sel := objc.RegisterName("stringWithUTF8String:")
23+ nsString := objc.ID(objc.GetClass("NSString")).Send(sel, "Hello, World!\x00")
24+
25+ result := cstrings.NSStringToString(nsString)
26+ if result != "Hello, World!" {
27+ t.Errorf("expected %q, got %q", "Hello, World!", result)
28+ }
29+ })
30+
31+ t.Run("empty NSString returns empty string", func(t *testing.T) {
32+ sel := objc.RegisterName("stringWithUTF8String:")
33+ nsString := objc.ID(objc.GetClass("NSString")).Send(sel, "\x00")
34+
35+ result := cstrings.NSStringToString(nsString)
36+ if result != "" {
37+ t.Errorf("expected empty string, got %q", result)
38+ }
39+ })
40+
41+ t.Run("non-NSString panics", func(t *testing.T) {
42+ defer func() {
43+ if r := recover(); r == nil {
44+ t.Error("expected panic for non-NSString ID")
45+ }
46+ }()
47+
48+ classNSNumber := objc.GetClass("NSNumber")
49+ sel := objc.RegisterName("numberWithInt:")
50+ nsNumber := objc.ID(classNSNumber).Send(sel, 42)
51+
52+ cstrings.NSStringToString(nsNumber)
53+ })
54+}
@@ -0,0 +1,54 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package cstrings_test
5+
6+import (
7+ "testing"
8+
9+ "github.com/ebitengine/purego/cstrings"
10+ "github.com/ebitengine/purego/objc"
11+)
12+
13+func TestNSStringToString(t *testing.T) {
14+ t.Run("nil ID returns empty string", func(t *testing.T) {
15+ result := cstrings.NSStringToString(0)
16+ if result != "" {
17+ t.Errorf("expected empty string for nil ID, got %q", result)
18+ }
19+ })
20+
21+ t.Run("valid NSString returns correct string", func(t *testing.T) {
22+ sel := objc.RegisterName("stringWithUTF8String:")
23+ nsString := objc.ID(objc.GetClass("NSString")).Send(sel, "Hello, World!\x00")
24+
25+ result := cstrings.NSStringToString(nsString)
26+ if result != "Hello, World!" {
27+ t.Errorf("expected %q, got %q", "Hello, World!", result)
28+ }
29+ })
30+
31+ t.Run("empty NSString returns empty string", func(t *testing.T) {
32+ sel := objc.RegisterName("stringWithUTF8String:")
33+ nsString := objc.ID(objc.GetClass("NSString")).Send(sel, "\x00")
34+
35+ result := cstrings.NSStringToString(nsString)
36+ if result != "" {
37+ t.Errorf("expected empty string, got %q", result)
38+ }
39+ })
40+
41+ t.Run("non-NSString panics", func(t *testing.T) {
42+ defer func() {
43+ if r := recover(); r == nil {
44+ t.Error("expected panic for non-NSString ID")
45+ }
46+ }()
47+
48+ classNSNumber := objc.GetClass("NSNumber")
49+ sel := objc.RegisterName("numberWithInt:")
50+ nsNumber := objc.ID(classNSNumber).Send(sel, 42)
51+
52+ cstrings.NSStringToString(nsNumber)
53+ })
54+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close