ozmos/starboard

A command palette for Laravel

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/ozmos/starboard

v0.1.0 2025-11-24 08:23 UTC

This package is auto-updated.

Last update: 2025-11-24 08:26:45 UTC


README

Starboard is a command palette for Laravel applications. It provides a powerful, extensible command interface that allows you to quickly access and interact with your application's functionality. It is intended for use within the development environment only, but you can use it in production if you so desire.

Starboard Demo

Table of Contents

Installation

Require the package through composer

composer require ozmos/starboard

Publish the javascript assets to ./public/vendor/starboard

php artisan vendor:publish --tag="starboard-assets"

Usage

Enable Starboard and register commands in a service provider

use Illuminate\Support\ServiceProvider;
use Ozmos\Starboard\Facades\Starboard;

class AppServiceProvider extends ServiceProvider
{
  public function boot(): void
  {
    Starboard::enable(app()->isLocal());
    Starboard::register([
      \Ozmos\Starboard\Commands\ImpersonateUser::make(),
      \Ozmos\Starboard\Commands\TelescopeRequests::make(),
    ]);
  }
}

Render starboard in your application using the @starboard blade directive, most likely in your root layout

<html>
  <body>
    @starboard
  </body>
</html>

Built in commands

ImpersonateUser

Search your User model and log in as user.

Starboard::register([
  \Ozmos\Starboard\Commands\ImpersonateUser::make()
]);

You can optionally configure this command further to suit your application

Starboard::register([
  \Ozmos\Starboard\Commands\ImpersonateUser::make()
    // title of the command (appears in initial palette list)
    ->commandTitle('Impersonate User')
    // what model to query
    ->model(\App\Models\User::class)
    // searchable table columns
    ->columns(['id', 'name', 'email'])
    // customise how the users appear in the list
    ->itemBuilder(function (ListItemBuilder $builder, $user) {
      return $builder
        ->title($user->name)
        ->subtitle($user->email)
        ->id($user->getKey());
    })
    // search filter placeholder
    ->placeholder('Search for a user')
    // what happens when the user is selected in the list
    ->onSelect(function (string $id) {
      $user = $this->model::find($id);
      Auth::login($user);
      return CommandResponse::make()->reload();
    })
]);

TailLogs

Returns the last n lines of a log file

Starboard::register([
  \Ozmos\Starboard\Commands\TailLogs::make()
]);

You can optionally configure the log file and number of lines returned

Starboard::register([
  \Ozmos\Starboard\Commands\TailLogs::make()
    ->logFile(storage_path('logs/laravel.log'))
    ->lines(500)
]);

Telescope Requests

View the most recent requests from your application. Requires telescope to be installed/enabled

Starboard::register([
  \Ozmos\Starboard\Commands\TelescopeRequests::make(),
]);

Telescope Mail

View the most recent mail sent from your application. Requires telescope to be installed/enabled

Starboard::register([
  \Ozmos\Starboard\Commands\TelescopeMail::make(),
]);

Telescope Jobs

View the most recent jobs from your application. Requires telescope to be installed/enabled

Starboard::register([
  \Ozmos\Starboard\Commands\TelescopeJobs::make(),
]);

Custom Commands

All commands extend from the Ozmos\Starboard\Commands\BaseCommand class. You can either extend this base class directly, or use one of the several command builders offered to speed up the process.

Anatomy of a command

All commands extend BaseCommand and must implement:

  • id(): string - A unique identifier for the command
  • title(): string - The display name shown in the command palette
  • render(CommandContext $context) - Returns a view that defines what the command displays

The render method must return a view provided by the Ozmos\Starboard\Commands\Views\CommandView class. There are two main view types:

List Views

List views display a searchable list of items. Use CommandView::list() to create a list view:

return CommandView::list()
  ->render(function (ListBuilder $builder) {
    // Return a collection of ListItemBuilder instances
    return collect($items)->map(function ($item) use ($builder) {
      return $builder
        ->item()
        ->title($item->name)
        ->subtitle($item->description)
        ->id($item->id);
    });
  })
  ->rerunsOnInputChange() // Re-run when user types
  ->placeholder('Search items...')
  ->onSelect(function (string $id) {
    // Handle item selection
    return CommandResponse::make()->push($detailCommand);
  })
  ->action('refresh', function (string $listItemId) {
    // Custom action for list items
    return CommandResponse::make()->toast('Refreshed!');
  });

List views support:

  • Async filtering: Use rerunsOnInputChange() to filter results as the user types
  • Pagination: Return a paginated result from your query (Laravel's paginator is supported)
  • Actions: Add custom actions that appear as buttons for each list item
  • Placeholder text: Customize the search input placeholder

Detail Views

Detail views display detailed information about a single item. Use CommandView::detail() to create a detail view:

return CommandView::detail()
  ->render(function (DetailBuilder $builder) {
    return $builder
      ->text('Label', 'Value')                    // Plain text field
      ->preformatted('Code', $code)              // Preformatted text (monospace)
      ->markdown('Description', $markdown)       // Markdown content (rendered as HTML)
      ->html('Content', $html, iframe: false)     // Raw HTML content
      ->action('refresh', function () {
        return CommandResponse::make()
          ->replace($this)
          ->toast('Refreshed!');
      });
  });

Detail views support:

  • Text blocks: Simple label-value pairs
  • Preformatted blocks: Monospace text (great for code, JSON, etc.)
  • Markdown blocks: Rendered markdown content
  • HTML blocks: Raw HTML (with optional iframe rendering)
  • Actions: Add custom action buttons

Model searching commands

If your command operates around searching a list of models you can use the ModelLookup command builder

Starboard::register([
  \Ozmos\Starboard\Commands\ModelLookup::make()
     // title of the command (appears in initial palette list)
    ->commandTitle('Team Lookup')
    // id of the command (must be unique across all registered commands)
    ->commandId('team-lookup')
    // what model to query
    ->model(\App\Models\User::class)
    // searchable table columns
    ->columns(['id', 'name', 'email'])
    // customise how the users appear in the list
    ->itemBuilder(function (ListItemBuilder $builder, $user) {
      return $builder
        ->title($user->name)
        ->subtitle($user->email)
        ->id($user->getKey());
    })
    // search filter placeholder
    ->placeholder('Search for a user')
    // what happens when the user is selected in the list
    ->onSelect(function (string $id) {
      $user = $this->model::find($id);
      Auth::login($user);
      return CommandResponse::make()->reload();
    })
]);

Building a command from scratch

Here we will build a complete example that demonstrates:

  • Creating a custom command that returns a list of items
  • Creating a child command that shows detail views
  • Using CommandResponse to navigate between commands
  • Using actions for additional functionality

Example: Language Search Command

First, let's create a command that searches through a list of programming languages:

use Ozmos\Starboard\Commands\BaseCommand;
use Ozmos\Starboard\Commands\CommandContext;
use Ozmos\Starboard\Commands\CommandResponse;
use Ozmos\Starboard\Commands\Views\CommandView;
use Ozmos\Starboard\Commands\Views\ListBuilder;

class SearchLanguages extends BaseCommand
{
  public function id(): string
  {
    return 'search-languages';
  }

  public function title(): string
  {
    return 'Search Languages';
  }

  public function render(CommandContext $context)
  {
    $languages = ['PHP', 'JavaScript', 'Python', 'Ruby', 'Go', 'Rust'];
    
    return CommandView::list()
      ->render(function (ListBuilder $builder) use ($context, $languages) {
        return collect($languages)
          ->when($context->input(), fn($query) => $query->filter(
            fn($language) => str_contains(strtolower($language), strtolower($context->input()))
          ))
          ->map(function ($language) use ($builder) {
            return $builder
              ->item()
              ->title($language)
              ->id($language);
          });
      })
      ->rerunsOnInputChange()
      ->placeholder('Search for a programming language...')
      ->onSelect($this->onSelect(...));
  }

  private function onSelect(string $language)
  {
    return CommandResponse::make()->push(LanguageDetail::make()->forLanguage($language));
  }

  public function children(): array
  {
    return [
      LanguageDetail::make()->hidden(),
    ];
  }
}

Now, let's create the detail command that shows information about a selected language:

use Ozmos\Starboard\Commands\BaseCommand;
use Ozmos\Starboard\Commands\CommandContext;
use Ozmos\Starboard\Commands\CommandResponse;
use Ozmos\Starboard\Commands\Views\CommandView;
use Ozmos\Starboard\Commands\Views\DetailBuilder;

class LanguageDetail extends BaseCommand
{
  public string $language;

  public function forLanguage(string $language): self
  {
    $this->language = $language;
    return $this;
  }

  public function id(): string
  {
    return 'language-detail';
  }

  public function title(): string
  {
    return 'Language Detail';
  }

  public function render(CommandContext $context)
  {
    return CommandView::detail()
      ->render(function (DetailBuilder $builder) {
        $info = $this->getLanguageInfo($this->language);
        
        return $builder
          ->text('Name', $this->language)
          ->text('Type', $info['type'])
          ->text('Year Created', $info['year'])
          ->preformatted('Description', $info['description'])
          ->markdown('Features', $info['features'])
          ->action('view-docs', fn() => CommandResponse::make()
            ->openUrl($info['docs_url'])
            ->toast('Opening documentation...'));
      });
  }

  private function getLanguageInfo(string $language): array
  {
    // Your logic to fetch language information
    return [
      'type' => 'Interpreted',
      'year' => '1995',
      'description' => 'A popular programming language...',
      'features' => '- Feature 1\n- Feature 2',
      'docs_url' => "https://example.com/docs/{$language}",
    ];
  }
}

Register your custom commands:

Starboard::register([
  SearchLanguages::make(),
]);

Understanding Command Responses

When a user interacts with your command (selects an item, clicks an action), you return a CommandResponse that tells Starboard what to do next:

  • push($command) - Navigate to a new command (adds to navigation stack)
  • replace($command) - Replace the current command
  • openUrl($url) - Open a URL in a new tab
  • reload() - Reload the current page
  • dismiss() - Close the command palette
  • toast($message) - Show a toast notification (can be chained with other responses)

Understanding Command Context

The CommandContext provides information about the current state:

  • $context->input() - The current search input from the user
  • $context->page() - The current page number (for pagination)
  • $context->cursor() - The pagination cursor (for cursor-based pagination)

Child Commands

Child commands are commands that are automatically registered when their parent is registered, but are marked as hidden (not discoverable in the root command list). They're typically used for detail views or sub-commands that are only accessible through their parent.

To create a child command, return it from the children() method and mark it as hidden:

public function children(): array
{
  return [
    LanguageDetail::make()->hidden(),
  ];
}

View Types Reference

ListBuilder Methods

  • render(Closure $callback) - Define how list items are generated. The callback receives a ListBuilder and should return a collection of ListItemBuilder instances or a paginator.
  • item() - Create a new list item builder
  • placeholder(string $text) - Set the search input placeholder
  • rerunsOnInputChange(bool $async = true) - Enable automatic re-rendering when the user types
  • onSelect(Closure $callback) - Handle item selection. Callback receives the item ID and should return a CommandResponse
  • action(string $name, Closure $callback) - Add a custom action button. Callback receives the list item ID

ListItemBuilder Methods

  • id(string $id) - Set the unique identifier for this item
  • title(string $title) - Set the main title text
  • subtitle(?string $subtitle) - Set optional subtitle text

DetailBuilder Methods

  • render(Closure $callback) - Define the detail content. The callback receives a DetailBuilder
  • text(string $label, string $value) - Add a plain text field
  • preformatted(string $label, string $value) - Add preformatted (monospace) text
  • markdown(string $label, string $value) - Add markdown content (rendered as HTML)
  • html(string $label, string $value, bool $iframe = false) - Add raw HTML content
  • action(string $name, Closure $callback) - Add a custom action button

CommandResponse Methods

  • push(BaseCommand $command) - Navigate to a new command (adds to navigation history)
  • replace(BaseCommand $command) - Replace the current command (no history)
  • openUrl(string $url) - Open a URL in a new browser tab
  • reload() - Reload the current page
  • dismiss() - Close the command palette
  • toast(string $message) - Show a toast notification (can be chained)

CommandContext Methods

  • input() - Get the current search input value
  • page() - Get the current page number (for pagination)
  • cursor() - Get the pagination cursor (for cursor-based pagination)

Advanced Usage

Custom Query Logic

When using ModelLookup, you can override the default query logic:

\Ozmos\Starboard\Commands\ModelLookup::make()
  ->model(\App\Models\User::class)
  ->columns(['name', 'email'])
  ->query(function (CommandContext $context) {
    // Custom query logic
    return \App\Models\User::query()
      ->where('active', true)
      ->when($context->input(), function ($query) use ($context) {
        $query->where('name', 'like', '%' . $context->input() . '%');
      })
      ->orderBy('created_at', 'desc')
      ->limit(20);
  })

Pagination

List views support Laravel's pagination. Simply return a paginated result:

return CommandView::list()
  ->render(function (ListBuilder $builder) use ($context) {
    return \App\Models\User::query()
      ->when($context->input(), fn($q) => $q->where('name', 'like', '%' . $context->input() . '%'))
      ->paginate(perPage: 10, page: $context->page())
      ->through(function ($user) use ($builder) {
        return $builder
          ->item()
          ->title($user->name)
          ->id($user->id);
      });
  });

List Item Actions

Add custom actions to list items that appear as buttons:

return CommandView::list()
  ->render(function (ListBuilder $builder) {
    // ... build items
  })
  ->action('edit', function (string $itemId) {
    return CommandResponse::make()
      ->openUrl("/users/{$itemId}/edit")
      ->toast('Opening editor...');
  })
  ->action('delete', function (string $itemId) {
    \App\Models\User::find($itemId)->delete();
    return CommandResponse::make()
      ->replace($this)
      ->toast('User deleted');
  });

Making Commands Hidden

Commands that shouldn't appear in the root command list can be marked as hidden:

$command->hidden();

Hidden commands are typically child commands that are only accessible through their parent command.

Configuring

Publish the config to config/starboard.php

php artisan vendor:publish --tag="starboard-config"

The configuration file allows you to customize the routing for Starboard endpoints:

return [
  'routes' => [
    'prefix' => '_starboard',      // URL prefix for Starboard routes
    'middleware' => ['web'],        // Middleware to apply to routes
    'name' => 'starboard.',         // Route name prefix
  ],
];

Keyboard Shortcuts

Starboard supports the following keyboard shortcuts:

  • Cmd+K / Ctrl+K - Open/close the command palette
  • Esc - Close the command palette or go back
  • / - Navigate through list items
  • Enter - Select the highlighted item
  • Cmd+Enter / Ctrl+Enter - Execute the first action (if available)

Troubleshooting

Commands not appearing

  • Ensure Starboard is enabled: Starboard::enable(true)
  • Check that commands are registered in a service provider's boot() method
  • Verify commands are marked as discoverable (not hidden)

Command ID conflicts

If you see "Command already registered" errors, ensure all command IDs are unique. You can override the id() method to provide a custom identifier.

Assets not loading

Make sure you've published the assets:

php artisan vendor:publish --tag="starboard-assets"

And that the @starboard directive is included in your layout file.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Support

For issues, questions, or feature requests, please visit the GitHub repository.

License

Starboard is open-sourced software licensed under the MIT license.