feat(systemviews): add config route
- commit
- 5d04e809032eceb2c131a2dbdff8e1471fddd2a2
- author
- brice · · 2026-04-11 19:04 UTC
- committer
- brice · · 2026-04-12 04:52 UTC
- parent
- 681c467
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +399 -0
M internal/infrastructure/systemviewsbridge/client.go +41 -0
| @@ -23,6 +23,8 @@ type Client struct { | |||
| 23 | 23 | fetch Transport | |
| 24 | 24 | } | |
| 25 | 25 | ||
| 26 | + | var _ port.SystemviewConfigService = (*Client)(nil) | |
| 27 | + | ||
| 26 | 28 | var requestSeq atomic.Uint64 | |
| 27 | 29 | ||
| 28 | 30 | // NewClient creates a bridge client with native WebKit and fetch transports. | |
| @@ -81,6 +83,45 @@ func (c *Client) DeleteRange(ctx context.Context, rangeID string) error { | |||
| 81 | 83 | return err | |
| 82 | 84 | } | |
| 83 | 85 | ||
| 86 | + | func (c *Client) Current(ctx context.Context) (port.SystemviewConfigPayload, error) { | |
| 87 | + | return request[port.SystemviewConfigPayload](c, ctx, "/api/config", struct { | |
| 88 | + | RequestID string `json:"requestId"` | |
| 89 | + | }{RequestID: nextRequestID()}) | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | func (c *Client) Default(ctx context.Context) (port.SystemviewConfigPayload, error) { | |
| 93 | + | return request[port.SystemviewConfigPayload](c, ctx, "/api/config/default", struct { | |
| 94 | + | RequestID string `json:"requestId"` | |
| 95 | + | }{RequestID: nextRequestID()}) | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | func (c *Client) Save(ctx context.Context, cfg port.WebUIConfig) error { | |
| 99 | + | _, err := request[struct{}](c, ctx, "save_config", cfg) | |
| 100 | + | return err | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | func (c *Client) GetKeybindings(ctx context.Context) (any, error) { | |
| 104 | + | return request[any](c, ctx, "get_keybindings", struct { | |
| 105 | + | RequestID string `json:"requestId"` | |
| 106 | + | }{RequestID: nextRequestID()}) | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | func (c *Client) SetKeybinding(ctx context.Context, req port.SetKeybindingRequest) (any, error) { | |
| 110 | + | return request[any](c, ctx, "set_keybinding", req) | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func (c *Client) ResetKeybinding(ctx context.Context, req port.ResetKeybindingRequest) error { | |
| 114 | + | _, err := request[struct{}](c, ctx, "reset_keybinding", req) | |
| 115 | + | return err | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | func (c *Client) ResetAllKeybindings(ctx context.Context) error { | |
| 119 | + | _, err := request[struct{}](c, ctx, "reset_all_keybindings", struct { | |
| 120 | + | RequestID string `json:"requestId"` | |
| 121 | + | }{RequestID: nextRequestID()}) | |
| 122 | + | return err | |
| 123 | + | } | |
| 124 | + | ||
| 84 | 125 | func (c *Client) Analytics(ctx context.Context) (*entity.HistoryAnalytics, error) { | |
| 85 | 126 | return request[*entity.HistoryAnalytics](c, ctx, "history_analytics", struct { | |
| 86 | 127 | RequestID string `json:"requestId"` | |
| @@ -23,6 +23,8 @@ type Client struct { | |||||
| 23 | fetch Transport | 23 | fetch Transport | ||
| 24 | } | 24 | } | ||
| 25 | 25 | ||||
| 26 | + | var _ port.SystemviewConfigService = (*Client)(nil) | |||
| 27 | + | ||||
| 26 | var requestSeq atomic.Uint64 | 28 | var requestSeq atomic.Uint64 | ||
| 27 | 29 | ||||
| 28 | // NewClient creates a bridge client with native WebKit and fetch transports. | 30 | // NewClient creates a bridge client with native WebKit and fetch transports. | ||
| @@ -81,6 +83,45 @@ func (c *Client) DeleteRange(ctx context.Context, rangeID string) error { | |||||
| 81 | return err | 83 | return err | ||
| 82 | } | 84 | } | ||
| 83 | 85 | ||||
| 86 | + | func (c *Client) Current(ctx context.Context) (port.SystemviewConfigPayload, error) { | |||
| 87 | + | return request[port.SystemviewConfigPayload](c, ctx, "/api/config", struct { | |||
| 88 | + | RequestID string `json:"requestId"` | |||
| 89 | + | }{RequestID: nextRequestID()}) | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | func (c *Client) Default(ctx context.Context) (port.SystemviewConfigPayload, error) { | |||
| 93 | + | return request[port.SystemviewConfigPayload](c, ctx, "/api/config/default", struct { | |||
| 94 | + | RequestID string `json:"requestId"` | |||
| 95 | + | }{RequestID: nextRequestID()}) | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | func (c *Client) Save(ctx context.Context, cfg port.WebUIConfig) error { | |||
| 99 | + | _, err := request[struct{}](c, ctx, "save_config", cfg) | |||
| 100 | + | return err | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | func (c *Client) GetKeybindings(ctx context.Context) (any, error) { | |||
| 104 | + | return request[any](c, ctx, "get_keybindings", struct { | |||
| 105 | + | RequestID string `json:"requestId"` | |||
| 106 | + | }{RequestID: nextRequestID()}) | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | func (c *Client) SetKeybinding(ctx context.Context, req port.SetKeybindingRequest) (any, error) { | |||
| 110 | + | return request[any](c, ctx, "set_keybinding", req) | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | func (c *Client) ResetKeybinding(ctx context.Context, req port.ResetKeybindingRequest) error { | |||
| 114 | + | _, err := request[struct{}](c, ctx, "reset_keybinding", req) | |||
| 115 | + | return err | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | func (c *Client) ResetAllKeybindings(ctx context.Context) error { | |||
| 119 | + | _, err := request[struct{}](c, ctx, "reset_all_keybindings", struct { | |||
| 120 | + | RequestID string `json:"requestId"` | |||
| 121 | + | }{RequestID: nextRequestID()}) | |||
| 122 | + | return err | |||
| 123 | + | } | |||
| 124 | + | ||||
| 84 | func (c *Client) Analytics(ctx context.Context) (*entity.HistoryAnalytics, error) { | 125 | func (c *Client) Analytics(ctx context.Context) (*entity.HistoryAnalytics, error) { | ||
| 85 | return request[*entity.HistoryAnalytics](c, ctx, "history_analytics", struct { | 126 | return request[*entity.HistoryAnalytics](c, ctx, "history_analytics", struct { | ||
| 86 | RequestID string `json:"requestId"` | 127 | RequestID string `json:"requestId"` | ||
M internal/infrastructure/systemviewsbridge/client_test.go +135 -0
| @@ -221,6 +221,141 @@ func TestClientListTagsDecodesTags(t *testing.T) { | |||
| 221 | 221 | } | |
| 222 | 222 | } | |
| 223 | 223 | ||
| 224 | + | func TestClientCurrentAndDefaultDecodeConfigPayload(t *testing.T) { | |
| 225 | + | t.Parallel() | |
| 226 | + | ||
| 227 | + | resp := []byte(`{"requestId":"req-14","success":true,"data":{"engine_type":"webkit","default_search_engine":"DuckDuckGo"}}`) | |
| 228 | + | ||
| 229 | + | currentClient := NewClient(&fakeTransport{available: true, response: resp}, nil) | |
| 230 | + | current, err := currentClient.Current(context.Background()) | |
| 231 | + | if err != nil { | |
| 232 | + | t.Fatalf("Current() error = %v", err) | |
| 233 | + | } | |
| 234 | + | if current.EngineType != "webkit" || current.DefaultSearchEngine != "DuckDuckGo" { | |
| 235 | + | t.Fatalf("Current() = %+v", current) | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | var msg port.WebUIMessage | |
| 239 | + | if err := json.Unmarshal(currentClient.native.(*fakeTransport).last, &msg); err != nil { | |
| 240 | + | t.Fatalf("unmarshal sent envelope: %v", err) | |
| 241 | + | } | |
| 242 | + | if msg.Type != "/api/config" { | |
| 243 | + | t.Fatalf("sent type = %q, want %q", msg.Type, "/api/config") | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | defaultClient := NewClient(&fakeTransport{available: true, response: resp}, nil) | |
| 247 | + | defaultCfg, err := defaultClient.Default(context.Background()) | |
| 248 | + | if err != nil { | |
| 249 | + | t.Fatalf("Default() error = %v", err) | |
| 250 | + | } | |
| 251 | + | if defaultCfg.EngineType != "webkit" || defaultCfg.DefaultSearchEngine != "DuckDuckGo" { | |
| 252 | + | t.Fatalf("Default() = %+v", defaultCfg) | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | if err := json.Unmarshal(defaultClient.native.(*fakeTransport).last, &msg); err != nil { | |
| 256 | + | t.Fatalf("unmarshal sent envelope: %v", err) | |
| 257 | + | } | |
| 258 | + | if msg.Type != "/api/config/default" { | |
| 259 | + | t.Fatalf("sent type = %q, want %q", msg.Type, "/api/config/default") | |
| 260 | + | } | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | func TestClientConfigActionsUseExpectedMessageTypes(t *testing.T) { | |
| 264 | + | t.Parallel() | |
| 265 | + | ||
| 266 | + | tests := []struct { | |
| 267 | + | name string | |
| 268 | + | call func(*Client) error | |
| 269 | + | want string | |
| 270 | + | payload any | |
| 271 | + | }{ | |
| 272 | + | { | |
| 273 | + | name: "save config", | |
| 274 | + | call: func(client *Client) error { | |
| 275 | + | return client.Save(context.Background(), port.WebUIConfig{DefaultSearchEngine: "DuckDuckGo"}) | |
| 276 | + | }, | |
| 277 | + | want: "save_config", | |
| 278 | + | payload: port.WebUIConfig{DefaultSearchEngine: "DuckDuckGo"}, | |
| 279 | + | }, | |
| 280 | + | { | |
| 281 | + | name: "get keybindings", | |
| 282 | + | call: func(client *Client) error { | |
| 283 | + | got, err := client.GetKeybindings(context.Background()) | |
| 284 | + | if err != nil { | |
| 285 | + | return err | |
| 286 | + | } | |
| 287 | + | m, ok := got.(map[string]any) | |
| 288 | + | if !ok || len(m) == 0 { | |
| 289 | + | t.Fatalf("GetKeybindings() = %#v", got) | |
| 290 | + | } | |
| 291 | + | return nil | |
| 292 | + | }, | |
| 293 | + | want: "get_keybindings", | |
| 294 | + | }, | |
| 295 | + | { | |
| 296 | + | name: "set keybinding", | |
| 297 | + | call: func(client *Client) error { | |
| 298 | + | got, err := client.SetKeybinding(context.Background(), port.SetKeybindingRequest{RequestID: "req-1", Mode: "default", Action: "open", Keys: []string{"ctrl+o"}}) | |
| 299 | + | if err != nil { | |
| 300 | + | return err | |
| 301 | + | } | |
| 302 | + | m, ok := got.(map[string]any) | |
| 303 | + | if !ok || m["status"] != "success" { | |
| 304 | + | t.Fatalf("SetKeybinding() = %#v", got) | |
| 305 | + | } | |
| 306 | + | return nil | |
| 307 | + | }, | |
| 308 | + | want: "set_keybinding", | |
| 309 | + | payload: port.SetKeybindingRequest{RequestID: "req-1", Mode: "default", Action: "open", Keys: []string{"ctrl+o"}}, | |
| 310 | + | }, | |
| 311 | + | { | |
| 312 | + | name: "reset keybinding", | |
| 313 | + | call: func(client *Client) error { | |
| 314 | + | return client.ResetKeybinding(context.Background(), port.ResetKeybindingRequest{RequestID: "req-2", Mode: "default", Action: "open"}) | |
| 315 | + | }, | |
| 316 | + | want: "reset_keybinding", | |
| 317 | + | payload: port.ResetKeybindingRequest{RequestID: "req-2", Mode: "default", Action: "open"}, | |
| 318 | + | }, | |
| 319 | + | { | |
| 320 | + | name: "reset all keybindings", | |
| 321 | + | call: func(client *Client) error { | |
| 322 | + | return client.ResetAllKeybindings(context.Background()) | |
| 323 | + | }, | |
| 324 | + | want: "reset_all_keybindings", | |
| 325 | + | }, | |
| 326 | + | } | |
| 327 | + | ||
| 328 | + | for _, tt := range tests { | |
| 329 | + | t.Run(tt.name, func(t *testing.T) { | |
| 330 | + | t.Parallel() | |
| 331 | + | ||
| 332 | + | transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-15","success":true,"data":{"status":"success","conflicts":[]}}`)} | |
| 333 | + | if tt.name == "save config" || tt.name == "reset keybinding" || tt.name == "reset all keybindings" { | |
| 334 | + | transport.response = []byte(`{"requestId":"req-15","success":true}`) | |
| 335 | + | } | |
| 336 | + | if tt.name == "get keybindings" { | |
| 337 | + | transport.response = []byte(`{"requestId":"req-15","success":true,"data":{"groups":[{"mode":"default","display_name":"Default","bindings":[{"action":"open","description":"Open","keys":["ctrl+o"],"default_keys":["ctrl+o"],"is_custom":false}]}]}}`) | |
| 338 | + | } | |
| 339 | + | client := NewClient(transport, nil) | |
| 340 | + | ||
| 341 | + | if err := tt.call(client); err != nil { | |
| 342 | + | t.Fatalf("call() error = %v", err) | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | var msg port.WebUIMessage | |
| 346 | + | if err := json.Unmarshal(transport.last, &msg); err != nil { | |
| 347 | + | t.Fatalf("unmarshal sent envelope: %v", err) | |
| 348 | + | } | |
| 349 | + | if msg.Type != tt.want { | |
| 350 | + | t.Fatalf("sent type = %q, want %q", msg.Type, tt.want) | |
| 351 | + | } | |
| 352 | + | if tt.payload != nil && len(msg.Payload) == 0 { | |
| 353 | + | t.Fatal("sent payload was empty") | |
| 354 | + | } | |
| 355 | + | }) | |
| 356 | + | } | |
| 357 | + | } | |
| 358 | + | ||
| 224 | 359 | type fakeTransport struct { | |
| 225 | 360 | available bool | |
| 226 | 361 | called bool | |
| @@ -221,6 +221,141 @@ func TestClientListTagsDecodesTags(t *testing.T) { | |||||
| 221 | } | 221 | } | ||
| 222 | } | 222 | } | ||
| 223 | 223 | ||||
| 224 | + | func TestClientCurrentAndDefaultDecodeConfigPayload(t *testing.T) { | |||
| 225 | + | t.Parallel() | |||
| 226 | + | ||||
| 227 | + | resp := []byte(`{"requestId":"req-14","success":true,"data":{"engine_type":"webkit","default_search_engine":"DuckDuckGo"}}`) | |||
| 228 | + | ||||
| 229 | + | currentClient := NewClient(&fakeTransport{available: true, response: resp}, nil) | |||
| 230 | + | current, err := currentClient.Current(context.Background()) | |||
| 231 | + | if err != nil { | |||
| 232 | + | t.Fatalf("Current() error = %v", err) | |||
| 233 | + | } | |||
| 234 | + | if current.EngineType != "webkit" || current.DefaultSearchEngine != "DuckDuckGo" { | |||
| 235 | + | t.Fatalf("Current() = %+v", current) | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | var msg port.WebUIMessage | |||
| 239 | + | if err := json.Unmarshal(currentClient.native.(*fakeTransport).last, &msg); err != nil { | |||
| 240 | + | t.Fatalf("unmarshal sent envelope: %v", err) | |||
| 241 | + | } | |||
| 242 | + | if msg.Type != "/api/config" { | |||
| 243 | + | t.Fatalf("sent type = %q, want %q", msg.Type, "/api/config") | |||
| 244 | + | } | |||
| 245 | + | ||||
| 246 | + | defaultClient := NewClient(&fakeTransport{available: true, response: resp}, nil) | |||
| 247 | + | defaultCfg, err := defaultClient.Default(context.Background()) | |||
| 248 | + | if err != nil { | |||
| 249 | + | t.Fatalf("Default() error = %v", err) | |||
| 250 | + | } | |||
| 251 | + | if defaultCfg.EngineType != "webkit" || defaultCfg.DefaultSearchEngine != "DuckDuckGo" { | |||
| 252 | + | t.Fatalf("Default() = %+v", defaultCfg) | |||
| 253 | + | } | |||
| 254 | + | ||||
| 255 | + | if err := json.Unmarshal(defaultClient.native.(*fakeTransport).last, &msg); err != nil { | |||
| 256 | + | t.Fatalf("unmarshal sent envelope: %v", err) | |||
| 257 | + | } | |||
| 258 | + | if msg.Type != "/api/config/default" { | |||
| 259 | + | t.Fatalf("sent type = %q, want %q", msg.Type, "/api/config/default") | |||
| 260 | + | } | |||
| 261 | + | } | |||
| 262 | + | ||||
| 263 | + | func TestClientConfigActionsUseExpectedMessageTypes(t *testing.T) { | |||
| 264 | + | t.Parallel() | |||
| 265 | + | ||||
| 266 | + | tests := []struct { | |||
| 267 | + | name string | |||
| 268 | + | call func(*Client) error | |||
| 269 | + | want string | |||
| 270 | + | payload any | |||
| 271 | + | }{ | |||
| 272 | + | { | |||
| 273 | + | name: "save config", | |||
| 274 | + | call: func(client *Client) error { | |||
| 275 | + | return client.Save(context.Background(), port.WebUIConfig{DefaultSearchEngine: "DuckDuckGo"}) | |||
| 276 | + | }, | |||
| 277 | + | want: "save_config", | |||
| 278 | + | payload: port.WebUIConfig{DefaultSearchEngine: "DuckDuckGo"}, | |||
| 279 | + | }, | |||
| 280 | + | { | |||
| 281 | + | name: "get keybindings", | |||
| 282 | + | call: func(client *Client) error { | |||
| 283 | + | got, err := client.GetKeybindings(context.Background()) | |||
| 284 | + | if err != nil { | |||
| 285 | + | return err | |||
| 286 | + | } | |||
| 287 | + | m, ok := got.(map[string]any) | |||
| 288 | + | if !ok || len(m) == 0 { | |||
| 289 | + | t.Fatalf("GetKeybindings() = %#v", got) | |||
| 290 | + | } | |||
| 291 | + | return nil | |||
| 292 | + | }, | |||
| 293 | + | want: "get_keybindings", | |||
| 294 | + | }, | |||
| 295 | + | { | |||
| 296 | + | name: "set keybinding", | |||
| 297 | + | call: func(client *Client) error { | |||
| 298 | + | got, err := client.SetKeybinding(context.Background(), port.SetKeybindingRequest{RequestID: "req-1", Mode: "default", Action: "open", Keys: []string{"ctrl+o"}}) | |||
| 299 | + | if err != nil { | |||
| 300 | + | return err | |||
| 301 | + | } | |||
| 302 | + | m, ok := got.(map[string]any) | |||
| 303 | + | if !ok || m["status"] != "success" { | |||
| 304 | + | t.Fatalf("SetKeybinding() = %#v", got) | |||
| 305 | + | } | |||
| 306 | + | return nil | |||
| 307 | + | }, | |||
| 308 | + | want: "set_keybinding", | |||
| 309 | + | payload: port.SetKeybindingRequest{RequestID: "req-1", Mode: "default", Action: "open", Keys: []string{"ctrl+o"}}, | |||
| 310 | + | }, | |||
| 311 | + | { | |||
| 312 | + | name: "reset keybinding", | |||
| 313 | + | call: func(client *Client) error { | |||
| 314 | + | return client.ResetKeybinding(context.Background(), port.ResetKeybindingRequest{RequestID: "req-2", Mode: "default", Action: "open"}) | |||
| 315 | + | }, | |||
| 316 | + | want: "reset_keybinding", | |||
| 317 | + | payload: port.ResetKeybindingRequest{RequestID: "req-2", Mode: "default", Action: "open"}, | |||
| 318 | + | }, | |||
| 319 | + | { | |||
| 320 | + | name: "reset all keybindings", | |||
| 321 | + | call: func(client *Client) error { | |||
| 322 | + | return client.ResetAllKeybindings(context.Background()) | |||
| 323 | + | }, | |||
| 324 | + | want: "reset_all_keybindings", | |||
| 325 | + | }, | |||
| 326 | + | } | |||
| 327 | + | ||||
| 328 | + | for _, tt := range tests { | |||
| 329 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 330 | + | t.Parallel() | |||
| 331 | + | ||||
| 332 | + | transport := &fakeTransport{available: true, response: []byte(`{"requestId":"req-15","success":true,"data":{"status":"success","conflicts":[]}}`)} | |||
| 333 | + | if tt.name == "save config" || tt.name == "reset keybinding" || tt.name == "reset all keybindings" { | |||
| 334 | + | transport.response = []byte(`{"requestId":"req-15","success":true}`) | |||
| 335 | + | } | |||
| 336 | + | if tt.name == "get keybindings" { | |||
| 337 | + | transport.response = []byte(`{"requestId":"req-15","success":true,"data":{"groups":[{"mode":"default","display_name":"Default","bindings":[{"action":"open","description":"Open","keys":["ctrl+o"],"default_keys":["ctrl+o"],"is_custom":false}]}]}}`) | |||
| 338 | + | } | |||
| 339 | + | client := NewClient(transport, nil) | |||
| 340 | + | ||||
| 341 | + | if err := tt.call(client); err != nil { | |||
| 342 | + | t.Fatalf("call() error = %v", err) | |||
| 343 | + | } | |||
| 344 | + | ||||
| 345 | + | var msg port.WebUIMessage | |||
| 346 | + | if err := json.Unmarshal(transport.last, &msg); err != nil { | |||
| 347 | + | t.Fatalf("unmarshal sent envelope: %v", err) | |||
| 348 | + | } | |||
| 349 | + | if msg.Type != tt.want { | |||
| 350 | + | t.Fatalf("sent type = %q, want %q", msg.Type, tt.want) | |||
| 351 | + | } | |||
| 352 | + | if tt.payload != nil && len(msg.Payload) == 0 { | |||
| 353 | + | t.Fatal("sent payload was empty") | |||
| 354 | + | } | |||
| 355 | + | }) | |||
| 356 | + | } | |||
| 357 | + | } | |||
| 358 | + | ||||
| 224 | type fakeTransport struct { | 359 | type fakeTransport struct { | ||
| 225 | available bool | 360 | available bool | ||
| 226 | called bool | 361 | called bool | ||
M internal/ui/systemviews/app.go +34 -0
| @@ -12,6 +12,7 @@ type Dependencies struct { | |||
| 12 | 12 | DOM DOM | |
| 13 | 13 | Favorites port.SystemviewFavoritesService | |
| 14 | 14 | History port.SystemviewHistoryService | |
| 15 | + | Config port.SystemviewConfigService | |
| 15 | 16 | LocationURI string | |
| 16 | 17 | } | |
| 17 | 18 | ||
| @@ -22,6 +23,8 @@ type App struct { | |||
| 22 | 23 | favorites []*entity.Favorite | |
| 23 | 24 | folders []*entity.Folder | |
| 24 | 25 | tags []*entity.Tag | |
| 26 | + | config *port.SystemviewConfigPayload | |
| 27 | + | keybindings any | |
| 25 | 28 | renderedHTML string | |
| 26 | 29 | } | |
| 27 | 30 | ||
| @@ -103,11 +106,42 @@ func (a *App) LoadInitial(ctx context.Context) error { | |||
| 103 | 106 | a.tags = tags | |
| 104 | 107 | a.renderedHTML = favoritesHTML(favorites, folders, tags) | |
| 105 | 108 | return nil | |
| 109 | + | case RouteConfig: | |
| 110 | + | if a.deps.Config == nil { | |
| 111 | + | a.historyEntries = nil | |
| 112 | + | a.favorites = nil | |
| 113 | + | a.folders = nil | |
| 114 | + | a.tags = nil | |
| 115 | + | a.config = nil | |
| 116 | + | a.keybindings = nil | |
| 117 | + | a.renderedHTML = placeholderHTML(a.currentRoute) | |
| 118 | + | return nil | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | config, err := a.deps.Config.Current(ctx) | |
| 122 | + | if err != nil { | |
| 123 | + | return err | |
| 124 | + | } | |
| 125 | + | keybindings, err := a.deps.Config.GetKeybindings(ctx) | |
| 126 | + | if err != nil { | |
| 127 | + | return err | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | a.historyEntries = nil | |
| 131 | + | a.favorites = nil | |
| 132 | + | a.folders = nil | |
| 133 | + | a.tags = nil | |
| 134 | + | a.config = &config | |
| 135 | + | a.keybindings = keybindings | |
| 136 | + | a.renderedHTML = configHTML(config, keybindings) | |
| 137 | + | return nil | |
| 106 | 138 | default: | |
| 107 | 139 | a.historyEntries = nil | |
| 108 | 140 | a.favorites = nil | |
| 109 | 141 | a.folders = nil | |
| 110 | 142 | a.tags = nil | |
| 143 | + | a.config = nil | |
| 144 | + | a.keybindings = nil | |
| 111 | 145 | a.renderedHTML = placeholderHTML(a.currentRoute) | |
| 112 | 146 | return nil | |
| 113 | 147 | } | |
| @@ -12,6 +12,7 @@ type Dependencies struct { | |||||
| 12 | DOM DOM | 12 | DOM DOM | ||
| 13 | Favorites port.SystemviewFavoritesService | 13 | Favorites port.SystemviewFavoritesService | ||
| 14 | History port.SystemviewHistoryService | 14 | History port.SystemviewHistoryService | ||
| 15 | + | Config port.SystemviewConfigService | |||
| 15 | LocationURI string | 16 | LocationURI string | ||
| 16 | } | 17 | } | ||
| 17 | 18 | ||||
| @@ -22,6 +23,8 @@ type App struct { | |||||
| 22 | favorites []*entity.Favorite | 23 | favorites []*entity.Favorite | ||
| 23 | folders []*entity.Folder | 24 | folders []*entity.Folder | ||
| 24 | tags []*entity.Tag | 25 | tags []*entity.Tag | ||
| 26 | + | config *port.SystemviewConfigPayload | |||
| 27 | + | keybindings any | |||
| 25 | renderedHTML string | 28 | renderedHTML string | ||
| 26 | } | 29 | } | ||
| 27 | 30 | ||||
| @@ -103,11 +106,42 @@ func (a *App) LoadInitial(ctx context.Context) error { | |||||
| 103 | a.tags = tags | 106 | a.tags = tags | ||
| 104 | a.renderedHTML = favoritesHTML(favorites, folders, tags) | 107 | a.renderedHTML = favoritesHTML(favorites, folders, tags) | ||
| 105 | return nil | 108 | return nil | ||
| 109 | + | case RouteConfig: | |||
| 110 | + | if a.deps.Config == nil { | |||
| 111 | + | a.historyEntries = nil | |||
| 112 | + | a.favorites = nil | |||
| 113 | + | a.folders = nil | |||
| 114 | + | a.tags = nil | |||
| 115 | + | a.config = nil | |||
| 116 | + | a.keybindings = nil | |||
| 117 | + | a.renderedHTML = placeholderHTML(a.currentRoute) | |||
| 118 | + | return nil | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | config, err := a.deps.Config.Current(ctx) | |||
| 122 | + | if err != nil { | |||
| 123 | + | return err | |||
| 124 | + | } | |||
| 125 | + | keybindings, err := a.deps.Config.GetKeybindings(ctx) | |||
| 126 | + | if err != nil { | |||
| 127 | + | return err | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | a.historyEntries = nil | |||
| 131 | + | a.favorites = nil | |||
| 132 | + | a.folders = nil | |||
| 133 | + | a.tags = nil | |||
| 134 | + | a.config = &config | |||
| 135 | + | a.keybindings = keybindings | |||
| 136 | + | a.renderedHTML = configHTML(config, keybindings) | |||
| 137 | + | return nil | |||
| 106 | default: | 138 | default: | ||
| 107 | a.historyEntries = nil | 139 | a.historyEntries = nil | ||
| 108 | a.favorites = nil | 140 | a.favorites = nil | ||
| 109 | a.folders = nil | 141 | a.folders = nil | ||
| 110 | a.tags = nil | 142 | a.tags = nil | ||
| 143 | + | a.config = nil | |||
| 144 | + | a.keybindings = nil | |||
| 111 | a.renderedHTML = placeholderHTML(a.currentRoute) | 145 | a.renderedHTML = placeholderHTML(a.currentRoute) | ||
| 112 | return nil | 146 | return nil | ||
| 113 | } | 147 | } | ||
M internal/ui/systemviews/app_test.go +78 -0
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "testing" | |
| 6 | 6 | ||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |
| 7 | 8 | "github.com/bnema/dumber/internal/domain/entity" | |
| 8 | 9 | "github.com/stretchr/testify/assert" | |
| 9 | 10 | "github.com/stretchr/testify/require" | |
| @@ -102,6 +103,34 @@ func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) { | |||
| 102 | 103 | assert.True(t, favorites.calledTags) | |
| 103 | 104 | } | |
| 104 | 105 | ||
| 106 | + | func TestAppLoadInitialConfigRouteRendersData(t *testing.T) { | |
| 107 | + | t.Parallel() | |
| 108 | + | ||
| 109 | + | config := port.SystemviewConfigPayload{ | |
| 110 | + | EngineType: "webkit", | |
| 111 | + | DefaultSearchEngine: "DuckDuckGo", | |
| 112 | + | } | |
| 113 | + | service := &fakeConfigService{ | |
| 114 | + | current: config, | |
| 115 | + | keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{Mode: "default", DisplayName: "Default", Bindings: []port.KeybindingEntry{{Action: "open", Description: "Open", Keys: []string{"ctrl+o"}}}}}}, | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | app := NewApp(Dependencies{ | |
| 119 | + | Config: service, | |
| 120 | + | LocationURI: "dumb://config", | |
| 121 | + | }) | |
| 122 | + | ||
| 123 | + | require.NoError(t, app.LoadInitial(context.Background())) | |
| 124 | + | assert.Equal(t, RouteConfig, app.CurrentRoute()) | |
| 125 | + | require.NotNil(t, app.config) | |
| 126 | + | assert.Equal(t, "webkit", app.config.EngineType) | |
| 127 | + | assert.Contains(t, app.renderedHTML, "webkit") | |
| 128 | + | assert.Contains(t, app.renderedHTML, "DuckDuckGo") | |
| 129 | + | assert.Contains(t, app.renderedHTML, "1 group") | |
| 130 | + | assert.True(t, service.calledCurrent) | |
| 131 | + | assert.True(t, service.calledKeybindings) | |
| 132 | + | } | |
| 133 | + | ||
| 105 | 134 | type fakeDOM struct { | |
| 106 | 135 | mounted bool | |
| 107 | 136 | html string | |
| @@ -192,3 +221,52 @@ func (s *fakeFavoritesService) DeleteTag(context.Context, int64) error { return | |||
| 192 | 221 | func (s *fakeFavoritesService) AssignTag(context.Context, int64, int64) error { return nil } | |
| 193 | 222 | ||
| 194 | 223 | func (s *fakeFavoritesService) RemoveTag(context.Context, int64, int64) error { return nil } | |
| 224 | + | ||
| 225 | + | type fakeConfigService struct { | |
| 226 | + | calledCurrent bool | |
| 227 | + | calledDefault bool | |
| 228 | + | calledSave bool | |
| 229 | + | calledKeybindings bool | |
| 230 | + | calledSet bool | |
| 231 | + | calledReset bool | |
| 232 | + | calledResetAll bool | |
| 233 | + | ||
| 234 | + | current port.SystemviewConfigPayload | |
| 235 | + | defaultCfg port.SystemviewConfigPayload | |
| 236 | + | keybindings any | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | func (s *fakeConfigService) Current(context.Context) (port.SystemviewConfigPayload, error) { | |
| 240 | + | s.calledCurrent = true | |
| 241 | + | return s.current, nil | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | func (s *fakeConfigService) Default(context.Context) (port.SystemviewConfigPayload, error) { | |
| 245 | + | s.calledDefault = true | |
| 246 | + | return s.defaultCfg, nil | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | func (s *fakeConfigService) Save(context.Context, port.WebUIConfig) error { | |
| 250 | + | s.calledSave = true | |
| 251 | + | return nil | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | func (s *fakeConfigService) GetKeybindings(context.Context) (any, error) { | |
| 255 | + | s.calledKeybindings = true | |
| 256 | + | return s.keybindings, nil | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | func (s *fakeConfigService) SetKeybinding(context.Context, port.SetKeybindingRequest) (any, error) { | |
| 260 | + | s.calledSet = true | |
| 261 | + | return nil, nil | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | func (s *fakeConfigService) ResetKeybinding(context.Context, port.ResetKeybindingRequest) error { | |
| 265 | + | s.calledReset = true | |
| 266 | + | return nil | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | func (s *fakeConfigService) ResetAllKeybindings(context.Context) error { | |
| 270 | + | s.calledResetAll = true | |
| 271 | + | return nil | |
| 272 | + | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "testing" | 5 | "testing" | ||
| 6 | 6 | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 7 | "github.com/bnema/dumber/internal/domain/entity" | 8 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 8 | "github.com/stretchr/testify/assert" | 9 | "github.com/stretchr/testify/assert" | ||
| 9 | "github.com/stretchr/testify/require" | 10 | "github.com/stretchr/testify/require" | ||
| @@ -102,6 +103,34 @@ func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) { | |||||
| 102 | assert.True(t, favorites.calledTags) | 103 | assert.True(t, favorites.calledTags) | ||
| 103 | } | 104 | } | ||
| 104 | 105 | ||||
| 106 | + | func TestAppLoadInitialConfigRouteRendersData(t *testing.T) { | |||
| 107 | + | t.Parallel() | |||
| 108 | + | ||||
| 109 | + | config := port.SystemviewConfigPayload{ | |||
| 110 | + | EngineType: "webkit", | |||
| 111 | + | DefaultSearchEngine: "DuckDuckGo", | |||
| 112 | + | } | |||
| 113 | + | service := &fakeConfigService{ | |||
| 114 | + | current: config, | |||
| 115 | + | keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{Mode: "default", DisplayName: "Default", Bindings: []port.KeybindingEntry{{Action: "open", Description: "Open", Keys: []string{"ctrl+o"}}}}}}, | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | app := NewApp(Dependencies{ | |||
| 119 | + | Config: service, | |||
| 120 | + | LocationURI: "dumb://config", | |||
| 121 | + | }) | |||
| 122 | + | ||||
| 123 | + | require.NoError(t, app.LoadInitial(context.Background())) | |||
| 124 | + | assert.Equal(t, RouteConfig, app.CurrentRoute()) | |||
| 125 | + | require.NotNil(t, app.config) | |||
| 126 | + | assert.Equal(t, "webkit", app.config.EngineType) | |||
| 127 | + | assert.Contains(t, app.renderedHTML, "webkit") | |||
| 128 | + | assert.Contains(t, app.renderedHTML, "DuckDuckGo") | |||
| 129 | + | assert.Contains(t, app.renderedHTML, "1 group") | |||
| 130 | + | assert.True(t, service.calledCurrent) | |||
| 131 | + | assert.True(t, service.calledKeybindings) | |||
| 132 | + | } | |||
| 133 | + | ||||
| 105 | type fakeDOM struct { | 134 | type fakeDOM struct { | ||
| 106 | mounted bool | 135 | mounted bool | ||
| 107 | html string | 136 | html string | ||
| @@ -192,3 +221,52 @@ func (s *fakeFavoritesService) DeleteTag(context.Context, int64) error { return | |||||
| 192 | func (s *fakeFavoritesService) AssignTag(context.Context, int64, int64) error { return nil } | 221 | func (s *fakeFavoritesService) AssignTag(context.Context, int64, int64) error { return nil } | ||
| 193 | 222 | ||||
| 194 | func (s *fakeFavoritesService) RemoveTag(context.Context, int64, int64) error { return nil } | 223 | func (s *fakeFavoritesService) RemoveTag(context.Context, int64, int64) error { return nil } | ||
| 224 | + | ||||
| 225 | + | type fakeConfigService struct { | |||
| 226 | + | calledCurrent bool | |||
| 227 | + | calledDefault bool | |||
| 228 | + | calledSave bool | |||
| 229 | + | calledKeybindings bool | |||
| 230 | + | calledSet bool | |||
| 231 | + | calledReset bool | |||
| 232 | + | calledResetAll bool | |||
| 233 | + | ||||
| 234 | + | current port.SystemviewConfigPayload | |||
| 235 | + | defaultCfg port.SystemviewConfigPayload | |||
| 236 | + | keybindings any | |||
| 237 | + | } | |||
| 238 | + | ||||
| 239 | + | func (s *fakeConfigService) Current(context.Context) (port.SystemviewConfigPayload, error) { | |||
| 240 | + | s.calledCurrent = true | |||
| 241 | + | return s.current, nil | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | func (s *fakeConfigService) Default(context.Context) (port.SystemviewConfigPayload, error) { | |||
| 245 | + | s.calledDefault = true | |||
| 246 | + | return s.defaultCfg, nil | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | func (s *fakeConfigService) Save(context.Context, port.WebUIConfig) error { | |||
| 250 | + | s.calledSave = true | |||
| 251 | + | return nil | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | func (s *fakeConfigService) GetKeybindings(context.Context) (any, error) { | |||
| 255 | + | s.calledKeybindings = true | |||
| 256 | + | return s.keybindings, nil | |||
| 257 | + | } | |||
| 258 | + | ||||
| 259 | + | func (s *fakeConfigService) SetKeybinding(context.Context, port.SetKeybindingRequest) (any, error) { | |||
| 260 | + | s.calledSet = true | |||
| 261 | + | return nil, nil | |||
| 262 | + | } | |||
| 263 | + | ||||
| 264 | + | func (s *fakeConfigService) ResetKeybinding(context.Context, port.ResetKeybindingRequest) error { | |||
| 265 | + | s.calledReset = true | |||
| 266 | + | return nil | |||
| 267 | + | } | |||
| 268 | + | ||||
| 269 | + | func (s *fakeConfigService) ResetAllKeybindings(context.Context) error { | |||
| 270 | + | s.calledResetAll = true | |||
| 271 | + | return nil | |||
| 272 | + | } | |||
A internal/ui/systemviews/config_view.go +111 -0
| @@ -0,0 +1,111 @@ | |||
| 1 | + | package systemviews | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "html" | |
| 6 | + | "strings" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func configHTML(cfg port.SystemviewConfigPayload, keybindings any) string { | |
| 12 | + | groups, bindings, loaded := summarizeKeybindings(keybindings) | |
| 13 | + | status := "not loaded" | |
| 14 | + | if loaded { | |
| 15 | + | status = "loaded" | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | return fmt.Sprintf(`<!doctype html> | |
| 19 | + | <html lang="en"> | |
| 20 | + | <head> | |
| 21 | + | <meta charset="utf-8"> | |
| 22 | + | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| 23 | + | <title>systemviews config</title> | |
| 24 | + | </head> | |
| 25 | + | <body> | |
| 26 | + | <main id="app" data-route="config"> | |
| 27 | + | <h1>Config</h1> | |
| 28 | + | <p>Engine: %s</p> | |
| 29 | + | <p>Default search engine: %s</p> | |
| 30 | + | <p>Keybindings: %s (%d %s, %d %s)</p> | |
| 31 | + | </main> | |
| 32 | + | </body> | |
| 33 | + | </html>`, | |
| 34 | + | html.EscapeString(cfg.EngineType), | |
| 35 | + | html.EscapeString(cfg.DefaultSearchEngine), | |
| 36 | + | status, | |
| 37 | + | groups, plural(groups, "group"), | |
| 38 | + | bindings, plural(bindings, "binding"), | |
| 39 | + | ) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func summarizeKeybindings(keybindings any) (groups int, bindings int, loaded bool) { | |
| 43 | + | switch v := keybindings.(type) { | |
| 44 | + | case nil: | |
| 45 | + | return 0, 0, false | |
| 46 | + | case port.KeybindingsConfig: | |
| 47 | + | return countKeybindingGroups(v.Groups), countKeybindingBindings(v.Groups), true | |
| 48 | + | case *port.KeybindingsConfig: | |
| 49 | + | if v == nil { | |
| 50 | + | return 0, 0, false | |
| 51 | + | } | |
| 52 | + | return countKeybindingGroups(v.Groups), countKeybindingBindings(v.Groups), true | |
| 53 | + | case map[string]any: | |
| 54 | + | return countBridgeKeybindings(v) | |
| 55 | + | default: | |
| 56 | + | return 1, 0, true | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func countBridgeKeybindings(data map[string]any) (groups int, bindings int, loaded bool) { | |
| 61 | + | groupValue, ok := data["groups"] | |
| 62 | + | if !ok { | |
| 63 | + | return 1, 0, true | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | groupList, ok := groupValue.([]any) | |
| 67 | + | if !ok { | |
| 68 | + | return 1, 0, true | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | for _, group := range groupList { | |
| 72 | + | groupMap, ok := group.(map[string]any) | |
| 73 | + | if !ok { | |
| 74 | + | continue | |
| 75 | + | } | |
| 76 | + | groups++ | |
| 77 | + | if bindingValue, ok := groupMap["bindings"]; ok { | |
| 78 | + | if bindingList, ok := bindingValue.([]any); ok { | |
| 79 | + | bindings += len(bindingList) | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | return groups, bindings, true | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func countKeybindingGroups(groups []port.KeybindingGroup) int { | |
| 88 | + | count := 0 | |
| 89 | + | for _, group := range groups { | |
| 90 | + | if group.Mode == "" && group.DisplayName == "" && len(group.Bindings) == 0 { | |
| 91 | + | continue | |
| 92 | + | } | |
| 93 | + | count++ | |
| 94 | + | } | |
| 95 | + | return count | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | func countKeybindingBindings(groups []port.KeybindingGroup) int { | |
| 99 | + | count := 0 | |
| 100 | + | for _, group := range groups { | |
| 101 | + | count += len(group.Bindings) | |
| 102 | + | } | |
| 103 | + | return count | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | func plural(count int, singular string) string { | |
| 107 | + | if count == 1 { | |
| 108 | + | return singular | |
| 109 | + | } | |
| 110 | + | return strings.TrimSpace(singular + "s") | |
| 111 | + | } | |
| @@ -0,0 +1,111 @@ | |||||
| 1 | + | package systemviews | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "html" | |||
| 6 | + | "strings" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func configHTML(cfg port.SystemviewConfigPayload, keybindings any) string { | |||
| 12 | + | groups, bindings, loaded := summarizeKeybindings(keybindings) | |||
| 13 | + | status := "not loaded" | |||
| 14 | + | if loaded { | |||
| 15 | + | status = "loaded" | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | return fmt.Sprintf(`<!doctype html> | |||
| 19 | + | <html lang="en"> | |||
| 20 | + | <head> | |||
| 21 | + | <meta charset="utf-8"> | |||
| 22 | + | <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| 23 | + | <title>systemviews config</title> | |||
| 24 | + | </head> | |||
| 25 | + | <body> | |||
| 26 | + | <main id="app" data-route="config"> | |||
| 27 | + | <h1>Config</h1> | |||
| 28 | + | <p>Engine: %s</p> | |||
| 29 | + | <p>Default search engine: %s</p> | |||
| 30 | + | <p>Keybindings: %s (%d %s, %d %s)</p> | |||
| 31 | + | </main> | |||
| 32 | + | </body> | |||
| 33 | + | </html>`, | |||
| 34 | + | html.EscapeString(cfg.EngineType), | |||
| 35 | + | html.EscapeString(cfg.DefaultSearchEngine), | |||
| 36 | + | status, | |||
| 37 | + | groups, plural(groups, "group"), | |||
| 38 | + | bindings, plural(bindings, "binding"), | |||
| 39 | + | ) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func summarizeKeybindings(keybindings any) (groups int, bindings int, loaded bool) { | |||
| 43 | + | switch v := keybindings.(type) { | |||
| 44 | + | case nil: | |||
| 45 | + | return 0, 0, false | |||
| 46 | + | case port.KeybindingsConfig: | |||
| 47 | + | return countKeybindingGroups(v.Groups), countKeybindingBindings(v.Groups), true | |||
| 48 | + | case *port.KeybindingsConfig: | |||
| 49 | + | if v == nil { | |||
| 50 | + | return 0, 0, false | |||
| 51 | + | } | |||
| 52 | + | return countKeybindingGroups(v.Groups), countKeybindingBindings(v.Groups), true | |||
| 53 | + | case map[string]any: | |||
| 54 | + | return countBridgeKeybindings(v) | |||
| 55 | + | default: | |||
| 56 | + | return 1, 0, true | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func countBridgeKeybindings(data map[string]any) (groups int, bindings int, loaded bool) { | |||
| 61 | + | groupValue, ok := data["groups"] | |||
| 62 | + | if !ok { | |||
| 63 | + | return 1, 0, true | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | groupList, ok := groupValue.([]any) | |||
| 67 | + | if !ok { | |||
| 68 | + | return 1, 0, true | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | for _, group := range groupList { | |||
| 72 | + | groupMap, ok := group.(map[string]any) | |||
| 73 | + | if !ok { | |||
| 74 | + | continue | |||
| 75 | + | } | |||
| 76 | + | groups++ | |||
| 77 | + | if bindingValue, ok := groupMap["bindings"]; ok { | |||
| 78 | + | if bindingList, ok := bindingValue.([]any); ok { | |||
| 79 | + | bindings += len(bindingList) | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | return groups, bindings, true | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | func countKeybindingGroups(groups []port.KeybindingGroup) int { | |||
| 88 | + | count := 0 | |||
| 89 | + | for _, group := range groups { | |||
| 90 | + | if group.Mode == "" && group.DisplayName == "" && len(group.Bindings) == 0 { | |||
| 91 | + | continue | |||
| 92 | + | } | |||
| 93 | + | count++ | |||
| 94 | + | } | |||
| 95 | + | return count | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | func countKeybindingBindings(groups []port.KeybindingGroup) int { | |||
| 99 | + | count := 0 | |||
| 100 | + | for _, group := range groups { | |||
| 101 | + | count += len(group.Bindings) | |||
| 102 | + | } | |||
| 103 | + | return count | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | func plural(count int, singular string) string { | |||
| 107 | + | if count == 1 { | |||
| 108 | + | return singular | |||
| 109 | + | } | |||
| 110 | + | return strings.TrimSpace(singular + "s") | |||
| 111 | + | } | |||