Implementing Forms in Gemini

Back to SIG Miscellaneous Tutorials

Author: Gary Johnson

Date: 2022-02-01

There are several ways to implement forms in Gemini. After receiving constructive feedback on my "The Tragedy of &" message thread some time back, I made some enhancements to my Gemini server (Space-Age) to better support form logic:

Space-Age

I've built multiple interactive interfaces for use at my programming day job on top of Space-Age, including a simple Jira UI that lets me browse tickets by project, user, or arbitrary search term, create or delete issues, and edit their properties (e.g., status, assignee, summary).

In order to implement these kinds of interfaces, the trick is to re-imagine your user interactions through the lens of Gemini's 2 input methods:

Consider the following transformations from HTML form elements into Gemini:

HTML

 
 

 

 

 
View Profile Image

Gemini

=> $SCRIPT_PATH/username Username: $USERNAME
=> $SCRIPT_PATH/password Password: $PASSWORD

=> $SCRIPT_PATH/subscribed Subscribed: $SUBSCRIBED

* Message Format: $MESSAGE_FORMAT
=> $SCRIPT_PATH/message?every-message - Every Message
=> $SCRIPT_PATH/message?daily-digest - Daily Digest
=> $SCRIPT_PATH/message?weekly-digest - Weekly Digest

=> $SCRIPT_PATH/profile-image Set Profile Image
=> $PROFILE_IMAGE View Profile Image

In order to make this work in Gemini, your form page should be a CGI script (located at $SCRIPT_PATH - e.g., /my/cool/form), that defines the variables I listed above:

- $SCRIPT_PATH: "/my/cool/form"
- $USERNAME: "lambdatronic"
- $PASSWORD: "*********" (just a string with one * per password letter)
- $SUBSCRIBED: "yes"
- $MESSAGE_FORMAT: "Every Message"
- $PROFILE_IMAGE: "https://my-nextcloud-instance.org/lambdatronic-profile.png"

Now you'll need to add 5 route endpoints to your CGI script for the 5 form elements that we've included here. In the following descriptions, $QUERY is the value included after a ? in the URL. This can either be hard-coded (as in the Message Format example) or input by the user (as in the Username, Password, Subscribed, and Profile Image examples).

1. /username

If no $QUERY, return "10 Enter username".

If $QUERY, save this as the new username in your DB, and redirect to $SCRIPT_PATH by returning "30 $SCRIPT_PATH".

2. /password

If no $QUERY, return "11 Enter password".

If $QUERY, save this as the new password in your DB, and redirect to $SCRIPT_PATH by returning "30 $SCRIPT_PATH".

3. /subscribed

If no $QUERY, return "10 Subscribe? [y/n]".

If $QUERY, check to see if the user input "y", "Y", "n", or "N". If so, set the subscribed attribute to either yes or no in your DB and redirect to $SCRIPT_PATH by returning "30 $SCRIPT_PATH". If not, re-prompt the user with "10 You must enter y or n.\nSubscribe? [y/n]".

4. /message

As written above, this endpoint should only be triggered with a $QUERY parameter since they are all hard-coded in the form.

Grab the $QUERY parameter, set it as the new message format in your DB, and redirect to $SCRIPT_PATH by returning "30 $SCRIPT_PATH".

5. /profile-image

If no $QUERY, return "10 Input image URL".

If $QUERY, save this as the new profile image URL in your DB, and redirect to $SCRIPT_PATH by returning "30 $SCRIPT_PATH".

This simple example should give you most of the building blocks you need to write a CGI form in Gemini.

The only other piece you need to consider here is how to ensure that these various form interactions are associated with a particular user session. Gemini gives us essentially two ways to do this:

Client certificates

The blessed way of doing this is to require your user to authenticate with your form site by prompting their browser for a client certificate using a status 60 response. After they've done this, their browser will continue to pass the client cert alongside every other request coming to your server. Your CGI script can then use the cert hash as the session ID for reading and writing values to the DB.

Link-persisted session IDs

If you don't need or want to require users to authenticate with your form site, then you can have the $SCRIPT_PATH route generate a UUID (or other unique identifier for the session) and redirect to $SCRIPT_PATH/$SESSION_ID by returning "30 $SCRIPT_PATH/$SESSION_ID".

Have this route grab the $USERNAME, $PASSWORD, etc. values from the DB (which should initially all be empty strings) and display the form as written above.

Then implement the 5 route endpoints described above with the $SESSION_ID prefixed onto their routes (e.g., $SCRIPT_PATH/$SESSION_ID/username instead of $SCRIPT_PATH/username).

So to recap the routes in this instance:

Note that if anyone manually navigates back to $SCRIPT_PATH, a new session ID will be created and the form entry process will start over from the beginning.

Okay, that's pretty much it. When designing UIs in Gemini, remember that all the dynamic calculations are performed on the server side via CGI scripting (or whatever server-side programming model is provided by your chosen Gemini server), and the UIs use simple text markup with links and free-form text inputs as the two means of user interaction.

Your links can either hard-code values in them (like the session ID example or the multi-select message format example) or be used to prompt users for inputs that can either be visible (status 10) or hidden (status 11).

You can, of course, prompt for multiple values in a row by using redirects and persisting previous answers in your links like so:

$SCRIPT_PATH/username: "10 Enter username"
$SCRIPT_PATH/username?$QUERY: "30 $SCRIPT_PATH/username/$QUERY/password"
$SCRIPT_PATH/username/$USERNAME/password: "11 Enter password"
$SCRIPT_PATH/username/$USERNAME/password?$QUERY: "30 $SCRIPT_PATH/username/$USERNAME/password/$QUERY"

...and so on...

Finally, you probably also noticed that for uploading files (as in my profile image example), I find the easiest way is simply to prompt the user for a URL. This can point to any network accessible resource, whether over Gopher, Gemini, HTTP(S), FTP, etc. Your CGI script can then issue a file download for that URL and store the data on the server.

This means that your users can get any files they want onto your Gemini server as long as they can find a way to make them network-accessible. For some users, the simplest solution may just be to put them in their Dropbox or Nextcloud folders on their local machines, which will automatically upload them to their Dropbox or Nextcloud accounts. For other users, they may want to use FTP, SFTP, SAMBA/CIFS, or any other file sharing protocol to get them onto a machine that they control. The point is, you don't need to dictate this choice to your users. Everyone knows what a URL is. As long as they can give you a URL for their file, you can "upload" it to your Gemini server.

And that, as they say, is that.

Happy hacking in Geminispace!

Gary