返回 DeepSeek-Reasonix
native_host_rpc.go
根目录 / desktop / native_host_rpc.go
1 package main
2
3 import (
4 "context"
5 "log/slog"
6 "time"
7
8 "reasonix/desktop/internal/hostrpc"
9 )
10
11 // rpcNativeHost implements nativeHost over host/* requests to the Electron
12 // shell. Fire-and-forget window operations use a short timeout so a wedged
13 // shell can never block a business goroutine on the Go side.
14 type rpcNativeHost struct {
15 server *hostrpc.Server
16 }
17
18 var _ nativeHost = rpcNativeHost{}
19
20 const (
21 rpcHostWindowTimeout = 5 * time.Second
22 rpcHostDialogTimeout = 24 * time.Hour
23 )
24
25 type hostValueResult struct {
26 Value bool `json:"value"`
27 }
28
29 type hostPathResult struct {
30 Path string `json:"path"`
31 }
32
33 type hostPathsResult struct {
34 Paths []string `json:"paths"`
35 }
36
37 type hostButtonResult struct {
38 Button string `json:"button"`
39 }
40
41 type hostScreenList struct {
42 Screens []struct {
43 X int `json:"x"`
44 Y int `json:"y"`
45 Width int `json:"width"`
46 Height int `json:"height"`
47 Scale float64 `json:"scale"`
48 Primary bool `json:"primary"`
49 } `json:"screens"`
50 }
51
52 type hostFileFilter struct {
53 DisplayName string `json:"displayName"`
54 Pattern string `json:"pattern"`
55 }
56
57 type hostDialogParams struct {
58 Title string `json:"title,omitempty"`
59 DefaultDirectory string `json:"defaultDirectory,omitempty"`
60 DefaultFilename string `json:"defaultFilename,omitempty"`
61 Filters []hostFileFilter `json:"filters"`
62 Multiple bool `json:"multiple,omitempty"`
63 }
64
65 func hostDialogParamsFrom(opts nativeDialogOptions) hostDialogParams {
66 p := hostDialogParams{
67 Title: opts.Title,
68 DefaultDirectory: opts.DefaultDirectory,
69 DefaultFilename: opts.DefaultFilename,
70 Filters: []hostFileFilter{},
71 }
72 for _, f := range opts.Filters {
73 p.Filters = append(p.Filters, hostFileFilter(f))
74 }
75 return p
76 }
77
78 func (h rpcNativeHost) call(ctx context.Context, method string, params any, result any, timeout time.Duration) error {
79 if ctx == nil {
80 ctx = context.Background()
81 }
82 ctx, cancel := context.WithTimeout(ctx, timeout)
83 defer cancel()
84 err := h.server.Request(ctx, method, params, result)
85 if err != nil {
86 slog.Warn("desktop host: native call failed", "method", method, "err", err)
87 }
88 return err
89 }
90
91 func (h rpcNativeHost) fire(ctx context.Context, method string, params any) {
92 _ = h.call(ctx, method, params, nil, rpcHostWindowTimeout)
93 }
94
95 func (h rpcNativeHost) ShowWindow(ctx context.Context) {
96 h.fire(ctx, "host/window.show", map[string]string{"reason": "service"})
97 }
98 func (h rpcNativeHost) ShowApplication(ctx context.Context) {
99 h.fire(ctx, "host/window.show", map[string]string{"reason": "application"})
100 }
101 func (h rpcNativeHost) HideWindow(ctx context.Context) { h.fire(ctx, "host/window.hide", struct{}{}) }
102 func (h rpcNativeHost) HideApplication(ctx context.Context) { h.fire(ctx, "host/app.hide", struct{}{}) }
103 func (h rpcNativeHost) MaximiseWindow(ctx context.Context) {
104 h.fire(ctx, "host/window.maximise", struct{}{})
105 }
106 func (h rpcNativeHost) UnmaximiseWindow(ctx context.Context) {
107 h.fire(ctx, "host/window.unmaximise", struct{}{})
108 }
109 func (h rpcNativeHost) MinimiseWindow(ctx context.Context) {
110 h.fire(ctx, "host/window.minimise", struct{}{})
111 }
112 func (h rpcNativeHost) UnminimiseWindow(ctx context.Context) {
113 h.fire(ctx, "host/window.unminimise", struct{}{})
114 }
115 func (h rpcNativeHost) ToggleMaximiseWindow(ctx context.Context) {
116 h.fire(ctx, "host/window.toggleMaximise", struct{}{})
117 }
118 func (h rpcNativeHost) CenterWindow(ctx context.Context) {
119 h.fire(ctx, "host/window.center", struct{}{})
120 }
121
122 func (h rpcNativeHost) WindowIsMaximised(ctx context.Context) bool {
123 var out hostValueResult
124 _ = h.call(ctx, "host/window.isMaximised", struct{}{}, &out, rpcHostWindowTimeout)
125 return out.Value
126 }
127
128 func (h rpcNativeHost) WindowIsMinimised(ctx context.Context) bool {
129 var out hostValueResult
130 _ = h.call(ctx, "host/window.isMinimised", struct{}{}, &out, rpcHostWindowTimeout)
131 return out.Value
132 }
133
134 func (h rpcNativeHost) SetWindowPosition(ctx context.Context, x, y int) {
135 h.fire(ctx, "host/window.setPosition", map[string]int{"x": x, "y": y})
136 }
137
138 func (h rpcNativeHost) SetWindowTitle(ctx context.Context, title string) {
139 h.fire(ctx, "host/window.setTitle", map[string]string{"title": title})
140 }
141
142 func (h rpcNativeHost) Screens(ctx context.Context) ([]nativeScreen, error) {
143 var out hostScreenList
144 if err := h.call(ctx, "host/screen.list", struct{}{}, &out, rpcHostWindowTimeout); err != nil {
145 return nil, err
146 }
147 screens := make([]nativeScreen, 0, len(out.Screens))
148 for _, s := range out.Screens {
149 screens = append(screens, nativeScreen{Width: s.Width, Height: s.Height, Primary: s.Primary, Scale: s.Scale})
150 }
151 return screens, nil
152 }
153
154 func (h rpcNativeHost) OpenDirectoryDialog(ctx context.Context, opts nativeDialogOptions) (string, error) {
155 var out hostPathResult
156 err := h.call(ctx, "host/dialog.openDirectory", hostDialogParamsFrom(opts), &out, rpcHostDialogTimeout)
157 return out.Path, err
158 }
159
160 func (h rpcNativeHost) OpenFileDialog(ctx context.Context, opts nativeDialogOptions) (string, error) {
161 var out hostPathsResult
162 err := h.call(ctx, "host/dialog.openFile", hostDialogParamsFrom(opts), &out, rpcHostDialogTimeout)
163 if err != nil || len(out.Paths) == 0 {
164 return "", err
165 }
166 return out.Paths[0], nil
167 }
168
169 func (h rpcNativeHost) SaveFileDialog(ctx context.Context, opts nativeDialogOptions) (string, error) {
170 var out hostPathResult
171 err := h.call(ctx, "host/dialog.saveFile", hostDialogParamsFrom(opts), &out, rpcHostDialogTimeout)
172 return out.Path, err
173 }
174
175 func (h rpcNativeHost) MessageDialog(ctx context.Context, opts nativeMessageOptions) (string, error) {
176 buttons := opts.Buttons
177 if buttons == nil {
178 buttons = []string{}
179 }
180 var out hostButtonResult
181 err := h.call(ctx, "host/dialog.message", map[string]any{
182 "type": string(opts.Type),
183 "title": opts.Title,
184 "message": opts.Message,
185 "buttons": buttons,
186 "defaultButton": opts.DefaultButton,
187 "cancelButton": opts.CancelButton,
188 }, &out, rpcHostDialogTimeout)
189 return out.Button, err
190 }
191
192 func (h rpcNativeHost) OpenExternal(ctx context.Context, url string) {
193 h.fire(ctx, "host/shell.openExternal", map[string]string{"url": url})
194 }
195
196 func (h rpcNativeHost) Quit(ctx context.Context) { h.fire(ctx, "host/app.quit", struct{}{}) }
197
198 func (h rpcNativeHost) OpenDevTools(ctx context.Context) {
199 h.fire(ctx, "host/devtools.toggle", struct{}{})
200 }
201
201 lines GO