Iced Prompt (Part 1)
This is the first part of a blog series documenting my efforts in building a command prompt / launcher using iced.rs in the rust language.
You can find the source code for this project here: github.com/floscr/iced-prompt
Introduction
The idea
I’ve always been fascinated with Command prompts, they’re super interesting interfaces that bridge the gap between CLIs and UIs.
Alfred was my first introduction to programming way back, writing my own little prompt scripts.
The target would be to write a gui that can be interfaced with a JSON api that’s accessible from scripting languages like javascript or Babashka scripts.
Previous solution
I’ve been using rofi on linux, which is a let’s you create prompts via scripts.
But there is no good way to create multi-modal prompts that can deliver dynamic content (e.g.: searching the web via an api). And you’re restricted to a simple ui.
The requirements
- Search applications on my system
- Allow dynamic content via scripting
- Multiple types of blocks
- Dynamic Evaluation (evaluate math expressions: 1 + 1)
Iced Framework
Iced
For this project I’m using the iced.rs framework, which is a cross-platform GUI library for Rust taking inspiration from the Elm Model View Update Architecture.
I’m still actively learning rust and iced during this project, so some code might not be ideal.
A simple example application
When you’re used to web development this will feel familiar to you.
In our main.rs
we’re going to write a simple application that let’s a user input text and show it again in some text below.
First we’re going to set up some imports:
Next we’re going to create some state for our view
Here we’re adding the Debug
and Default
traits #[derive(Debug, Default)]
.
Debug
will allow you to print the value with println!
and default will give us default values for our struct (in this case an empty string).
So you could log the state like this:
After this we will set up our messages types, which are identifiers for events like in Redux or any other state management framework.
And finally we’re going to create a static
id for our input, so we can later focus it in our update
function
Now it’s time to write the application implementation:
We’re setting up this impl
with some wanted feature traits
like messaging, theming, async execution and without any flags.
Next we’re changing the theme for this component:
Now we add an initialization method new
.
This function expects a tuple of the initial state and an initial event.
In our case we want our input reference by the id we’ve set up before to be focused on launch of the application.
Another requirement is to set up the title for the app.
Now we’ve arrived at the update
function, which responds to events and aplies changes to the State
.
In our case we simple update the query property in our string.
Finally we’re closing out the impl with our view
which is responsible for rendering our UI. Here we render our input state and dispatch Message::UpdateQuery
whenever the user types in the input.
Now the only thing left is to launch the application:
And we’ve got our application!
You can find the full source code here
The application
This part will go a bit more in depth about the implementation of the application, so it’s less beginner friendly compared to the iced intro.
State & Types
The basic structure for an item in my application is a Command
.
It’s a recursive data type to allow the user for infinite possibilities. But the most basic use-case will be, that Commands will have a list of actions that can be executed on it (Copy, Open with default application, etc).
Commands can be of multiple CommandKinds
which for now will always be a shell command type that justs owns the shell command to be executed.
The action will determine if the prompt will go to the next level via ActionKind::Next
or exit via ActionKind::Exit
.
Items will be a special data type which will host it’s nested Commands in a HashMap
for performan access and an order property (This will be important later, as we can override this to easily filter items or change the order without having to remap the inner commands)
History
The history will be a singly linked list that consists of Commands.
This data type is ideal, as we only display the last history slice to the to the user, and to go back we pop
off the current head.
The implementation for the list was taken from CrazyRoka/rust-linkedlist
Application State
Here’s all our state wrapped into a single struct.
The filter
will be used to override the current history layers order
.
Selection will be governed of the current user selection, to execute commands.
Deserializing with serde
To allow the user scripting access via a json
api we’re going to use the fantastic serde library.
With serde
you can safely convert json strings from the user to your wanted types, this works for simple types using the Deserialize
trait.
This will now accept json like this:
Deserializing the items array
Of course we don’t want our users having to map our detailed data type via the json
api, for this serde has some tools like aliasing and custom deserialization.
For the Items
data type we don’t want the user having to generate uuids and provide the structure with the nested order and items HashMap
.
While the datatype looks like this:
We want the user api to be a simple array like this:
We can achieve this with our custom deserializer for items:
This requires a bit of verbose setup:
But the actual implementation is quite simple, we loop over the array and insert each item with a uuid in our data structure:
We can now use this method with the deserialize_with
trait like this:
Other traits I’ve used
Default: Will infer the default for missing properties in the json, in this case it will infer []
Alias: Easy aliasing so you can accept the origin property key as well as the alias.
Thoughts
The traits make deserialization nice to implement, but the api can get quite hidden from the developer.
A future consideration might be to create a translation data type that directly translates to the user json api.
Next steps
Phew, this post required quite a lot of setup 😅
The next parts in this series should be a lot shorter.
As next steps I want to:
- Add dynamic commands that evaluate directly when typing
- More command block types
- Themes
- Server / Client architecture (Keep the main process alive)
You can find the source code for this project here: github.com/floscr/iced-prompt