arcane-os 0.67.0 → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +9 -4
- package/browser-runtime/ai/browser-speech-providers.mjs +5 -3
- package/docs/reference/README.md +7 -0
- package/docs/reference/ai/browser-speech.md +49 -9
- package/docs/reference/availability-and-normalization.md +1 -0
- package/docs/reference/core-native-packaging.md +41 -0
- package/docs/reference/core-runtime.md +14 -0
- package/docs/reference/inventory/package-api.json +55 -13
- package/docs/reference/inventory/runtime-components.json +2 -1
- package/docs/reference/inventory/runtime-modules.json +3 -3
- package/docs/reference/runtime-components.md +64 -5
- package/docs/reference/runtime-modules.md +67 -20
- package/docs/reference/sdk-api.md +197 -7
- package/package.json +1 -1
- package/runtime/arcane/components/theme-switcher.html +112 -13
- package/runtime/arcane/modules/AI.js +5 -4
- package/runtime/arcane/modules/AIProviderRuntime.js +7 -3
- package/runtime/arcane/modules/PreferenceStore.js +18 -13
- package/runtime/arcane/modules/SystemAppearance.js +24 -6
- package/schemas/arcane-app.schema.json +10 -0
- package/src/app-descriptor.mjs +23 -1
- package/src/core/hosts/windows/ArcaneHost.cs +46 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +21 -0
- package/src/core/runtime.mjs +15 -1
- package/src/native/portable-provider.mjs +1 -0
|
@@ -1,11 +1,62 @@
|
|
|
1
1
|
<style>
|
|
2
|
-
:host{
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
:host {
|
|
3
|
+
display:inline-block;
|
|
4
|
+
min-inline-size:0;
|
|
5
|
+
max-inline-size:100%;
|
|
6
|
+
color:inherit;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.switcher {
|
|
10
|
+
display:flex;
|
|
11
|
+
flex-wrap:wrap;
|
|
12
|
+
align-items:center;
|
|
13
|
+
gap:.15rem;
|
|
14
|
+
box-sizing:border-box;
|
|
15
|
+
max-inline-size:100%;
|
|
16
|
+
padding:.2rem;
|
|
17
|
+
border:1px solid var(--app-bar-border,var(--border-color,rgba(255,255,255,.25)));
|
|
18
|
+
border-radius:1.6rem;
|
|
19
|
+
background:color-mix(in srgb,currentColor 8%,transparent);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
button {
|
|
23
|
+
appearance:none;
|
|
24
|
+
flex:1 0 auto;
|
|
25
|
+
box-sizing:border-box;
|
|
26
|
+
min-inline-size:2.75rem;
|
|
27
|
+
max-inline-size:100%;
|
|
28
|
+
min-block-size:2.75rem;
|
|
29
|
+
padding:.5rem .75rem;
|
|
30
|
+
border:0;
|
|
31
|
+
border-radius:1.4rem;
|
|
32
|
+
background:transparent;
|
|
33
|
+
color:inherit;
|
|
34
|
+
cursor:pointer;
|
|
35
|
+
font:700 .875rem/1.25 system-ui,sans-serif;
|
|
36
|
+
text-align:center;
|
|
37
|
+
white-space:normal;
|
|
38
|
+
overflow-wrap:anywhere;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
button:hover:not(:disabled):not([aria-pressed="true"]) {
|
|
42
|
+
background:color-mix(in srgb,currentColor 14%,transparent);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
button[aria-pressed="true"] {
|
|
46
|
+
background:var(--app-bar-accent,var(--secondary-color,rgb(196,154,82)));
|
|
47
|
+
color:var(--app-bar-active-text,var(--primary-color,rgb(23,34,56)));
|
|
48
|
+
box-shadow:inset 0 0 0 .1rem currentColor;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
button:disabled {
|
|
52
|
+
cursor:not-allowed;
|
|
53
|
+
opacity:.55;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
button:focus-visible {
|
|
57
|
+
outline:.18rem solid var(--focus-color,currentColor);
|
|
58
|
+
outline-offset:.12rem;
|
|
59
|
+
}
|
|
9
60
|
</style>
|
|
10
61
|
<div class="switcher" role="group" aria-label="Color scheme">
|
|
11
62
|
<button type="button" data-scheme="system" aria-pressed="false" title="Use device theme">Auto</button>
|
|
@@ -16,31 +67,79 @@
|
|
|
16
67
|
<script type="module">
|
|
17
68
|
const host=this;
|
|
18
69
|
const root=host.shadowRoot;
|
|
70
|
+
const group=root.querySelector('.switcher');
|
|
19
71
|
const buttons=Array.from(root.querySelectorAll('[data-scheme]'));
|
|
72
|
+
let modes=buttons.map(function scheme(button){return button.dataset.scheme;});
|
|
73
|
+
let labels=Object.fromEntries(buttons.map(function label(button){
|
|
74
|
+
return [button.dataset.scheme,button.textContent];
|
|
75
|
+
}));
|
|
76
|
+
let busy=false;
|
|
77
|
+
|
|
78
|
+
host.configure=configure;
|
|
79
|
+
configure({
|
|
80
|
+
modes:host.dataset.modes===undefined?modes:host.dataset.modes.split(/\s+/u).filter(Boolean),
|
|
81
|
+
labels:host.dataset.systemLabel===undefined?{}:{system:host.dataset.systemLabel}
|
|
82
|
+
});
|
|
83
|
+
|
|
20
84
|
const {default:ThemeManager}=await import('../modules/ThemeManager.js');
|
|
21
85
|
const manager=new ThemeManager();
|
|
22
86
|
|
|
23
87
|
host.setMode=setMode;
|
|
24
88
|
host.refresh=refresh;
|
|
25
|
-
|
|
89
|
+
group.addEventListener('click',async function selectScheme(event){
|
|
26
90
|
const button=event.target.closest('[data-scheme]');
|
|
27
|
-
if(!button) return;
|
|
91
|
+
if(!button||button.disabled) return;
|
|
28
92
|
setBusy(true);
|
|
29
93
|
try{
|
|
30
94
|
const state=button.dataset.scheme==='custom'?await manager.activateCustom():await manager.setScheme(button.dataset.scheme);
|
|
31
95
|
setMode(state.mode);setCustomAvailable(Boolean(state.theme));
|
|
32
96
|
}finally{setBusy(false);setCustomAvailable(Boolean(manager.customTheme));}
|
|
33
97
|
});
|
|
34
|
-
globalThis.addEventListener('arcane-theme-change',()
|
|
98
|
+
globalThis.addEventListener('arcane-theme-change',function themeChanged(){
|
|
99
|
+
refresh().catch(function refreshRejected(){});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function configure(options={}){
|
|
103
|
+
const nextModes=options.modes===undefined?modes:[...options.modes];
|
|
104
|
+
const nextLabels={...labels,...options.labels};
|
|
105
|
+
const visible=[];
|
|
106
|
+
for(const mode of nextModes){
|
|
107
|
+
const button=buttons.find(function matchingScheme(candidate){return candidate.dataset.scheme===mode;});
|
|
108
|
+
if(!button) throw new TypeError(`Unknown theme-switcher mode: ${mode}`);
|
|
109
|
+
if(visible.includes(button)) throw new TypeError(`Repeated theme-switcher mode: ${mode}`);
|
|
110
|
+
visible.push(button);
|
|
111
|
+
}
|
|
112
|
+
for(const button of buttons){
|
|
113
|
+
const label=nextLabels[button.dataset.scheme];
|
|
114
|
+
if(typeof label!=='string') throw new TypeError('Theme-switcher labels must be strings.');
|
|
115
|
+
}
|
|
116
|
+
modes=nextModes;
|
|
117
|
+
labels=nextLabels;
|
|
118
|
+
for(const button of buttons) button.textContent=labels[button.dataset.scheme];
|
|
119
|
+
const reordered=group.children.length!==visible.length||visible.some(function moved(button,index){
|
|
120
|
+
return group.children[index]!==button;
|
|
121
|
+
});
|
|
122
|
+
if(reordered){
|
|
123
|
+
const focused=root.activeElement;
|
|
124
|
+
group.replaceChildren(...visible);
|
|
125
|
+
if(visible.includes(focused)&&!focused.disabled) focused.focus({preventScroll:true});
|
|
126
|
+
}
|
|
127
|
+
return {modes:[...modes],labels:{...labels}};
|
|
128
|
+
}
|
|
35
129
|
|
|
36
130
|
function setMode(mode='system'){
|
|
37
131
|
for(const button of buttons) button.setAttribute('aria-pressed',String(button.dataset.scheme===mode));
|
|
38
132
|
host.dataset.mode=mode;
|
|
39
133
|
return mode;
|
|
40
134
|
}
|
|
41
|
-
function setBusy(value){
|
|
42
|
-
|
|
135
|
+
function setBusy(value){
|
|
136
|
+
busy=Boolean(value);
|
|
137
|
+
for(const button of buttons) button.disabled=busy;
|
|
138
|
+
}
|
|
139
|
+
function setCustomAvailable(value){
|
|
140
|
+
buttons.find(function customScheme(button){return button.dataset.scheme==='custom';}).disabled=busy||!value;
|
|
141
|
+
}
|
|
43
142
|
async function refresh(){const state=await manager.load();setMode(state.mode);setCustomAvailable(Boolean(state.theme));return state;}
|
|
44
143
|
|
|
45
|
-
refresh().catch(()
|
|
144
|
+
refresh().catch(function refreshRejected(){setMode('system');});
|
|
46
145
|
</script>
|
|
@@ -6336,7 +6336,7 @@ class AI {
|
|
|
6336
6336
|
return result;
|
|
6337
6337
|
}
|
|
6338
6338
|
|
|
6339
|
-
prepareTTS({parts, storage = null, identity = null, signal = null, onState = null} = {}) {
|
|
6339
|
+
prepareTTS({parts, textFormat, storage = null, identity = null, signal = null, onState = null} = {}) {
|
|
6340
6340
|
if(this.#providerSpeechModel()?.speech?.playback==='native'){
|
|
6341
6341
|
const error=new Error('Native browser speech has no audio file to prepare or store; use prepareTTSPlayback.');
|
|
6342
6342
|
error.code='ARCANE_AI_TTS_AUDIO_EXPORT_UNAVAILABLE';
|
|
@@ -6384,7 +6384,7 @@ class AI {
|
|
|
6384
6384
|
if(!is.finite(pauseAfterMs) || pauseAfterMs < 0) {
|
|
6385
6385
|
throw new RangeError('Prepared speech pauses must be nonnegative milliseconds.');
|
|
6386
6386
|
}
|
|
6387
|
-
const cleaned = stripSpeechFormatting(source.input);
|
|
6387
|
+
const cleaned = textFormat === 'plain' ? source.input : stripSpeechFormatting(source.input);
|
|
6388
6388
|
const {segments: outputs} = this.#segmentSpeechText(cleaned, true, segmentation);
|
|
6389
6389
|
for(const [index, input] of outputs.entries()) {
|
|
6390
6390
|
if(!input.trim()) continue;
|
|
@@ -7175,6 +7175,7 @@ class AI {
|
|
|
7175
7175
|
'model',
|
|
7176
7176
|
'voice',
|
|
7177
7177
|
'input',
|
|
7178
|
+
'textFormat',
|
|
7178
7179
|
'responseFormat',
|
|
7179
7180
|
'speed'
|
|
7180
7181
|
]);
|
|
@@ -7183,7 +7184,7 @@ class AI {
|
|
|
7183
7184
|
||!acceptedKeys.has(key)
|
|
7184
7185
|
||!Object.hasOwn(descriptors[key],'value')){
|
|
7185
7186
|
const error=new TypeError(
|
|
7186
|
-
'AI.fetchTTS accepts only model, voice, input, responseFormat, and speed data properties.'
|
|
7187
|
+
'AI.fetchTTS accepts only model, voice, input, textFormat, responseFormat, and speed data properties.'
|
|
7187
7188
|
);
|
|
7188
7189
|
error.code='ARCANE_AI_TTS_REQUEST_INVALID';
|
|
7189
7190
|
throw error;
|
|
@@ -7208,7 +7209,7 @@ class AI {
|
|
|
7208
7209
|
error.code='ARCANE_AI_TTS_INPUT_INVALID';
|
|
7209
7210
|
throw error;
|
|
7210
7211
|
}
|
|
7211
|
-
const input=speechInputPrepared===true
|
|
7212
|
+
const input=speechInputPrepared===true||descriptors.textFormat?.value==='plain'
|
|
7212
7213
|
?suppliedInput
|
|
7213
7214
|
:stripSpeechFormatting(suppliedInput);
|
|
7214
7215
|
const selection=this.#providerRuntime.selection('tts');
|
|
@@ -3386,15 +3386,19 @@ export class AIProviderRuntime {
|
|
|
3386
3386
|
if (options.signal?.aborted) {
|
|
3387
3387
|
return Promise.reject(normalizedAbort());
|
|
3388
3388
|
}
|
|
3389
|
-
if (role === 'tts' && !queued
|
|
3389
|
+
if (role === 'tts' && !queued) {
|
|
3390
3390
|
const payload = options.payload;
|
|
3391
3391
|
const field = Object.hasOwn(payload ?? {}, 'input') ? 'input' : 'text';
|
|
3392
3392
|
const text = Object.getOwnPropertyDescriptor(payload ?? {}, field)?.value;
|
|
3393
3393
|
if (is.string(text)) {
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3394
|
+
const textFormat = Object.getOwnPropertyDescriptor(payload, 'textFormat')?.value;
|
|
3395
|
+
const input = speechInputPrepared === true || textFormat === 'plain'
|
|
3396
|
+
? text
|
|
3397
|
+
: stripSpeechFormatting(text);
|
|
3398
|
+
if (input !== text || Object.hasOwn(payload, 'textFormat')) {
|
|
3396
3399
|
const payloadProperties = Object.getOwnPropertyDescriptors(payload);
|
|
3397
3400
|
payloadProperties[field] = {...payloadProperties[field], value: input};
|
|
3401
|
+
delete payloadProperties.textFormat;
|
|
3398
3402
|
const requestProperties = Object.getOwnPropertyDescriptors(options);
|
|
3399
3403
|
requestProperties.payload = {
|
|
3400
3404
|
...requestProperties.payload,
|
|
@@ -153,21 +153,26 @@ function nativeAdapter(){
|
|
|
153
153
|
return preferences;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
function isUnsupportedNativeAdapter(error){
|
|
157
|
-
return error?.code==='ANDROID_CAPABILITY_UNSUPPORTED'
|
|
156
|
+
function isUnsupportedNativeAdapter(error,method){
|
|
157
|
+
return error?.code==='ANDROID_CAPABILITY_UNSUPPORTED'
|
|
158
|
+
||(error?.code==='METHOD_NOT_ALLOWED'
|
|
159
|
+
&&error.reason==='core-namespace-unavailable'
|
|
160
|
+
&&error.namespace==='preferences'
|
|
161
|
+
&&error.method===`preferences.${method}`);
|
|
158
162
|
}
|
|
159
163
|
|
|
160
|
-
function preferenceAdapter(){
|
|
164
|
+
function preferenceAdapter(assertActive){
|
|
161
165
|
const local=localAdapter('arcane.preferences');
|
|
162
166
|
if(is.function(globalThis.arcaneAndroid?.postMessage)) return local;
|
|
163
167
|
const native=nativeAdapter();
|
|
164
168
|
if(!native) return local;
|
|
165
169
|
let active=native;
|
|
166
|
-
async function call(method,args){
|
|
170
|
+
async function call(method,args,context){
|
|
167
171
|
try{
|
|
168
172
|
return await active[method](...args);
|
|
169
173
|
}catch(error){
|
|
170
|
-
if(active!==native||!isUnsupportedNativeAdapter(error)) throw error;
|
|
174
|
+
if(active!==native||!isUnsupportedNativeAdapter(error,method)) throw error;
|
|
175
|
+
assertActive(context?.signal);
|
|
171
176
|
active=local;
|
|
172
177
|
delete adapter.setMany;
|
|
173
178
|
if(!is.function(active[method])) throw error;
|
|
@@ -175,19 +180,19 @@ function preferenceAdapter(){
|
|
|
175
180
|
}
|
|
176
181
|
}
|
|
177
182
|
const adapter={
|
|
178
|
-
async get(key){
|
|
179
|
-
return call('get',[key]);
|
|
183
|
+
async get(key,context){
|
|
184
|
+
return call('get',[key,context],context);
|
|
180
185
|
},
|
|
181
|
-
async set(key,value){
|
|
182
|
-
return call('set',[key,value]);
|
|
186
|
+
async set(key,value,context){
|
|
187
|
+
return call('set',[key,value,context],context);
|
|
183
188
|
},
|
|
184
|
-
async delete(key){
|
|
185
|
-
return call('delete',[key]);
|
|
189
|
+
async delete(key,context){
|
|
190
|
+
return call('delete',[key,context],context);
|
|
186
191
|
}
|
|
187
192
|
};
|
|
188
193
|
if(is.function(native.setMany)){
|
|
189
194
|
adapter.setMany=async function setMany(entries,context){
|
|
190
|
-
return call('setMany',[entries,context]);
|
|
195
|
+
return call('setMany',[entries,context],context);
|
|
191
196
|
};
|
|
192
197
|
}
|
|
193
198
|
return adapter;
|
|
@@ -204,7 +209,7 @@ export default class PreferenceStore extends EventTarget{
|
|
|
204
209
|
super();
|
|
205
210
|
this.namespace=String(namespace||'arcane');
|
|
206
211
|
this.schema=preferenceSchema(schema);
|
|
207
|
-
this.adapter=validateAdapter(adapter||preferenceAdapter());
|
|
212
|
+
this.adapter=validateAdapter(adapter||preferenceAdapter(this.#assertOperationActive.bind(this)));
|
|
208
213
|
this.values=this.defaults();
|
|
209
214
|
this.#events=createArcaneEventSource(
|
|
210
215
|
this,
|
|
@@ -8,16 +8,34 @@ export default class SystemAppearance{
|
|
|
8
8
|
|
|
9
9
|
async current(){
|
|
10
10
|
if(!this.api||!is.function(this.api.current)) return {supported:false,platform:'browser'};
|
|
11
|
-
|
|
11
|
+
try{
|
|
12
|
+
return await this.api.current();
|
|
13
|
+
}catch(error){
|
|
14
|
+
return unsupportedNamespace(error,'current');
|
|
15
|
+
}
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
async apply(input={}){
|
|
15
19
|
if(!this.available()) return {supported:false,platform:'browser'};
|
|
16
20
|
const scheme=['system','light','dark'].includes(input.scheme)?input.scheme:'system';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
try{
|
|
22
|
+
return await this.api.apply({
|
|
23
|
+
scheme,
|
|
24
|
+
captionColor:scheme==='system'?null:input.captionColor||null,
|
|
25
|
+
textColor:scheme==='system'?null:input.textColor||null
|
|
26
|
+
});
|
|
27
|
+
}catch(error){
|
|
28
|
+
return unsupportedNamespace(error,'apply');
|
|
29
|
+
}
|
|
22
30
|
}
|
|
23
31
|
}
|
|
32
|
+
|
|
33
|
+
function unsupportedNamespace(error,method){
|
|
34
|
+
if(error?.code==='METHOD_NOT_ALLOWED'
|
|
35
|
+
&&error.reason==='core-namespace-unavailable'
|
|
36
|
+
&&error.namespace==='appearance'
|
|
37
|
+
&&error.method===`appearance.${method}`){
|
|
38
|
+
return {supported:false,reason:'core-namespace-unavailable',error};
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
@@ -132,6 +132,16 @@
|
|
|
132
132
|
}
|
|
133
133
|
},
|
|
134
134
|
"localAI": { "$ref": "#/$defs/localAI" },
|
|
135
|
+
"window": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"additionalProperties": false,
|
|
138
|
+
"description": "Initial native client dimensions in logical pixels at 96 DPI, and whether the user can resize the window. Omitted fields retain the host defaults.",
|
|
139
|
+
"properties": {
|
|
140
|
+
"width": { "type": "integer", "minimum": 1 },
|
|
141
|
+
"height": { "type": "integer", "minimum": 1 },
|
|
142
|
+
"resizable": { "type": "boolean" }
|
|
143
|
+
}
|
|
144
|
+
},
|
|
135
145
|
"bundledApps": {
|
|
136
146
|
"type": "array",
|
|
137
147
|
"uniqueItems": true,
|
package/src/app-descriptor.mjs
CHANGED
|
@@ -235,8 +235,26 @@ function validateDocumentCatalog(value){
|
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
function validateNativeWindow(value){
|
|
239
|
+
if(value===undefined)return undefined;
|
|
240
|
+
assertOnlyKeys(value,new Set(['width','height','resizable']),'descriptor.native.window');
|
|
241
|
+
for(const dimension of ['width','height']){
|
|
242
|
+
if(value[dimension]!==undefined&&(!is.integer(value[dimension])||value[dimension]<=0)){
|
|
243
|
+
fail(`descriptor.native.window.${dimension} must be a positive integer.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if(value.resizable!==undefined&&!is.boolean(value.resizable)){
|
|
247
|
+
fail('descriptor.native.window.resizable must be a boolean.');
|
|
248
|
+
}
|
|
249
|
+
return completeValue({
|
|
250
|
+
...(value.width===undefined?{}:{width:value.width}),
|
|
251
|
+
...(value.height===undefined?{}:{height:value.height}),
|
|
252
|
+
...(value.resizable===undefined?{}:{resizable:value.resizable})
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
238
256
|
function validateNative(value,appId){
|
|
239
|
-
assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog','localAI','services']),'descriptor.native');
|
|
257
|
+
assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog','localAI','services','window']),'descriptor.native');
|
|
240
258
|
if(!NATIVE_TYPES.has(value.type))fail('descriptor.native.type is unsupported.');
|
|
241
259
|
const icon=value.icon===null?null:normalizeRelativePath(value.icon,'descriptor.native.icon');
|
|
242
260
|
if(!is.integer(value.order)||value.order<0){
|
|
@@ -246,6 +264,7 @@ function validateNative(value,appId){
|
|
|
246
264
|
pattern:APP_ID_PATTERN
|
|
247
265
|
});
|
|
248
266
|
const documentCatalog=validateDocumentCatalog(value.documentCatalog);
|
|
267
|
+
const window=validateNativeWindow(value.window);
|
|
249
268
|
let services;
|
|
250
269
|
if(value.services!==undefined){
|
|
251
270
|
if(!is.array(value.services))fail('descriptor.native.services must be an array.');
|
|
@@ -259,6 +278,7 @@ function validateNative(value,appId){
|
|
|
259
278
|
icon,
|
|
260
279
|
order:value.order,
|
|
261
280
|
bundledApps,
|
|
281
|
+
...(window===undefined?{}:{window}),
|
|
262
282
|
...(services===undefined?{}:{services}),
|
|
263
283
|
...(value.localAI===undefined?{}:{localAI:normalizeLocalAIConfig(value.localAI)}),
|
|
264
284
|
...(documentCatalog?{documentCatalog}:{})
|
|
@@ -395,6 +415,7 @@ export function projectNativeDescriptor(descriptor,{source}={}){
|
|
|
395
415
|
...(value.native.bundledApps.length?{bundledApps:[...value.native.bundledApps]}:{}),
|
|
396
416
|
...(value.native.documentCatalog?{documentCatalog:value.native.documentCatalog}:{}),
|
|
397
417
|
...(value.native.services===undefined?{}:{services:value.native.services}),
|
|
418
|
+
...(value.native.window===undefined?{}:{window:value.native.window}),
|
|
398
419
|
...(value.native.localAI===undefined?{}:{localAI:value.native.localAI}),
|
|
399
420
|
include:[...value.package.include]
|
|
400
421
|
};
|
|
@@ -440,6 +461,7 @@ function synthesizedDescriptor(packageManifest,nativeDescriptor){
|
|
|
440
461
|
order:is.integer(native.order)?native.order:100,
|
|
441
462
|
bundledApps:[...(native.bundledApps??[])],
|
|
442
463
|
...(native.services===undefined?{}:{services:native.services}),
|
|
464
|
+
...(native.window===undefined?{}:{window:native.window}),
|
|
443
465
|
...((packageManifest.localAI??native.localAI)===undefined?{}:{localAI:packageManifest.localAI??native.localAI}),
|
|
444
466
|
...(native.documentCatalog?{documentCatalog:native.documentCatalog}:{})
|
|
445
467
|
},
|
|
@@ -26,6 +26,10 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
26
26
|
public string CoreExecutable { get; set; }
|
|
27
27
|
public string CoreArguments { get; set; }
|
|
28
28
|
public string CoreWorkingDirectory { get; set; }
|
|
29
|
+
/// <summary>Optional initial client dimensions in logical pixels at 96 DPI.</summary>
|
|
30
|
+
public double? InitialClientWidth { get; set; }
|
|
31
|
+
public double? InitialClientHeight { get; set; }
|
|
32
|
+
public bool? Resizable { get; set; }
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
public static class ArcaneHost
|
|
@@ -128,6 +132,8 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
128
132
|
{
|
|
129
133
|
if (options == null) throw new ArgumentNullException("options");
|
|
130
134
|
if (onDiagnostic == null) throw new ArgumentNullException("onDiagnostic");
|
|
135
|
+
ValidateInitialDimension(options.InitialClientWidth, "InitialClientWidth");
|
|
136
|
+
ValidateInitialDimension(options.InitialClientHeight, "InitialClientHeight");
|
|
131
137
|
this.options = options;
|
|
132
138
|
this.onDiagnostic = onDiagnostic;
|
|
133
139
|
this.onError = onError;
|
|
@@ -135,6 +141,11 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
135
141
|
completion.Task.ContinueWith(ObserveReportedFailure, TaskContinuationOptions.OnlyOnFaulted);
|
|
136
142
|
Text = options.Title;
|
|
137
143
|
AutoScaleMode = AutoScaleMode.Dpi;
|
|
144
|
+
if (options.Resizable.HasValue)
|
|
145
|
+
{
|
|
146
|
+
FormBorderStyle = options.Resizable.Value ? FormBorderStyle.Sizable : FormBorderStyle.FixedSingle;
|
|
147
|
+
MaximizeBox = options.Resizable.Value;
|
|
148
|
+
}
|
|
138
149
|
webView = new WebView2 { Dock = DockStyle.Fill };
|
|
139
150
|
Controls.Add(webView);
|
|
140
151
|
}
|
|
@@ -145,6 +156,9 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
145
156
|
|
|
146
157
|
protected override void OnLoad(EventArgs args)
|
|
147
158
|
{
|
|
159
|
+
// Apply the initial choice once. A composing launcher's Load handler
|
|
160
|
+
// can then restore its own saved state; later user sizing is untouched.
|
|
161
|
+
if (!started && !closing) ApplyInitialWindowSize();
|
|
148
162
|
base.OnLoad(args);
|
|
149
163
|
if (started || closing) return;
|
|
150
164
|
started = true;
|
|
@@ -152,6 +166,38 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
152
166
|
initialization.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
|
|
153
167
|
}
|
|
154
168
|
|
|
169
|
+
private static void ValidateInitialDimension(double? value, string name)
|
|
170
|
+
{
|
|
171
|
+
if (value.HasValue && (Double.IsNaN(value.Value) || Double.IsInfinity(value.Value)
|
|
172
|
+
|| value.Value <= 0 || Math.Truncate(value.Value) != value.Value))
|
|
173
|
+
throw new ArgumentOutOfRangeException(name, "Select a positive integral logical client dimension.");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private void ApplyInitialWindowSize()
|
|
177
|
+
{
|
|
178
|
+
if ((!options.InitialClientWidth.HasValue && !options.InitialClientHeight.HasValue)
|
|
179
|
+
|| WindowState != FormWindowState.Normal) return;
|
|
180
|
+
|
|
181
|
+
Rectangle workArea = Screen.FromControl(this).WorkingArea;
|
|
182
|
+
int availableWidth = Math.Max(1, workArea.Width - (Width - ClientSize.Width));
|
|
183
|
+
int availableHeight = Math.Max(1, workArea.Height - (Height - ClientSize.Height));
|
|
184
|
+
using (Graphics display = CreateGraphics())
|
|
185
|
+
{
|
|
186
|
+
double width = options.InitialClientWidth.HasValue
|
|
187
|
+
? options.InitialClientWidth.Value * display.DpiX / 96.0 : ClientSize.Width;
|
|
188
|
+
double height = options.InitialClientHeight.HasValue
|
|
189
|
+
? options.InitialClientHeight.Value * display.DpiY / 96.0 : ClientSize.Height;
|
|
190
|
+
// Fit before converting to native integer coordinates. This is
|
|
191
|
+
// an initial screen fit, not a persistent maximum window size.
|
|
192
|
+
ClientSize = new Size(
|
|
193
|
+
(int)Math.Max(1, Math.Min(availableWidth, Math.Round(width))),
|
|
194
|
+
(int)Math.Max(1, Math.Min(availableHeight, Math.Round(height))));
|
|
195
|
+
}
|
|
196
|
+
Location = new Point(
|
|
197
|
+
Math.Max(workArea.Left, Math.Min(Left, workArea.Right - Width)),
|
|
198
|
+
Math.Max(workArea.Top, Math.Min(Top, workArea.Bottom - Height)));
|
|
199
|
+
}
|
|
200
|
+
|
|
155
201
|
private async Task InitializeAsync()
|
|
156
202
|
{
|
|
157
203
|
try
|
|
@@ -48,6 +48,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
48
48
|
Dictionary<string, object> manifest = ReadRecord(Path.Combine(directory, "arcane-native.json"));
|
|
49
49
|
Dictionary<string, object> app = RequiredRecord(manifest, "app");
|
|
50
50
|
Dictionary<string, object> client = RequiredRecord(manifest, "client");
|
|
51
|
+
Dictionary<string, object> windowOptions = manifest.ContainsKey("window") ? RequiredRecord(manifest, "window") : null;
|
|
51
52
|
string appId = RequiredString(app, "id");
|
|
52
53
|
object title;
|
|
53
54
|
string name = app.TryGetValue("displayName", out title) && title is string ? (string)title : appId;
|
|
@@ -71,6 +72,9 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
71
72
|
OriginHost = "arcane.local",
|
|
72
73
|
ProfileDirectory = Path.Combine(profileParent, "WebView2"),
|
|
73
74
|
Title = name,
|
|
75
|
+
InitialClientWidth = OptionalWindowDimension(windowOptions, "width"),
|
|
76
|
+
InitialClientHeight = OptionalWindowDimension(windowOptions, "height"),
|
|
77
|
+
Resizable = OptionalWindowResizable(windowOptions),
|
|
74
78
|
IconPath = String.IsNullOrEmpty(icon) ? null : Path.Combine(directory, icon),
|
|
75
79
|
ClassicClientSource = File.ReadAllText(Path.Combine(directory, RequiredString(client, "source"))),
|
|
76
80
|
CoreExecutable = Path.Combine(directory, "runtime", "ArcaneCore.exe"),
|
|
@@ -164,6 +168,23 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
164
168
|
return text;
|
|
165
169
|
}
|
|
166
170
|
|
|
171
|
+
private static double? OptionalWindowDimension(Dictionary<string, object> window, string name)
|
|
172
|
+
{
|
|
173
|
+
object value;
|
|
174
|
+
if (window == null || !window.TryGetValue(name, out value)) return null;
|
|
175
|
+
if (value is int || value is long || value is decimal || value is double)
|
|
176
|
+
return Convert.ToDouble(value, System.Globalization.CultureInfo.InvariantCulture);
|
|
177
|
+
throw new FormatException("The native manifest needs a number at window." + name + ".");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private static bool? OptionalWindowResizable(Dictionary<string, object> window)
|
|
181
|
+
{
|
|
182
|
+
object value;
|
|
183
|
+
if (window == null || !window.TryGetValue("resizable", out value)) return null;
|
|
184
|
+
if (value is bool) return (bool)value;
|
|
185
|
+
throw new FormatException("The native manifest needs a boolean at window.resizable.");
|
|
186
|
+
}
|
|
187
|
+
|
|
167
188
|
private static string QuoteArgument(string value)
|
|
168
189
|
{
|
|
169
190
|
// Windows command-line escaping is transport-local. The child
|
package/src/core/runtime.mjs
CHANGED
|
@@ -267,7 +267,21 @@ class CoreRuntime {
|
|
|
267
267
|
|
|
268
268
|
#request(frame) {
|
|
269
269
|
const registration = this.#methods.get(frame.method);
|
|
270
|
-
if (!registration)
|
|
270
|
+
if (!registration) {
|
|
271
|
+
const namespace = frame.method.split('.')[0];
|
|
272
|
+
const prefix = `${namespace}.`;
|
|
273
|
+
const namespacePresent = this.#services.has(namespace)
|
|
274
|
+
|| [...this.#methods.keys(), 'system.ping', 'app.current', 'version.current'].some(
|
|
275
|
+
function inNamespace(method) { return method.startsWith(prefix); }
|
|
276
|
+
);
|
|
277
|
+
throw new CoreError({
|
|
278
|
+
code: 'METHOD_NOT_ALLOWED',
|
|
279
|
+
message: `Core does not expose ${frame.method}.`,
|
|
280
|
+
reason: namespacePresent ? 'core-method-unavailable' : 'core-namespace-unavailable',
|
|
281
|
+
namespace,
|
|
282
|
+
method: frame.method
|
|
283
|
+
});
|
|
284
|
+
}
|
|
271
285
|
const {service, handle, lifetime} = registration;
|
|
272
286
|
const controller = new AbortController();
|
|
273
287
|
const request = {id: frame.id, method: frame.method, lifetime, controller, task: null};
|
|
@@ -196,6 +196,7 @@ export function createPortableNativeProvider({services, packagedWeb = false, web
|
|
|
196
196
|
target,
|
|
197
197
|
webRoot: 'app',
|
|
198
198
|
start: release.manifest?.app?.start ?? './index.html',
|
|
199
|
+
...(appDescriptor.native?.window === undefined ? {} : {window: {...appDescriptor.native.window}}),
|
|
199
200
|
core: {
|
|
200
201
|
entry: 'runtime/arcane-core.mjs', transport: 'stdio', protocol: 'arcane/1', services: selectedServices,
|
|
201
202
|
...(localAI === undefined ? {} : {localAIRuntimes: bundled.runtimes})
|