Fafi Browser, a Racket-based Gemini client
Posted
Some months ago I started toying with the Gemini protocol in Racket. I think the project is interesting enough to share with the larger world.
The last few months have been a challenge for the whole world and I have been without much energy to work on anything besides work. The last two days I decided to revisit this toy and see how far I could advance it. I want Fafi to become a good choice for Gemini users.
Yesterday I spent the hours working on the protocol handling to make sure I can pass the client torture test suite. Fafi is not yet compliant with 100% of the tests but I think it passes 40+ of the 51 tests, which should make it good enough for a lot of use cases.
Today I worked on new features. Some features were asked by friends and others were just features I wanted to work on to learn more about Racket Graphical User Interface toolkit.
Thankful to Erkin and Gophwr
Fafi and I owes a lot to a Racket-based Gopher client called Gophwr made by Erkin. They were kind enough to not only release the software with a FOSS license but also to reach out and point me towards it. I have stolen a lot of code snippets from Gophwr as the clients are quite similar and have a lot of overlap.
Command line options
A friend asked to be able to pass Gemini capsules on the command line. I added it today alongside some options for setting a debug mode that outputs some debugging info while you’re using it.
Specifying a capsule:
$ racket main.rkt gemini://port1965.eu
Setting the debug mode:
$ racket main.rkt --debug
Tabs! π
I don’t know how common tabs are in Gemini browsers but Fafi has tabs now. The implementation was surprisingly straightforward considering I am a Racket and Gemini newbie. Tabs are kept on a simple Racket list. Each tab in the tab-panel%
corresponds to a tab
struct in the tabs
list. The tab
structure holds the URL and content for a tab. Since Gemini is stateless it is very easy to store the content in memory after fetching it.
(struct tab (url content))
(define tabs '())
Switching tabs is easy:
(define (switch-tab n)
(when (debug-mode?)
(begin
(displayln (format "switch tab: ~a" n))
(pretty-print tabs)))
(define tab-n (list-ref tabs n))
(send frame set-status-text "")
(if (non-empty-string? (tab-url tab-n))
(send address-bar set-value (tab-url tab-n))
(send address-bar set-value ""))
(cond [(false? (tab-url tab-n)) (send gemini-view clear)]
[(false? (tab-content tab-n)) (send gemini-view set-url (tab-url tab-n))]
[else (send gemini-view set-content (tab-content tab-n))]))
Menus
I kinda copied the menus of Gophwr but tweaked them to the feature set I current have. I basically wanted an easy way to quit the browser and also to launch a web browser to the source code repository and the issue tracker.
Keyboard shortcuts
Again, learned how to handle that with Gophwr. Copied and adapted some of their shortcuts. Then added ones for my own use case, mostly tab management.
- F5: reload
- alt+left: go back
- alt+right: go forward
- ctrl+w: close tab
- ctrl+t: new tab
- ctrl+q: quit
Launching web pages in the default browser
Web links present in gemtext pages will open in the default web browser.
My current favourite thing about this project
Racket GUI is growing on me but it is not my favourite part of this project. What I like the most is how I handle gemtext by converting it to s-expressions. After I have the little gemtext AST-like list in a variable it becomes so easy to work all the rest out.
I’ve learned how to do that by checking out the markdown-view%
by Alex.
Next steps
I need to figure out how I can find if the user is pressing ctrl
during a text%
clickback. I couldn’t find this out and to add that handler to the frame%
sounds like overkill to me. These are the two features I want to implement next:
- Bookmarks
- Table of contents
Source & Issue Tracker
Did you enjoyed reading this content? Want to support me?
You can buy me a coffee at ko-fi.
Comments? Questions? Feedback?
You can reach out to me on Twitter, or Mastodon, Secure Scuttlebutt, or through WebMentions.