Building a Custom Laravel Terminal UI with Livewire

Learn to create a robust and interactive terminal interface, complete with real-time updates and input validation, using Laravel and Livewire.

laravel livewire terminal ui

If you’ve ever found yourself stuck in a Laravel project’s terminal, struggling to navigate through endless commands and outputs, you’re not alone. The out-of-the-box terminal UI can quickly become cluttered and overwhelming as your projects grow.

You’ll build a custom Laravel terminal UI that streamlines interactions and enhances productivity. By the end of this tutorial, you’ll have a robust and interactive interface for executing commands, complete with real-time updates and input validation. You’ll also learn how to deploy this custom UI to production, ensuring seamless integration with your existing projects.

Prerequisites: Installing Laravel and Livewire

To build a custom Laravel terminal UI with Livewire, you’ll need to have both frameworks installed on your machine. This section will walk you through installing Laravel and Livewire.

First, install Composer if you haven’t already. You can download it from the official Composer website or use a package manager like Homebrew (on macOS) or Chocolatey (on Windows).

# Install Composer using terminal
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php --install-dir=/usr/local/bin --filename=composer

Once Composer is installed, create a new Laravel project. You can do this by running the following command:

# Create a new Laravel project in the current directory
composer create-project --prefer-dist laravel/laravel terminal-ui

Next, install Livewire using Composer. Navigate to your newly created Laravel project and run the following command:

# Install Livewire package using composer require
composer require livewire/livewire

After installation is complete, configure your Laravel project’s environment by running cp .env.example .env followed by php artisan key:generate.

That’s it for this section. With both Laravel and Livewire installed, you’re now ready to set up Livewire in the next section.

Setting Up Livewire

After installing Livewire, there’s no service provider to register by hand. Livewire uses Laravel’s package auto-discovery, so Composer’s post-autoload-dump script runs php artisan package:discover and registers the provider for you. You can confirm it was picked up by checking the cached package manifest:

grep livewire bootstrap/cache/packages.php

Livewire works without any configuration, but if you want to customize its settings you can publish its configuration file by running the following command:

php artisan livewire:config

This will create a new livewire.php file in your project’s config directory.

That’s it for setting up Livewire. Our next step is to define the terminal UI interface with Livewire components.

Defining the Terminal UI Interface with Livewire Components

In this step, we’ll define the interface for our custom terminal UI using Livewire components. We’ll create a new TerminalUI component that will serve as the main entry point for our application.

First, let’s create a new file called TerminalUI.php in the app/Livewire directory, which is where Livewire looks for class-based components:

// app/Livewire/TerminalUI.php

namespace App\Livewire;

use Livewire\Component;
use Illuminate\Support\Facades\Artisan;

class TerminalUI extends Component
{
    public $output = '';

    protected function getCommand(): string
    {
        return 'list';
    }

    public function mount()
    {
        $command = $this->getCommand();
        try {
            Artisan::call($command);
            $this->output = Artisan::output();
        } catch (\Exception $e) {
            $this->output = "Error executing command: {$e->getMessage()}";
        }
    }

    public function render()
    {
        return view('livewire.terminal-ui');
    }
}

In this example, we’re using Livewire’s Component class to create a new component that will handle the rendering of our terminal UI. We’ve also added three methods: getCommand() returns the Artisan command being executed, mount() runs it once when the component loads and stores its output (Artisan::call() returns an exit code, so the text comes from Artisan::output()), and render() returns the component’s view.

Next, let’s create the corresponding Blade view for our component:

{{-- resources/views/livewire/terminal-ui.blade.php --}}

<div class="container">
    <h1>Terminal UI</h1>
    <pre>{{ $output }}</pre>
</div>

This view simply displays the output captured in our mount() method. Note that a Livewire view must have a single root element, which is why everything is wrapped in one <div>. We’ll continue to build on this component in the next section, where we’ll add interactive features using Livewire’s wire:click directive.

We now have a basic terminal UI interface up and running.

Building Interactive Commands with Livewire’s wire:click Directive

Now that we have our terminal UI interface set up, it’s time to make it interactive by building custom commands. We’ll use Livewire’s wire:click directive to create these commands. This directive wires an element in the UI to a public method on a Livewire component.

