Skip to content

Commit 610dbb3

Browse files
committed
fix: prioritize options over bool for macOS prefs and harden install script
- Check catalogItem.options before bool type in ConfigDetail and ConfigEditor so dropdown prefs (e.g. desktop click behavior) render correctly instead of showing ON/OFF toggle - Move exit 0 inside main() in install scripts to prevent bash hanging on /dev/tty after curl pipe completes - Add exec fallback exit in private install script
1 parent f478219 commit 610dbb3

File tree

5 files changed

+37
-17
lines changed

5 files changed

+37
-17
lines changed

src/lib/components/ConfigDetail.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -468,14 +468,14 @@
468468
<div class="prefs-kv-row">
469469
<span class="prefs-kv-label">{catalogItem?.label ?? pref.key}</span>
470470
<span class="prefs-kv-value">
471-
{#if (catalogItem?.type ?? pref.type) === 'bool'}
471+
{#if catalogItem?.options}
472+
{@const opt = catalogItem.options.find((o: { value: string; label: string }) => o.value === pref.value)}
473+
<span class="pref-option-val">{opt?.label ?? pref.value}</span>
474+
{:else if (catalogItem?.type ?? pref.type) === 'bool'}
472475
{@const boolOn = pref.value === 'true' || pref.value === '1'}
473476
<span class="pref-bool {boolOn ? 'pref-bool-on' : 'pref-bool-off'}">
474477
{boolOn ? 'ON' : 'OFF'}
475478
</span>
476-
{:else if catalogItem?.options}
477-
{@const opt = catalogItem.options.find((o: { value: string; label: string }) => o.value === pref.value)}
478-
<span class="pref-option-val">{opt?.label ?? pref.value}</span>
479479
{:else}
480480
<span class="pref-raw-val">{pref.value}</span>
481481
{/if}

src/lib/components/ConfigEditor.svelte

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -554,15 +554,7 @@
554554
</div>
555555
{#if added && prefIdx >= 0}
556556
<div class="pref-control">
557-
{#if item.type === 'bool'}
558-
<button
559-
class="pref-bool"
560-
class:on={macosPrefs[prefIdx].value === 'true'}
561-
onclick={() => updatePrefValue(prefIdx, macosPrefs[prefIdx].value === 'true' ? 'false' : 'true')}
562-
>
563-
{macosPrefs[prefIdx].value === 'true' ? 'ON' : 'OFF'}
564-
</button>
565-
{:else if item.options}
557+
{#if item.options}
566558
<select
567559
class="pref-sel"
568560
value={macosPrefs[prefIdx].value}
@@ -572,6 +564,14 @@
572564
<option value={opt.value}>{opt.label}</option>
573565
{/each}
574566
</select>
567+
{:else if item.type === 'bool'}
568+
<button
569+
class="pref-bool"
570+
class:on={macosPrefs[prefIdx].value === 'true'}
571+
onclick={() => updatePrefValue(prefIdx, macosPrefs[prefIdx].value === 'true' ? 'false' : 'true')}
572+
>
573+
{macosPrefs[prefIdx].value === 'true' ? 'ON' : 'OFF'}
574+
</button>
575575
{:else}
576576
<input
577577
type={(item.type === 'int' || item.type === 'float') ? 'number' : 'text'}

src/lib/macos-prefs-catalog.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export const MACOS_PREF_CATALOG: MacOSPrefCatalogItem[] = [
3838
{ id: 'trackpad-natural-scroll', category: 'Trackpad', label: 'Natural scrolling', description: 'Scroll content in the direction your fingers move (iOS-style)', domain: 'NSGlobalDomain', key: 'com.apple.swipescrolldirection', type: 'bool', defaultValue: 'true' },
3939
{ id: 'trackpad-three-finger-drag', category: 'Trackpad', label: 'Three-finger drag', description: 'Drag windows and items using three fingers on the trackpad', domain: 'com.apple.AppleMultitouchTrackpad', key: 'TrackpadThreeFingerDrag', type: 'bool', defaultValue: 'false' },
4040

41-
// Desktop
42-
{ id: 'desktop-click-to-show', category: 'Desktop', label: 'Click desktop to show it', description: 'Click the desktop wallpaper to bring it to the front and hide all windows', domain: 'com.apple.WindowManager', key: 'EnableStandardClickToShowDesktop', type: 'bool', defaultValue: 'false' },
41+
// Desktop & Stage Manager
42+
{ id: 'desktop-click-to-show', category: 'Desktop & Stage Manager', label: 'Click wallpaper to show desktop', description: 'When clicking the desktop wallpaper, hide windows to reveal the desktop', domain: 'com.apple.WindowManager', key: 'EnableStandardClickToShowDesktop', type: 'bool', defaultValue: 'false', options: [{ value: 'true', label: 'Always' }, { value: 'false', label: 'Only in Stage Manager' }] },
4343

4444
// Keyboard
4545
{ id: 'keyboard-key-repeat', category: 'Keyboard', label: 'Key repeat rate', description: 'How fast keys repeat when held down — lower number = faster (min 1)', domain: 'NSGlobalDomain', key: 'KeyRepeat', type: 'int', defaultValue: '2', min: 1, max: 15 },

src/lib/server/install-script.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ describe('generatePrivateInstallScript', () => {
1212
expect(script).toContain('main()');
1313
expect(script).toMatch(/main "\$@"\s*$/);
1414
expect(script).toContain('exec < /dev/tty');
15+
16+
// CRITICAL: main() must never return after exec < /dev/tty redirects stdin.
17+
// Every code path inside main() must end with exit or exec, otherwise
18+
// bash hangs waiting on /dev/tty when run via "curl | bash".
19+
expect(script).toContain('exit 1\n}');
1520
});
1621

1722
it('should generate private install script with sanitized username and slug', () => {
@@ -116,8 +121,15 @@ describe('generateInstallScript', () => {
116121
const script = generateInstallScript('testuser', 'my-config', '', '');
117122

118123
expect(script).toContain('main()');
119-
expect(script).toMatch(/main "\$@"\s*\nexit 0\s*$/);
124+
expect(script).toMatch(/main "\$@"\s*$/);
120125
expect(script).toContain('exec < /dev/tty');
126+
127+
// CRITICAL: main() must never return after exec < /dev/tty redirects stdin.
128+
// Every code path inside main() must end with exit or exec, otherwise
129+
// bash hangs waiting on /dev/tty when run via "curl | bash".
130+
expect(script).toContain('exit 0\n}');
131+
// No code should appear between main "$@" and end of script
132+
expect(script).not.toMatch(/main "\$@"[\s\S]*exit/);
121133
});
122134

123135
it('should generate basic install script without custom content', () => {

src/lib/server/install-script.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ set -e
1616
main() {
1717
# When run via "curl | bash", stdin is the script content, not the terminal.
1818
# Reopen stdin from /dev/tty so interactive prompts (sudo, Homebrew) work.
19+
# IMPORTANT: This redirects stdin for the ENTIRE process. After main() returns,
20+
# bash would try to read the next command from /dev/tty (not the pipe) and hang.
21+
# Therefore main() MUST exit explicitly — never let it return to the caller.
1922
if [ ! -t 0 ] && [ -e /dev/tty ]; then
2023
exec < /dev/tty || true
2124
fi
@@ -84,6 +87,8 @@ echo "Authorized! Fetching install script..."
8487
echo ""
8588
8689
exec bash <(curl -fsSL -H "Authorization: Bearer \$TOKEN" "\$APP_URL/${safeUsername}/${safeSlug}/install")
90+
echo "Error: Failed to launch install script" >&2
91+
exit 1
8792
}
8893
8994
main "\$@"
@@ -107,6 +112,9 @@ TAP_NAME="openbootdotdev/tap"
107112
main() {
108113
# When run via "curl | bash", stdin is the script content, not the terminal.
109114
# Reopen stdin from /dev/tty so interactive prompts (read, sudo, Homebrew) work.
115+
# IMPORTANT: This redirects stdin for the ENTIRE process. After main() returns,
116+
# bash would try to read the next command from /dev/tty (not the pipe) and hang.
117+
# Therefore main() MUST exit explicitly — never let it return to the caller.
110118
if [[ ! -t 0 ]] && [[ -e /dev/tty ]]; then
111119
exec < /dev/tty || true
112120
fi
@@ -290,9 +298,9 @@ done
290298
291299
echo ""
292300
echo "Installation complete!"
301+
exit 0
293302
}
294303
295304
main "\$@"
296-
exit 0
297305
`;
298306
}

0 commit comments

Comments
 (0)