返回 Skill 列表
extension
分类: 开发与工程无需 API Key

script-kit-app-shell

构建并配置Script Kit应用程序外壳——所有提示的统一框架/铬系统。使用场景:(1) 创建需要头部、底部或铬的新提示视图 (2) 根据布局需求配置ShellSpec (3) 使用HeaderSpec, FooterSpec, ChromeSpec工作 (4) 管理焦点策略 (5) 通过KeymapSpec设置键盘绑定 (6) 设计外壳框架样式。触发词包括:“app shell”, “shell spec”, “header spec”, “footer spec”, “chrome spec”, “focus policy”, “keymap spec”, “shell focus”。

person作者: jakexiaohubgithub

Script Kit App Shell

The App Shell is a presentational frame/chrome layer that wraps prompt content with consistent styling, focus management, and keyboard routing.

Architecture

The shell is a frame + chrome layer, not another "app":

  • State and lifecycle remain in the window root (e.g., ScriptListApp)
  • Takes stable focus handles by reference (doesn't own them)
  • Never allocates per-render (uses SmallVec, SharedString, action enums)

Core Types

ShellSpec - Main Layout Specification

ShellSpec::new()
    .header(HeaderSpec::search("Type to search..."))
    .footer(FooterSpec::new().primary("Run", "Enter"))
    .content(my_content_element)
    .chrome(ChromeSpec::full_frame())
    .focus_policy(FocusPolicy::HeaderInput)
    .keymap(KeymapSpec::new())

| Field | Type | Purpose | |-------|------|---------| | header | Option<HeaderSpec> | Header with input/buttons/logo | | footer | Option<FooterSpec> | Footer with actions/info | | content | Option<AnyElement> | Main content element | | chrome | ChromeSpec | Frame styling (bg, shadow, radius) | | focus_policy | FocusPolicy | Where focus lands on activation | | keymap | KeymapSpec | Per-view keybindings |

HeaderSpec - Header Configuration

// Simple search header
HeaderSpec::search("Type to filter...")

// With buttons
HeaderSpec::search("Search scripts")
    .button("Run", "Enter")
    .secondary_button("Actions", "Cmd+K")

// With path prefix
HeaderSpec::search("Enter name")
    .path_prefix("~/scripts/")
    .ask_ai_hint(true)  // Shows "Ask AI [Tab]"

// Manual input setup
HeaderSpec::new()
    .text("current input value")
    .cursor_visible(true)
    .focused(true)
    .logo(true)

| Field | Type | Purpose | |-------|------|---------| | input | Option<InputSpec> | Search input configuration | | buttons | SmallVec<[ButtonSpec; 4]> | Action buttons (max ~4) | | show_logo | bool | Display Script Kit logo | | path_prefix | Option<SharedString> | Path shown before input | | show_ask_ai_hint | bool | Show "Ask AI [Tab]" hint |

InputSpec - Input Field State

InputSpec {
    placeholder: "Search...".into(),
    text: current_text.clone(),
    cursor_visible: true,  // For blinking animation
    is_focused: true,
}

FooterSpec - Footer Configuration

FooterSpec::new()
    .primary("Run Script", "Enter")
    .secondary("Actions", "Cmd+K")
    .helper("Tab 1 of 2")
    .info("TypeScript")
    .logo(true)

| Field | Type | Purpose | |-------|------|---------| | primary_label | SharedString | Primary action text | | primary_shortcut | SharedString | Primary shortcut hint | | secondary_label | Option<SharedString> | Secondary action text | | secondary_shortcut | Option<SharedString> | Secondary shortcut hint | | show_logo | bool | Display Script Kit logo | | helper_text | Option<SharedString> | Helper text (left side) | | info_label | Option<SharedString> | Info label (right side) |

ChromeSpec - Frame Styling

// Full frame (default) - rounded bg, shadow, divider
ChromeSpec::full_frame()

// Minimal - tighter styling, no divider
ChromeSpec::minimal()

// Content only - no background/shadow
ChromeSpec::content_only()

// Custom
ChromeSpec::full_frame()
    .radius(16.0)
    .padding(8.0)
    .divider(DividerSpec::Hairline)
    .opacity(0.85)

| Mode | Background | Shadow | Divider | Use Case | |------|------------|--------|---------|----------| | FullFrame | Yes | Yes | Hairline | ScriptList, ArgPrompt | | MinimalFrame | Yes | Yes | None | HUD, compact prompts | | ContentOnly | No | No | None | Custom-styled overlays |

ChromeMode Methods

chrome.mode.shows_divider()   // true for FullFrame only
chrome.mode.has_background()  // true except ContentOnly
chrome.mode.has_shadow()      // true except ContentOnly
chrome.should_show_divider()  // combines mode + DividerSpec

DividerSpec - Header/Content Separator

DividerSpec::None      // No divider
DividerSpec::Hairline  // 1px line (default)

Focus Management

FocusPolicy - Where Focus Goes

FocusPolicy::Preserve     // Don't change focus (overlays, HUD)
FocusPolicy::HeaderInput  // Focus search box (default)
FocusPolicy::Content      // Focus content area (editor, term)

ShellFocus - Stable Focus Handles

Created once per window, stored in root state:

// In app initialization
let shell_focus = ShellFocus::new(cx);

// Apply policy on view transition (not every render)
shell_focus.apply_policy(spec.focus_policy, window, cx);

// Query focus state
shell_focus.is_header_focused(window)
shell_focus.is_content_focused(window)
shell_focus.is_any_focused(window)

// Manual focus control
shell_focus.focus_header(window, cx);
shell_focus.focus_content(window, cx);

// For track_focus
shell_focus.root_handle()

Keyboard Routing

ShellAction - Action Enum

ShellAction::Cancel      // Escape - cancel/close/back
ShellAction::Run         // Enter - run/submit/select
ShellAction::OpenActions // Cmd+K - actions dialog
ShellAction::FocusSearch // Focus search input
ShellAction::Next        // Down arrow - next item
ShellAction::Prev        // Up arrow - previous item
ShellAction::Tab         // Tab - next field/AI hint
ShellAction::ShiftTab    // Shift+Tab - previous field
ShellAction::Close       // Cmd+W - close window
ShellAction::Settings    // Cmd+, - open settings
ShellAction::None        // No action (key not handled)

KeymapSpec - Per-View Bindings

// Add custom bindings
KeymapSpec::new()
    .bind("p", ShellAction::Prev)
    .bind("n", ShellAction::Next)

// With modifiers
KeymapSpec::new()
    .bind_with_modifiers("s", Modifiers::command(), ShellAction::Run)

// Modal (blocks global shortcuts)
KeymapSpec::modal()
    .bind("escape", ShellAction::Cancel)

Default Bindings

| Key | Modifiers | Action | |-----|-----------|--------| | escape | - | Cancel | | enter | - | Run | | k | Cmd | OpenActions | | w | Cmd | Close | | , | Cmd | Settings | | tab | - | Tab | | tab | Shift | ShiftTab | | up | - | Prev | | down | - | Next |

Routing Priority

  1. View-specific KeymapSpec bindings
  2. Default shell bindings (unless modal: true)
// Route a key event
let action = route_key(key, &modifiers, &view_keymap);
if action.is_handled() {
    // Process action
}

Button System

ButtonSpec

ButtonSpec::primary("Run", "Enter")   // Primary style
ButtonSpec::secondary("More", "...")  // Ghost style

// Custom action
ButtonSpec::primary("Submit", "Cmd+S")
    .action(ButtonAction::Custom(42))
    .variant(ButtonVariant::Icon)

ButtonAction

ButtonAction::Submit       // Default primary action
ButtonAction::OpenActions  // Open actions dialog
ButtonAction::TogglePreview
ButtonAction::Cancel
ButtonAction::Custom(u32)  // Custom action by ID

ButtonVariant

ButtonVariant::Primary  // Solid accent color
ButtonVariant::Ghost    // Text only
ButtonVariant::Icon     // Icon button

Style Caching

ShellStyleCache

Pre-computed styles to avoid per-render computation:

// Create from theme
let style_cache = ShellStyleCache::from_theme(&theme, revision);

// Check/update cache
if !style_cache.is_valid(new_revision) {
    style_cache.update_if_needed(&theme, new_revision);
}

Contains:

  • frame_bg: Hsla with vibrancy opacity (70-85%)
  • shadows: Vec<BoxShadow>
  • radius: Pixels
  • header: HeaderColors
  • footer: FooterColors
  • divider: DividerColors

Rendering

AppShell::render

impl ScriptListApp {
    fn render_shell(&self, window: &mut Window, cx: &mut App) -> AnyElement {
        let spec = self.build_shell_spec(cx);
        let runtime = ShellRuntime {
            focus: &self.shell_focus,
            style: &self.style_cache,
            cursor_visible: self.cursor_visible,
        };
        AppShell::render(spec, &runtime, window, cx)
    }
}

Common Patterns

Script List View

ShellSpec::new()
    .header(
        HeaderSpec::search("Type to search...")
            .text(&self.search_text)
            .button("Run", "Enter")
            .secondary_button("Actions", "Cmd+K")
            .ask_ai_hint(true)
    )
    .footer(
        FooterSpec::new()
            .primary("Run Script", "Enter")
            .secondary("Actions", "Cmd+K")
            .helper(format!("{} scripts", self.scripts.len()))
    )
    .content(self.render_script_list(cx))
    .chrome(ChromeSpec::full_frame())
    .focus_policy(FocusPolicy::HeaderInput)

Arg Prompt

ShellSpec::new()
    .header(
        HeaderSpec::search(&self.prompt.placeholder)
            .text(&self.input)
            .path_prefix(self.prompt.path.as_deref())
    )
    .footer(
        FooterSpec::new()
            .primary("Submit", "Enter")
            .info(&self.prompt.hint)
    )
    .content(self.render_choices(cx))
    .chrome(ChromeSpec::full_frame())
    .focus_policy(FocusPolicy::HeaderInput)

HUD Notification

ShellSpec::new()
    .content(self.render_hud_content(cx))
    .chrome(ChromeSpec::minimal())
    .focus_policy(FocusPolicy::Preserve)
    .keymap(KeymapSpec::modal())

Editor Prompt

ShellSpec::new()
    .header(HeaderSpec::new().logo(true))
    .footer(
        FooterSpec::new()
            .primary("Save", "Cmd+S")
            .info("TypeScript")
    )
    .content(self.render_editor(cx))
    .chrome(ChromeSpec::full_frame())
    .focus_policy(FocusPolicy::Content)

File Structure

src/app_shell/
  mod.rs    - Module exports and re-exports
  shell.rs  - AppShell renderer (main entry point)
  spec.rs   - ShellSpec, HeaderSpec, FooterSpec, InputSpec, ButtonSpec
  chrome.rs - ChromeMode, ChromeSpec, DividerSpec
  focus.rs  - FocusPolicy, ShellFocus
  keymap.rs - KeymapSpec, ShellAction, route_key(), default_bindings()
  style.rs  - ShellStyleCache, HeaderColors, FooterColors, DividerColors
  tests.rs  - Unit tests

Module Exports

pub use chrome::{ChromeMode, ChromeSpec, DividerSpec};
pub use focus::{FocusPolicy, ShellFocus};
pub use keymap::{KeymapSpec, ShellAction};
pub use shell::AppShell;
pub use spec::{ButtonSpec, FooterSpec, HeaderSpec, InputSpec, ShellSpec};
pub use style::ShellStyleCache;