Let’s start with a simple example: creating a command to list all users in the system. Generate a class-based component with php artisan make:livewire UserList --class, then open the new app/Livewire/UserList.php file and update its contents as follows:

namespace App\Livewire;

use App\Models\User;
use Livewire\Component;

class UserList extends Component
{
    public $users = [];

    public function render()
    {
        return view('livewire.user-list');
    }

    public function loadUsers()
    {
        $this->users = User::orderBy('name')->get();
    }
}

In the code above, we’ve declared a public method called loadUsers; any public method on a Livewire component can be called from the browser. When this method is executed (we’ll see how to trigger it shortly), it will fetch all users from the database and assign them to our $users property.

Next, update your resources/views/livewire/user-list.blade.php file to display the list of users:

<div>
    @foreach($users as $user)
        <p>{{ $user->name }}</p>
    @endforeach

    <button type="button" wire:click="loadUsers">
        Refresh Users List
    </button>
</div>

In this view, we’re using a foreach loop to display the list of users. We’ve also added a button that will trigger the loadUsers method when clicked.

Implementing Real-time Updates and Feedback with Livewire

One of the most powerful features of Livewire is its ability to provide real-time updates and feedback to users. This can be achieved through a combination of Livewire’s wire: directives (such as wire:poll), components, and events.

To have something to display, we’ll record each executed command in a command_logs table. Create the model and its migration with php artisan make:model CommandLog -m, add a string column in the generated migration’s up() method, and run php artisan migrate:

// database/migrations/xxxx_xx_xx_xxxxxx_create_command_logs_table.php (inside up())
Schema::create('command_logs', function (Blueprint $table) {
    $table->id();
    $table->string('command');
    $table->timestamps();
});

Then allow the command attribute to be mass assigned in app/Models/CommandLog.php:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class CommandLog extends Model
{
    protected $fillable = ['command'];
}

Now let’s update our existing terminal UI component (TerminalUI.php) to display real-time updates:

// app/Livewire/TerminalUI.php

namespace App\Livewire;

use Livewire\Component;
use App\Models\CommandLog;

class TerminalUI extends Component
{
    public function render()
    {
        $logs = CommandLog::latest()->take(10)->get();

        return view('livewire.terminal-ui', [
            'logs' => $logs,
        ]);
    }
}

In this updated code, we’re fetching the latest 10 command logs and passing them to our Blade template (livewire/terminal-ui.blade.php). We’ll use Livewire’s wire:poll directive in our template to keep a list of these logs up to date:

{{-- resources/views/livewire/terminal-ui.blade.php --}}

<div wire:poll.3s>
    <ul>
        @foreach($logs as $log)
            <li>{{ $log->command }} ({{ $log->created_at }})</li>
        @endforeach
    </ul>
</div>

In this template, we’re using the wire:poll.3s directive to poll for updates every 3 seconds. We’ll also display each log’s command and creation date.

With these changes in place, our terminal UI will now update in real-time whenever a new log is created. This provides an engaging user experience and helps users stay informed about the status of their commands.

We’re getting closer to completing our custom Laravel terminal UI with Livewire! Next, we’ll add input validation, and in the final section we’ll deploy this feature-rich interface to production.

Adding Input Validation and Error Handling for User Input

Input validation and error handling are crucial components of a robust terminal UI. As users input commands and data, it’s essential to verify their inputs against expected formats and rules to prevent potential security vulnerabilities and ensure the application behaves as intended.

To implement input validation in our Livewire component, we can utilize Livewire’s validate() method, which accepts the same rules as Laravel’s built-in Validator and automatically exposes any errors to the view. Because this component runs Artisan commands on the server, we’ll also restrict the command field to an explicit allow-list instead of executing whatever the user types. First, let’s add the input properties and a processCommand() action to our TerminalUI component:

// app/Livewire/TerminalUI.php

namespace App\Livewire;

use App\Models\CommandLog;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Validation\Rule;
use Livewire\Component;

class TerminalUI extends Component
{
    public string $command = '';
    public ?string $data = null;
    public string $output = '';

    /**
     * The only Artisan commands that may be run from the browser.
     */
    protected array $allowedCommands = ['list', 'about', 'route:list', 'migrate:status'];

    public function processCommand()
    {
        $validated = $this->validate([
            'command' => ['required', 'string', Rule::in($this->allowedCommands)],
            'data' => ['nullable', 'json'],
        ]);

        $arguments = json_decode($validated['data'] ?? '{}', true) ?? [];

        try {
            Artisan::call($validated['command'], $arguments);
            $this->output = Artisan::output();
        } catch (\Exception $e) {
            $this->output = "Error executing command: {$e->getMessage()}";
        }

        CommandLog::create(['command' => $validated['command']]);
    }

    public function render()
    {
        return view('livewire.terminal-ui', [
            'logs' => CommandLog::latest()->take(10)->get(),
        ]);
    }
}

Next, we’ll update our component’s Blade view with a form that submits to processCommand and uses Blade’s @error directive to display validation messages:

{{-- resources/views/livewire/terminal-ui.blade.php --}}

<div>
    <h3>Input Validation</h3>

    <form wire:submit="processCommand">
        <input type="text" wire:model="command" placeholder="route:list">
        @error('command')
            <div class="alert alert-danger">{{ $message }}</div>
        @enderror

        <textarea wire:model="data" placeholder='{"--except-vendor": true}'></textarea>
        @error('data')
            <div class="alert alert-danger">{{ $message }}</div>
        @enderror

        <button type="submit">Run</button>
    </form>

    <pre>{{ $output }}</pre>

    <ul wire:poll.3s>
        @foreach($logs as $log)
            <li>{{ $log->command }} ({{ $log->created_at }})</li>
        @endforeach
    </ul>
</div>

The log list from the previous section is kept at the bottom of the view, so every command you run appears there within a few seconds. By integrating input validation and error handling, we’ve significantly enhanced the robustness of our custom Laravel terminal UI.

Deploying the Custom Laravel Terminal UI to Production

To deploy our custom Laravel terminal UI to production, we’ll follow a straightforward process. First, ensure that your project is properly configured for deployment by running composer install and updating your .env file with the correct database credentials.

Next, create a new release on GitHub or your chosen version control platform. Then, run git push origin main to update the remote repository with our changes. On the server, install the production dependencies, build the front-end assets with Vite, and cache the framework configuration:

composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan optimize

These commands compile your CSS and JavaScript into public/build and cache your configuration, routes, and views. Livewire serves its own JavaScript through a Laravel route, so there’s nothing extra to publish for it.

With the assets in place, we can configure our Nginx or Apache server to serve the application from Laravel’s public directory. Update your nginx.conf or httpd.conf file accordingly.

server {
    listen 80;
    server_name example.com;

    root /path/to/project/public;

    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

After updating the configuration, restart your web server to pick up the changes.

Our custom Laravel terminal UI is now live in production! This concludes our seven-part tutorial on building a custom terminal interface with Livewire and deploying it to production.

Frequently Asked Questions

How do I troubleshoot the ‘LivewireServiceProvider not found’ error when trying to install Livewire in Laravel?

Check that you have installed Composer and run the command composer require livewire/livewire correctly, then run php artisan package:discover so Laravel’s package auto-discovery registers Livewire’s service provider. Also make sure livewire/livewire isn’t listed under extra.laravel.dont-discover in your composer.json, and clear any stale caches with php artisan optimize:clear.

What are some common pitfalls to avoid when building a custom Laravel terminal UI with Livewire?

Be mindful of input validation and sanitization to prevent SQL injection or cross-site scripting (XSS) attacks. Also, ensure that your Livewire components are properly updated and re-rendered in real-time.

How does this approach compare to using a third-party terminal tool like Terminalizer?

Terminalizer solves a different problem: it records terminal sessions and renders them as animated GIFs, so it can’t execute commands inside your application. Building a custom UI with Livewire provides more flexibility and customization options. With Livewire, you can create a tailored interface that meets your specific project needs.

Can I use this tutorial to build a terminal UI for an existing Laravel project?

Yes, you can follow the steps outlined in this tutorial to integrate a custom terminal UI with Livewire into an existing project. However, be sure to update any necessary dependencies and configuration files accordingly.

Comments

comments