Planet Scheme

Friday, September 4, 2026

crumbles.blog

WTF is going on with R7RS Large? 2026 edition

Do come in and sit down. Pour yourself a cup of tea – I’ve just made a fresh pot.

It’s been quite a ride since we last met to talk about these things. I became Scheme chair, but I’m not any more. Indeed, I’ve had to quit Scheme entirely. Let me tell you how this happened.

Obviously, I’m a lot more personally (and emotionally) invested in telling the story this time than I was last time. It should go without saying that, like last time, this is my own view on what happened.

The story so far

In the beginning, Scheme was created.

Scheme was born into a very sexist environment in the CS and AI laboratories at MIT. A 1983 report, Barriers to Equality in Academia: Women in Computer Science at MIT, describes how women working and studying in the labs were systematically treated as less qualified than men, their input less valued then men’s, and they were routinely sexualized and insulted by their male colleagues. This is the environment Scheme grew up in before it made its way out of the lab and into the world: the Structure and Interpretation of Computer Programs curriculum had been trialled and developed at MIT over the years leading up to the report; the first edition of the textbook would be published the following year.1

Unsurprisingly, the experiences of the women at MIT in the early 1980s could just as well describe the experiences of women in the free software community today. Indeed, despite a strong set of recommendations in the report, MIT itself never really improved by any account, continuing to employ (for example) Richard Stallman despite women at MIT and in the Cambridge/Boston hacker scene having warned one another for decades about his tendency to make unwanted advances.

What does Scheme carry of the legacy of the place it was born? Scheme and the free software movement were both born in the same place, of the same culture – usually something I say with a sense of familial solidarity, but make no mistake, the culture we both inherited has its more shameful aspects too.

I’m not going to answer this question directly. Just bear it in mind as I tell my story.

What happened, then?

As I mentioned last time, the old steering committee – the body responsible for chartering the Scheme working group and appointing the chair – was something of an absentee landlord. Plans to have regular meetings never panned out. They could be reached for emergencies, but it became clear that the working group wanted things that would necessitate some renegotiation of our rules of engagement – a revision to the working group’s charter – and the old steering committee was not in a competent position to be able to do this. So I suggested to the WG that we asked them to resign, which they did, and hold a new election, which they tasked us on the WG with organizing.

Meanwhile, in parallel, SRFI 261 was finalized. There are many, many problems with this SRFI;2 I raised many issues before finalization, but was ignored (because I say too much – oh, sorry, what was that about sexism again?). But the most troubling was that it seemed to make incompatible amendments to previous SRFIs, including my own, without the consent of their authors, in a way which would also affect the metadata about the entire SRFI database maintained by the SRFI editor.

Arthur Gleckler is, since 2015, the only SRFI editor. So I asked him to adjudicate and give his view on how this SRFI affected previous SRFIs and how he would maintain the SRFI metadata in future.

Glecker … ignored me. He just didn’t give a clear answer to the questions I had. Despite being as clear as I could that I was making two specific procedural objections to what SRFI 261 seemed to do to my own previous work, Gleckler seemed to conflate this with the other problems I had with the SRFI (which I agreed were within the SRFI author’s personal power to maintain through to finalization).

To me, it was clear that some mediation was needed, so I intended to ask the incoming steering committee to help. However, Arthur then accepted a nomination to the steering committee, which would mean it would no longer be neutral between the two of us. So I asked him from whom he considered his authority as SRFI editor to derive, and consequently if he had an idea who would be a suitable mediator – a question he explicitly refused to answer, accusing me of being underhanded for even asking it.

As the election progressed it became clear that we would only have three candidates for the three positions on the steering committee: Marc Feeley (Gambit; on the previous SC as well, and its most active member); Shiro Kawai (Gauche); and Arthur Gleckler (aforementioned SRFI editor). Seeing what lay ahead, I sent the other two candidates a gloomy email saying I didn’t see how I could continue working on Scheme if Gleckler were on the steering committee, and that I would have to warn vulnerable people to stay away as well if that happened.

Wait, what? Was there something else going on here?

Yes, let’s take a skip backwards in time to when I started as chair. Something I intended to do as soon as possible – knowing that I would regret it if I didn’t do it before it was actually needed – was put a code of conduct for the working group in place with a team to enforce it.

I took too long to do this. While I was still in the process of consulting experienced people for advice on setting up a moderation system, there was an incident. I can’t go into details because the person involved asked for privacy, but I tried to deal with it as best I could despite having failed to put in place a clear set of rules in time. Unfortunately, I made the mistake of getting Arthur Gleckler involved because of his role as SRFI editor, as well as the (old) steering committee.

Gleckler downplayed and defended what happened, even after seeing the offensive communications in question. He was not swayed by appeals to the law, which may well have made the issue a criminal matter in the jurisdictions of the people involved if the victim had chosen to push the case, and which at the very least would require a reprimand in any professional environment in any country with a semblance of workplace antidiscrimination law.

Ultimately, the person on the receiving end of this abuse left the Scheme community entirely.

To say I consider this a personal failure is an understatement. It weighs on me greatly. I am thoroughly ashamed of what happened in that case, on my own watch, and realize in hindsight that I should already have known that involving Gleckler was a bad idea from the outset.

Determined not to let something like this happen again, I put a code of conduct in place as quickly as possible. To get it done fast and without inviting controversy, I chose the standard and widely adopted Contributor Covenant,3 and I gave the (old) steering committee the task of enforcing it. This effectively delegated the moderatorial role the charter foresees for the chair back to the SC itself. I delegated this task for two reasons.

First, the chair is the most powerful in the working group and shouldn’t be accountable only to themself. (We’ll return to the subject of accountability later.) I also know that I have a prickly personality sometimes and I wanted to make sure there was a recourse for people to go to someone else and ask them to talk to me on their behalf if I upset them.

But also, I wanted protection myself. As a trans woman in a high-profile position, I am very well aware of the danger of online transmisogyny. I could only credibly have protection if someone else were ultimately responsible for it: issuing formal warnings or bans in response to attacks on oneself only invites further escalation. A third party is needed.

The old steering committee wasn’t active much, but they did respond to urgent matters – and they were, under the charter, the course of appeal on moderatorial issues anyway. Also, they were detached enough from the active WG members at the time that there would be a reasonably low risk of them defending personal friends and colleagues, rather than dealing justly. So I made them the first instance for code of conduct issues. In case they were too lethargic, I also wrote that if someone emailed them with a concern and there was no response, they could ask me or Marc Nieper-Wißkirchen to give them a poke and get them to act, which had also proven effective in the past.

But, with a new steering committee on the way in, and the very person I least trusted with sensitive matters due to be on it, it now seemed a very bad choice indeed to have the steering committee responsible for moderation and awareness.

But how did you actually get fired, then?

From December 2025 I was on leave from the chair anyway, initially due to health issues. In my absence, acting chairs took over organizing and running meetings. For various reasons including an increased workload at my paid employment and at Codeberg, my leave ended up being much longer than expected, but the WG made excellent progress while I was away.

After informing Marc Feeley and Shiro Kawai, the only other candidates, that I wasn’t feeling great about Gleckler being on the steering committee, at FOSDEM 2026 I took the opportunity of getting some sober advice from some trusted friends in the Scheme community. They suggested that if Gleckler was going to be on the SC, since I was the one who gave the job of moderation to the SC in the first place, I should take it away from them and set up a seperate moderation team. This seemed a good plan.

Also, in February – after I had already publically asked Gleckler why he intended to take a position on a committee which I had told him I wanted to take on a neutral mediation role between us – Gleckler did finally answer my questions about SRFI 261 to my complete satisfaction, in a private email. His answer did, in my view, contradict the stated opinion of SRFI 261’s author, and I still wonder why he never answered publically.

There were, indeed, only three candidates for the SC in the end, so Marc Feeley, Shiro Kawai, and Arthur Gleckler took office as the new, self-selected steering committee without being voted in by the Scheme community.

In July I looked at my calendar and felt that I could finally dedicate enough time to Scheme again, so I announced my intention to return to the chair, taking the next meeting for which the acting chairs had already surveyed potential dates. Meanwhile, I asked Sebastian Crane (many years chair of the board of F-Droid) and Peter Brett (Schemer and vice president of the Information Technology and Telecoms sector at the Prospect trade union) about joining a new moderation and awareness team, and they graciously accepted; we discussed who might make a suitable third member.

A few days after issuing the invitation to the meeting, I received an email from Marc Feeley inviting me to teleconference with the steering committee to discuss unspecified ‘urgent matters’. This email arrived on the day I was travelling to Electromagnetic Field – which was no secret (my Träwelling activities were also linked from my Mastodon account, my most active online presence, which likewise contained many posts about my travels in the UK). I likely didn’t even see it until I got back. Moreover, after EMF I got home and found I had caught some feverish sickness while there, and took a week to recover.

While the email mentioned the matters the committee wanted to discuss with me were urgent, there was no indication of a deadline for a response, nor that failure to respond promptly would be a sacking offence, nor any mention of previous attempts by the SC to contact me. It seemed prudent to me to wait until after I had recovered from my sickness – even until after my first meeting back in the WG chair, since I wanted to start to discuss with the WG the charter revision we desired.

Nonetheless, the Sunday before the planned meeting, I woke up from catching up on the sleep I missed due to EMF and EMF Flu and found I had been very publically fired. The given reason was that I had allegedly been ignoring the SC’s communications with me; they said they’d wanted to organize a status update meeting with me for a while, but I had been unreachable.

I had told the working group, including Gleckler, that I was on leave, and acting chairs were in place during that time. It is still unclear why they considered it so important to talk to me during my leave and didn’t talk to the acting chairs. In any case, both the steering committee and I are agreed on a timeline of their supposed attempts to contact me: their view is apparently that they appropriately communicated the urgency of their desire to speak with me personally; my view is that at no point did they mention urgency at all, except in their last email, nor any issues other than the one which Gleckler himself knew was already resolved.

This would also be a good time to remind you that when I put myself forward as a candidate for chair three years ago, I requested (very modest) remuneration for the position so I could guarantee I would be able to spend a reasonable minimum amount of time on Scheme matters. The old steering committee – which, again, included Marc Feeley, who should have been able to recall this – said they were unable to organize funding, but we agreed on a compromise that I would take the chair on the condition that they would not hold me to any deadlines.

After being fired, and following consultation with the acting chairs, the WG meeting took place with a different agenda than originally planned. I invited the steering committee to this meeting, hoping that a discussion between the working group and steering committee would clear the situation up and allow us to find new agreement and move forward as quickly as possible. Marc Feeley replied to the invitation 45 minutes before the planned meeting time, not explicitly declining the invitation but only repeating the committee’s excuse for having fired me and affirming the decision was final. (My invitation did not request my reinstatement nor anything like it.) In the end, nobody from the steering committee came to the meeting.

At an almost full meeting, I explained the situation as I saw it, and at the end, the WG nearly unanimously resolved to request my reinstatement.4 They also asked for a permanent vice chair, an idea I strongly agreed with as it would also increase the number of people with all the necessary information about the project organized in their head.

Both these requests were refused by the steering committee. When I pointed out privately that the WG has the authority to appoint its own officers apart from the chair, they said they would nonetheless not recognize the position of vice chair.

One of the acting chairs, who had been suggested as a candidate for the next chair, asked what the steering committee’s view on deadlines and the time commitment they expected, given they already broke the promise made to me by the last steering committee. But the steering committee had gone on holiday for the rest of the month and didn’t answer. The question is still unanswered nearly a month later.

The steering committee, at this point, seems to be refusing or ignoring every request the WG and its members made of it.

Finally, I received a private response to my email informing them that I had intended to set up an external moderation and awareness team and that, as ex-chair, I would be advising the new chair to do the same. They said they would not allow the WG to do this and considered themselves the only appropriate body to deal with code of conduct issues, once again unilaterally overruling the WG’s authority to appoint its own officers under its charter.

As I had made clear to Feeley and Kawai since January, and repeated in my mails to them since, if the only recourse in cases of harassment involved Gleckler, given his tendency to ignore problems, I would no longer feel safe in the working group and would have to warn others against joining it. Since they have made clear they intend to offer no other option nor allow any other option to be offered, and have given no assurances of having any system in mind to ensure that what happened last time won’t happen again, I left the working group and am no longer contributing to Scheme.5

What the fuck

So, what went wrong here?

Evidently, the new steering committee is arrogant. It seems to believe it knows best, and seems to intend to ignore what the working group wants, despite the working group having gained so much experience in what it needs in order to do its job best.

As for why I was fired … well, there’s good reason to doubt the official explanation, especially since the steering committee fired me and then immediately themselves did the thing they supposedly fired me for. I suspect the meeting they wanted to hold with me, the one they invited me to while I was at EMF Camp, was to tell me privately I was fired before announcing it publically or allowing me to even chair a single meeting after returning from leave. But that only deepens the question of what the real reason was.

The only explanations that have occurred to me: there is some accusation about me going around that the steering committee knows of but won’t tell me about (but they’ve denied this and I no longer think it’s likely); or the steering committee was unimpressed that I went on leave for so long (which would mean the official explanation at least contains and element of the truth, but then why don’t they say that, then? possibly because Feeley knows that would break the promise made by the last steering committee regarding time commitment and deadlines – but as if the reason they did give didn’t break that promise?)

As for the potential motives of any individual members:

  • Arthur Gleckler has a clear reason to want me gone, since from his perspective I not only tried to block him from the steering committee, it also seems that in his eyes I’m also some kind of Woke Stalin looking for people with wrong opinions to send them to the gulag. (Since he considers the ‘opinions’ in question to include unsolicited detailed speculation about my sexual desires sent to me by email, he is likely to be implacable on this point.) I have no idea why he became so passive-aggressive in December 2025 and started ignoring my questions, though, which is really what started this whole problem between us.

  • Marc Feeley’s role in all this puzzles me. He seemed to genuinely value my contributions to Scheme standardization, and to be very sympathetic to my concerns about safety, yet ultimately was the one who fired me and then refused the invitation to discuss the matter further with the working group. Since he was on the old steering committee, he’s also the one who broke the promise to me about no deadline; and the only one who has definitely seen first-hand Gleckler’s tendency to downplay and defend abuse. Yet he apparently stands behind the policy that I and others have to trust him to deal with problems if they arise, and took this position in the knowledge that this would leave me with no option but to leave, despite telling me how much he valued my work.

  • Shiro Kawai remains a complete wildcard to me. I know very little about him, and have not interacted with him beyond occasional discussions of technical matters, then a few emails since he joined the steering committee.

I’m also still concerned about the number of unaccountable positions of power and responsibility Arthur Gleckler has got for himself. The Scheme community is very dispersed, owing to the large landscape of different implementations, but a few things exist at the crossroads of the community, and Gleckler is now involved in all of them:

  • The Scheme.org website is ‘run according to a charter that aims to serve the whole Scheme community’. The charter names three people responsible for various aspects of running the site, including Gleckler. There is no indication of how these people are accountable to the ‘whole Scheme community’ they claim to serve.

    The domain name schemers.org redirects to scheme.org, but it’s not clear whether the rules listed in that charter apply to schemers.org and its subdomains (including srfi.schemers.org) as well. If they do, srfi.schemers.org is in violation of the rules (‘No embedded content from commercial services’) because it embeds Google Fonts.

  • The SRFI process, when it was set up in 1998, originally always had multiple editors, who could at least act as a check on one another’s work. Since 2015, Arthur Gleckler has been the only editor. He has explicitly refused to answer the question about who he is accountable to in this role, and who has the responsibility to appoint new SRFI editors. He has pointed out that he was the only one who volunteered to run the SRFI process when it would otherwise have been closed down in 2015, but to my knowledge, he has never made any attempt to find someone else to work with to restore the situation pre-2015 where multiple editors worked together.

  • Now Gleckler is on the steering committee as well, which is accountable to no one, serves as long as it pleases, and decides for itself who will serve on it and how they will be chosen. Like the old committee of SRFI editors before Gleckler took it over completely, they are at least accountable to one another, but towards the working group they are currently being secretive and intransigent. The working group asked for a fixed term before the new election started, mostly because of the bad memory of the previous committee which had been in office so long most of its members seemed to have lost interest, but a fixed term of office would also give the community explicit control over it.

    Given the steering committee’s attitude to the working group’s requests so far, I am doubtful we will see this happen.

    In practice, the steering committee is also somewhat accountable to the working group, who could just tell the steering committee to get lost and strike out on its own instead. (This was suggested – not by me – in response to my sacking.) Less dramatically, as we saw last year, the working group can also ask the steering committee for a new election, but the steering committee has no obligation to acquiesce. Also, right after an election in which there were 20 nominations and only three acceptances – a real indictment of the state of the community and the perception of its leadership positions – is not the right time to try and roll the dice again.

    As for Gleckler himself, he was quite upset that I tried to stop him accepting ‘a huge honor’ (his words) by pointing out that he was trying to join a group I had said I wanted to act as a neutral forum between us, and that this would prevent it from being neutral. The fact that he could have avoided that conflict by just answering my damn questions seemed to escape him, as did the idea of the steering committee as a responsible oversight body and not just another shiny hat for him to wear.

As chair, I suggested to the previous steering committee that Scheme needs a legal entity to manage the costs of running the digital infrastructure for these things the whole community depends on, and to ensure there are clear and enforceable rules about who is involved in running them and how. They declined to help set one up. Other Schemers have attacked the idea with a straw man, saying we’ll never get all the implementations to agree to be controlled by a single organization (though that’s not at all what this is about).

Well, now everything is controlled by a single entity. His name is Arthur Gleckler.

Well, then

So that’s the story of how the first woman ever to be involved in Scheme standardization came to be fired after she tried to put effective antidiscrimination and accountability measures in place.6

Draw from that what you will.

Think of me, perhaps, as Scheme’s Khrushchev: my greatest contribution is creating the environment in which I could be fired. Now let me retire to my dacha in peace.

What pisses me off the most is how this whole rigmarole has undermined the work I did over three years to try to slowly, slowly win back the trust of the Scheme community in the standardization process, after it was damaged first by the split over R6RS, then by R7RS Small’s decision to deepen the split by throwing away R6RS instead of making a compatible subset, then by years of dawdlingly slow progress on R7RS Large. I had to beg on stage for standardization work to be taken seriously again. Just as progress began to be regular and visible, everything was very publically thrown into chaos again. But don’t take it from me – ask your local Scheme implementer.

Oh, the teapot’s empty. After all this I fancy we both need something a bit stronger. Scotch?


If one meets a powerful person […] one can ask five questions: what power do you have; where did you get it; in whose interests do you exercise it; to whom are you accountable; and, how can we get rid of you? Anyone who cannot answer the last of those questions does not live in a democratic system. — Tony Benn


  1. It should go without saying that I am not making any direct personal accusations against Steele, Sussman, or Abelson. This is a comment on the culture of their workplace at the time, in which many influential early Schemers got started. I have no knowledge of their own individual actions within that workplace and whether they were a part of that culture’s problems or not.↩︎

  2. A major difference between SRFIs and similar processes like Python’s PEPs is that finalization of a SRFI implies approval by nobody other than its own author(s) and, to a lesser extent, the SRFI editor. It’s up to the Free Marketplace of Ideas and each individual Scheme implementation to decide which ones they want to support. So bad SRFIs get finalized sometimes – so be it. The problem I’m about to describe is a different one.↩︎

  3. I regret this in hindsight, too. While the Contributor Covenant is standard, it is also therefore the standard choice for projects which just adopt a code of conduct to do the minimum needed get the ‘SJWs’ to shut up, without actually doing any of the hard work of reflection and accountability. Whether this unfortunate aspect of the Contributor Covenant played a role in what would follow, I can’t say.

    I wanted to rewrite the code of conduct on the pattern of the one used at the Gulaschprogrammiernacht which is one of the most powerfully worded I’ve seen and, together with a strong presence of a trained awareness team throughout the whole event, created a genuine impression of care for the safety of attendees. (Other conferences take note.) I wanted something as powerful as that for my own WG. I didn’t get around to this, though.↩︎

  4. That ‘nearly’: one member was absent; one member was present in the IRC channel but didn’t say anything the whole meeting; one member expressed uncertainty, and later told me they thought I had ‘lost perspective’ because I raised the possibility that the steering committee might have had some other reason than claimed for wanting to dismiss me. All other members expressed support for my reinstatement. The log of the meeting is available to verify this.↩︎

  5. Practically, I would probably have had to leave the working group anyway for political reasons. There’s a reason ex-prime ministers don’t stick around on the backbenches after they retire. As a popular ex-chair forced out against the working group’s wishes, staying in the working group would only risk undermining the new chair’s authority in any case in which we happened to disagree. This would obviously be a toxic and counterproductive dynamic, and to avoid it disrupting the working group’s progress, the only option was for me to leave the working group. However, I didn’t expect to have to warn others against joining it, nor did I expect I would feel forced to withdraw entirely from contributing to Scheme, including (for example) from the SRFI process.↩︎

  6. I know I was the first woman to write a SRFI because the automated email sent out when it went to a last call for comments misgendered me – in 22 years previously, nobody had ever thought that the author of a SRFI would have a pronoun other than ‘he’. (Update: Mike Sperber, SRFI editor from 1998 to 2015, got in touch to say that during his term, there was no automated email which assumed anyone’s gender. He also confirmed there were no female authors during that time.)

    I would have been the first woman to have my name at the top of the report, and not merely in the acknowledgements section. In the acknowledgements section, as far as I know, there have only ever been two women’s names among the dozens of men’s names: Julie Sussman, wife of Gerry and contributor to SICP, and Betty Dexter who typeset RRRS in TeX back in 1985 (and whose name was removed from the R7RS Small report despite it still clearly being derived from her original TeX code).↩︎

Friday, September 4, 2026

Wednesday, September 2, 2026

Scheme Requests for Implementation

SRFI 281: Bytevector Utilities

SRFI 281 is now in draft status.

This SRFI implements a modified version of the R6RS’s (rnrs bytevectors (6)) library, which has procedures for the serialization and deserialization of numbers into bytevectors, and conversion to/from Unicode. It also includes some procedures from SRFI 207 that are relevant to all bytevectors, such as serialization/deserialization into strings, and order predicates.

by Peter McGoron at Wednesday, September 2, 2026

Monday, August 31, 2026

Scheme Requests for Implementation

SRFI 262: Extensible pattern matcher

SRFI 262 is now in withdrawn status.

A pattern matching form which can operate on arbitrary Scheme values is defined. It conforms to the following design principles.

The syntax of patterns is declarative. The syntax is extensible in the same way that Scheme’s procedural syntax is extensible by macros.

For most use cases, the use of the pattern matcher should produce code which is essentially as efficient at run time as equivalent procedural Scheme code would be, assuming a moderately optimizing Scheme implementation. This applies only when the equivalent code is also equivalently correct in terms of handling of error cases (i.e. including all type checks done automatically by the pattern matcher). However, using extension syntax should not cause this principle to be violated (provided the extension syntax is appropriately implemented).

by Daphne Preston-Kendal at Monday, August 31, 2026

Sunday, August 30, 2026

Scheme Requests for Implementation

SRFI 280: Monads

SRFI 280 is now in draft status.

Monads are a fundamental concept in functional programming. Schemers have been using them for a long time, albeit with ad hoc constructions only meant for specific monads. This SRFI deals with monads generally by extending Scheme with first-class monad objects and adding Haskell-like syntax for composing monadic computations. The API is minimal and the implementation trivial.

by Hernán Ibarra Mejia at Sunday, August 30, 2026

Saturday, August 29, 2026

jointhefreeworld

Quest for the eternal Dock - lambdock

This is the story of lambdock: https://codeberg.org/jjba23/lambdock

lambdock is a modern, hyper-hackable, Wayland-native desktop dock application (C and Guile Scheme + GTK4) w/ REPL.

As a heavy computer user, spending most of my day enjoying digital life, and as an eye-candy enjoyer and workflow optimization afficionado, I have long dreamt of the ultimate desktop dock.

Here’s the story behind lambdock , the Wayland-native beast with infinite hackability, fluid physics animcations, instant responsiveness, and free as in freedom .

But to get here, I will first take you through the mindset, the graveyard of failed prototypes, the uphill battles and the ultimate conquest of GTK4 C runtime, and embedding of a living, breathing, interactive Lisp heart.

Quest for the eternal Dock  #

For years, desktop GNU/Linux users moving to Wayland faced a recurring tragedy: the loss of iconic, deeply hackable docks like Cairo-Dock and Plank. That being said, anno 2026, those (and other) projects have made great efforts to become Wayland-compatible, so that’s great. While bar engines like Waybar and EWW excel at status displays, a true application dock that can rival those heavyweights (and macOS too) requires a unique blend of layout positioning, dynamic window tracking, auto-hiding, and fluid hover physics for animations.

lambdock’s is a story of resilience in many ways, as I cannot even remember how many attempts I made at building a dock (in different ways). Little did I know how many paradigms, ideas and PoCs would collapse before my vision became tangible reality.

lambdock showcase

On the 30th of July 2026, I had a revelation and set out to build lambdock: a Wayland-native desktop dock that wouldn’t just replicate the macOS or Cairo-Dock experience, but would transcend it with full runtime inspectability, hackability, and Lisp enlightenment.

I find GTK to be the very best UI toolkit for GNU/Linux and other platforms, anno 2026. So for me that choice was pretty clear, even if I also toyed with Qt and others, but GTK always came on top. After the wreckage of many PoCs and while reading some HackerNews in bed, a thought crossed my mind, and suddenly, absolute clarity!

Why not use idiomatic modern GTK4, in the language that it’s written in. Oh wait, that language, C, has libguile.h, a great library inter-operability with Lisp (GNU Guile Scheme), allowing bi-directional bindings and communication. Why not go down this rabbit hole, which might at the same time teach me more about my favourite language (GNU Guile) and allow me a frictionless setup with GTK and native super performance.

I therefore designed lambdock in my mind, in bed, much inspired by Emacs. A powerful, small C core that powers the program, rendering, graphics and low level details, and an embedded GNU Guile Scheme engine that at runtime is the heart and brain of the whole thing. This architecture uniquely delivers interactive socket REPL control, Lisp metaprogramming macros, dynamic multi-dock spawning, native Wayland foreign-toplevel window tracking, and frame-clock-driven hover animations.

Graveyard of Prototypes  #

The journey to lambdock was paved with ambitious Proofs of Concept (PoCs) that ultimately died. I cycled through several languages, approaches, frameworks, and architecture experiments, chasing the dream of rapid development without sacrificing low-level display server control.

The quest initially aimed for a pure, 100% Lisp architecture using Guile GI and other existing Guile GTK bindings. However, this vision collapsed under sparse documentation and inscrutable binding layers. Bridging GTK’s imperative, object-oriented state with functional Scheme patterns created constant architectural friction, and a small community meant every binding edge-case became a dead end and a long dive in the rabbit hole.

Trying Python + GTK3/4 offered rapid prototyping and a massive library ecosystem, but crashed into severe real-time performance limits. Single-threaded bottlenecks and Global Interpreter Lock (GIL) stutters ruined fluid slide-out animations unless backed by custom C code. That also triggered me to think, might as well just write this whole thing in C. Coupled with a heavy memory footprint and fragile IPC mechanisms, the runtime proved too heavy and unpredictable for a low-latency desktop dock.

Attempts with Rust, GTK bindings, and custom compositor IPC promised memory safety and fearless concurrency, yet introduced severe verbosity and binding friction. Mismatches between Rust’s async event loops and GTK4’s main thread created structural problems, the borrow checker was a real PITA when working with GTK and the dynamic features I wanted to support, while the lack of reflection completely shut the door on (easily) embedding a live, interactive Lisp REPL.

I made a final attempt by using JavaScript/Node and Layer Shell targeted familiar web-style styling and asynchronous I/O. However, this suffered from bloated resource consumption, poor integration with low-level Wayland protocols, and no clear pathway for Lisp extensibility, and thus it quickly gained its place in the graveyard of abandoned prototypes.

Iron Skeleton, Lisp Soul  #

  • C + GTK4 + gtk4-layer-shell is the undisputed champion of native Wayland surface control. C provides raw speed, zero-cost GLib integration, memory layout efficiency, flawless Wayland scanner protocol generation and the best GTK documentation you can get.
  • GNU Guile Scheme ( libguile) is the ultimate runtime mind. Instead of configuring the dock with static, dead JSON or TOML files, embedding Lisp (Guile Scheme) via libguile.h gives the dock a living Lisp heart and turns it into an infinitely extensible program.
 /*  The moment C boots the Lisp engine in main.c  */
 int  main( int  argc,  char ** argv) {
 #ifdef DEFAULT_GSK_RENDERER
  g_setenv( "GSK_RENDERER", DEFAULT_GSK_RENDERER, FALSE);
 #endif
   /*  Boot the GNU Guile Scheme engine and surrender control to inner_main  */
  scm_boot_guile(argc, argv, inner_main,  NULL);
   return 0;
}

By hosting libguile directly inside C’s GTK4 main loop, lambdock achieves what I consider the holy grail, much like GNU Emacs does: infinite extensibility, uncompromising native performance for rendering and animation, and metaprogramming superpowers of Lisp for user configuration and live REPL inspection.



Etymology  #

The name lambdock is a play on words combining:

  • Ship Docks ⚓ where containers and applications dock safely.
  • lambda λ expressions from functional programming & Lisp enlightenment.
  • Lambs 🐑 (gentle, fluffy, lightweight, and clean).
  • Docks as desktop UI components (e.g., Plank, Cairo-Dock, macOS Dock).

lambdock adopts as its project logo the Agnus Dei: The Lamb of God carrying a cross and a red flag

You could say using this dock es casi una experiencia religiosa como la de Enrique Iglesias



Multi-Config & Multi-Dock Instance  #

lambdock features built-in multi-dock orchestration. Rather than being limited to a single dock bar, you can run multiple independent docks simultaneously across different screen edges or monitors.

  • Automatic Directory Monitoring: lambdock watches ~/.config/lambdock/ for files matching settings.scm or settings-*.scm (e.g., settings-left.scm, settings-bottom.scm).

Each configuration file defines its own isolated LambdockState, position ( dock-position), theme ( dock-theme), monitor targets ( dock-monitor), and item launcher layout ( dock-items).

  • Dynamic Spawning: Creating a new settings-2.scm file instantly spawns a new dock bar on screen.
  • Dynamic Hot-Reloading: Editing any settings-*.scm file hot-reloads that specific dock instance without flickering or restarting other running instances.
  • Dynamic Destruction: Deleting a settings-*.scm file safely tears down and destroys its corresponding dock window, removing Wayland handles and GTK widgets without crashing the application.


What systems does lambdock support?  #

lambdock is a GNU/Linux first utility.

The tool uses gtk4-layer-shell to render to the screen and positioning and for dock behavior.

That means it works well in Wayland compositors that support the wlr-layer-shell-unstable-v1 protocol, including:

  • KDE Plasma (Wayland session)
  • Smithay-based compositors: Niri, COSMIC Desktop
  • wlroots-based compositors: Sway, Hyprland, River, Wayfire
  • Mir-based compositors

Note: GNOME (Mutter) currently not supported due to not implementing the protocol



How the Bi-Directional Engine Works  #

lambdock is not merely configured by Scheme. It has an embedded, extensible Lisp engine and runtime environment which complements a high-performance C applciation core and graphics engine. Execution flows bi-directionally between C and Guile.

The entry point of the binary ( main.c) boots the Guile interpreter using scm_boot_guile. The C runtime acts as the host and invokes Scheme procedures to manage state and extract settings.

Scheme is not restricted to passive data declarations, as it can trigger actions inside the running C engine.

lambdock provides a beautiful Lisp DSL for defining your dock, based on items and presets

Scheme Constructor / Procedure Return Type Description Keyword Arguments
(app-item ...) Record Custom application launcher definition #:name, #:exec, #:icon
(dynamic-item ...) Record Dynamic polling widget displaying dynamic textual data #:name, #:icon, #:exec, #:poll-fn, #:interval-ms, #:hover-animate?
(preset-launcher 'symbol) Record Standard launcher resolved from internal preset list Symbol (e.g. 'emacs, 'alacritty)
(preset-launchers-for 'a 'b) List Batch helper returning a list of preset launchers Variadic list of symbols
(separator-item) Record Layout divider line None
(preset-icon 'symbol) String Resolves default icon name string for a given preset Symbol

The magic of lambdock lies in the seamless bi-directional bridge between the C graphics core and the GNU Guile Scheme engine. See an example config file:

 ;;  Modern, declarative Lisp configuration in ~/.config/lambdock/settings.scm
( define  dock-auto-hide? #t)
( define  dock-position 'bottom)
( define  dock-icon-size 48)
( define  dock-theme 'vanilla)
( define  dock-monitor 'all)

( define  dock-items
  (append
   (preset-launchers-for 'alacritty 'google-chrome 'spotify-flatpak 'nautilus)
   (list (separator-item))
   (preset-launchers-for 'emacs 'intellij 'bruno-flatpak)
   (list (separator-item))
   (preset-launchers-for 'ram 'cpu 'cpu-temp 'battery)))

 ;;  === you can evaluate any Lisp code here too ===

(use-modules (ice-9 popen)
             (ice-9 rdelim)
             (srfi srfi-19)
             (srfi srfi-1))

 ;;  Helper to run a shell command and return its output as a clean string
( define ( sh-output cmd)
  ( let* ((port (open-input-pipe cmd))
         (output (read-line port)))
    (close-pipe port)
    ( if (eof-object? output)  "" output)))

 ;;  Enable auto-hide only on a specific hostname (e.g., laptop setup)
( define  dock-auto-hide?
  (string=? (string-trim-both (sh-output  "hostname"))  "thinkpad"))


Read Eval Print Loop (REPL)  #

For purposes of development, experimentation and live hackability, lambdock features an embedded GNU Guile Scheme runtime. You can enable a background Unix domain socket REPL to query or dynamically alter the running dock’s state in real time.

Through Guile’s (system repl server), lambdock can spawn Unix domain sockets (one per dock instance) for example at /tmp/lambdock-repl.sock. You can plug directly into a running dock process via Emacs (Geiser), socat, or ncat.

Because GTK4 requires all UI modifications to happen on the main thread while the REPL listens on a background thread, lambdock uses GLib’s g_idle_add to dispatch Scheme-triggered UI updates safely:

 /*  Thread-safe C callback triggered from Guile REPL evaluation  */
 static  gboolean  on_manual_reload_idle( gpointer  user_data) {
  ( void)user_data;
  LOG_C_INFO( "Main",  "Redrawing UI from in-memory Guile state...");
  redraw_active_docks();
   return G_SOURCE_REMOVE;
}

 static  SCM  scm_reload_dock( void) {
  g_idle_add(on_manual_reload_idle,  NULL);
   return SCM_UNSPECIFIED;
}

You can edit a theme, change icon dimensions, or redefine launchers in Emacs, issue a (reload-dock!), and watch your dock transform live, without restarting or dropping a single frame!

In your ~/.config/lambdock/settings.scm file, set #:enable-repl? to #t. You can optionally customize the socket path using #:repl-socket-path (defaults to /tmp/lambdock-repl.sock):

(configure-dock!
  #:position 'bottom
  #:items (list ...)
  ;;  .... other settings
  #:enable-repl? #t
  #:repl-socket-path  "/tmp/lambdock-repl.sock")

You can then inspect or modify the dock state, and when desired also reload the dock (with reload-dock!).

Once connected, you can evaluate Scheme expressions against the running lambdock instance, possibilities are endless, e.g.

scheme@(guile-user)> (use-modules (lambdock core))
scheme@(guile-user)> (get-dock-theme) ;; -> 'vanilla
scheme@(guile-user)> (set-dock-theme! 'nature)
scheme@(guile-user)> (reload-dock!)

If you use Emacs with Geiser:

  1. Run M-x geiser-connect-local.
  2. Select guile as the Scheme implementation.
  3. Enter the socket path: /tmp/lambdock-repl.sock.

See here Emacs + Geiser GIF

lambdock showcase

You can connect directly from your terminal using different tooling, like nc, ncat, socat wrapped with rlwrap , and others:

socat - UNIX-CONNECT:/tmp/lambdock-repl.sock
ncat -U /tmp/lambdock-repl.sock
nc -U /tmp/lambdock-repl.sock
rlwrap socat - UNIX-CONNECT:/tmp/lambdock-repl.sock


Victory: Upstream in Guix, Nix, OCI and Beyond!  #

What began as a chaotic experiment in a local directory has officially arrived on the global stage.

lambdock is upstreamed and natively packaged in GNU Guix. You can run or install it in a single hermetic command:

guix time-machine --channels=channels.scm -- shell -f guix.scm -- lambdock

Fully upstreamed into Nixpkgs (today) and available as a Flake too:

nix --extra-experimental-features  'nix-command flakes' run .#

lambdock binaries and native packages are marching across the GNU/Linux ecosystem:

  • openSUSE / RPM: Packaged on OBS with native lambdock.spec.
  • Debian / Ubuntu: Full debian/ rules and unsigned .deb build pipeline.
  • Arch Linux: Ready-to-build PKGBUILD in packaging/arch.
  • Containers: Lightweight OCI images on DockerHub for Podman/Docker.


What does one learn after the Odyssey  #

  • Don’t fight the platform: If you are building a Wayland utility on GNU/Linux, consider embracing C and GTK4/Layer-Shell directly. The clarity, speed, and reliability are unmatched. Even if you might have to shoot yourself in the foot a couple times with memory management ☺️
  • Lisp remains THE supreme extension engine: Embedding libguile transformed a simple UI bar into an extensible and programmable canvas where users can run shell pipelines, system queries, and dynamic macros right inside their config files.
  • Don’t surrender to “good enough”: The failed PoCs were not wasted time, they were but the crucible that forged the ultimate architecture.

Long live Free Software, long live Lisp, and happy docking! 🐑⚓λ

Saturday, August 29, 2026

Thursday, August 20, 2026

Scheme Requests for Implementation

SRFI 274: Extended List Conversion Procedures

SRFI 274 is now in final status.

A set of modest extensions to list conversion and list copying procedures is proposed that aligns them with other conversion and copying procedures and allows for some operations on dotted and circular lists (collectively called improper lists). Authors of further SRFIs that include list conversion procedures are encouraged to align their behavior with the behavior in this SRFI.

by Peter McGoron at Thursday, August 20, 2026

Tuesday, August 18, 2026

LIPS Scheme Blog

Problem with Evaluating Scheme Expressions in Emacs

I was checking the new beta release of LIPS Scheme in GNU Emacs. I was searching for a way to execute an expression in Scheme mode when using the cmuscheme Scheme REPL in the other window. It turned out that it's standard C-x C-e (same as for elisp). The problem is that it displays the prompt multiple times when you execute code that doesn't produce any output.


see the rest of the article

Tuesday, August 18, 2026

Monday, August 17, 2026

The Racket Blog

Rhombus v1.1

Rhombus version 1.1 is now available!

We are pleased to announce Rhombus 1.1 is now available from https://rhombus-lang.org/.

Rhombus is a general-purpose programming language that is easy to use and uniquely customizable.

As of this release:

  • Add annot and annot.def as ways to define an annotation without directly writing meta-time (i.e., macro) code.

  • Add as as a binding form, which is sometimes more readable for naming than using && and provides a way to shadow an identifier that is bound as a binding form.

  • Change class to bind inherited names using the corresponding superclass or interface reference.

  • Change space.enforest to adjust scopes in the same way as for a macro transformer when applying an identifier handler.

  • ffi: Add an initialized-array variant of new.

  • pict: Change explain_anim to add a ~label_as argument. Change Pict.rebuilt to replace as rebuilt, and also add a ~as_rebuilt argument to select the old or new bevavior. The Pict.rebuild method also supports ~as_rebuilt. Improve magic_move and cross_fade to better handle paragraph points and multiple instances of a child pict.

  • slideshow: Add slide_transition and continued page numbering.

Thank you to community members who contributed this release.

Feedback Welcome

Questions and discussion welcome at the Racket community on Discourse or Discord (#rhombus channel)

Anyone can participate in Rhombus design discussions. The Racket team’s unofficial motto is anything we can do, you can do: programmers should feel empowered to participate in the creation of the languages they use. Discussions, pull requests, and issues are open to all, and a wide variety of perspectives is especially beneficial.

by Matthew Flatt at Monday, August 17, 2026

LIPS Scheme Blog

LIPS Scheme 1.0.0-beta.22 with Continuations and TCO

I'm excited to introduce a new beta version of LIPS Scheme. The most important features of this version are full continuations and TCO (Tail Call Optimization). They were inspired by JS-Scheme by Alex Yakovlev.


see the rest of the article

Monday, August 17, 2026

Thursday, August 13, 2026

The Racket Blog

Racket v9.3

posted by Stephen De Gabrielle and John Clements


We are pleased to announce Racket v9.3 is now available from https://download.racket-lang.org/.

As of this release:

  • The raco setup command can generate markdown documentation, using the --doc-markdown option.
  • The "#lang" teaching languages (BSL, …, ISL+; plus DeinProgram) have reached parity with the ones chosen using the Language dialog, and are the recommended choice.
  • DrRacket’s background expansion disables errortrace annotations, for faster syntax checking.
  • The raco pkg install command includes new options that provide more install-time configuration flexibility: --adjacent-deps, --destdir, and --attach, and a refined --skip-installed.
  • The ffi/unsafe/runtime-lib library provides a define-runtime-lib mechanism similar to define-runtime-path, allowing location of libraries located relative to a source file.
  • The prompt-tag/c contract generator no longer performs checking on call/cc when the #:call/cc option is not present.
  • The impersonate-prompt-tag function takes an additional argument that allows checking and update of results for composable continuations.
  • The error-syntax->srcloc-handler parameter provides control over the mapping from syntactic forms to source locations for error handling.
  • Uses of (tcp-listen 0) will retry when it fails with “address in use”.
  • The racket/base module requires fewer internal modules and instantiations.
  • The file/zip package provides a new mechanism for greatly increased control over zip file generation, allowing in-memory file sources and per-file compression control.

Thank you

The following people contributed to this release:

Alex Knauth, Alexander Shopov, Aris Spathis, Bert De Ketelaere, Bob Burger, Caleb Mazalevskis, Cameron Moy, Geoffrey J. Teale, Gustavo Massaccesi, Hannes Braun, Jade Sailor, Jason Hemann, Jens Axel Søgaard, John Clements, Jordan Johnson, Matthew Flatt, Matthias Felleisen, Mike Sperber, Nathan Dykman, Noah Ma, Philip McGrath, Robby Findler, Romeo Ahmed, Sam Tobin-Hochstadt, Shu-Hung You, Stefan Schwarzer, Stephen De Gabrielle, and Wing Hei Chan.

Racket is a community developed open source project and we welcome new contributors. See racket/README.md to learn how you can be a part of this amazing project.

Feedback Welcome

Questions and discussion welcome at the Racket community on Discourse or Discord.

Please share

If you can - please help get the word out to users and platform specific repo packagers

Racket - the Language-Oriented Programming Language - version 9.3 is now available from https://download.racket-lang.org

See https://blog.racket-lang.org/2026/08/racket-v9-3.html for the release announcement and highlights.

Thursday, August 13, 2026

Tuesday, August 11, 2026

Scheme Requests for Implementation

SRFI 279: In(tro)spection Protocol

SRFI 279 is now in draft status.

Interactive REPL-driven systems (that most Schemes are) need a way to get detailed information on a given piece of data. Inspectors, as these are conventionally called. This SRFI defines a basic protocol for inspectors, consisting of three procedures: inspect-properties, inspect-property, and inspect-describe. Some suggestions for standard and popular types’ inspection are also provided.

by Artyom Bologov at Tuesday, August 11, 2026

Monday, August 3, 2026

Scheme Requests for Implementation

SRFI 273: Extensions to Data (Type-)Checking

SRFI 273 is now in final status.

The original SRFI 253 established a basis for type-checked (or otherwise checked) data handling. But it lacked some quality-of-life features. This SRFI extends SRFI 253 to match existing implementation practice and common sense. Provided extensions are: check aliasing with define-check; pre- and post-declaration of type / check with declare-checked; return value checks in lambda-checked, case-lambda-checked, and define-checked; and some optimizable, supported, and explicitly unsupported patterns suggested to implementors.

by Artyom Bologov at Monday, August 3, 2026

Friday, July 31, 2026

Gwen Weinholt

The State of Chez Scheme in Debian

I have uploaded Chez Scheme 10.4.0 to Debian unstable. It has been a few years since there was a new Chez Scheme version in Debian, and that is all on me. 😅

The new release builds fine on all architectures according to the build logs. In case you missed it, Chez Scheme got an infusion of energy from the Racket people and gained portable bytecode support a few years ago. So for those architectures where there is no native backend, Chez instead generates portable bytecode.

There was a problem with m68k and hppa where they would sometimes get the wrong endianness for the portable bytecode, possibly depending on which buildd picked them up. But that should be fixed now as debian/rules constructs the machine type from Debian’s build variables.

Debian Scheme Dream Team

I moved the package to the Debian Scheme Dream Team! So now there are more people who can help maintain it. The team has been gathering some mass recently, which is really nice to see. I hope that together we can make Scheme a stronger language in Debian.

Cross-compilation was broken

I enabled cross-compilation from amd64 to arm64 in the Salsa pipelines and found that it was actually broken! The problem was that cross-compilation kicks off a secondary build where several of our build parameters were missing. So the secondary build couldn’t find zuo and also got the wrong C compiler.

This has been fixed by patching build.zuo. This patch should be upstreamed.

Future work

The chezscheme-dev package is not something I have actually tested myself. It ships libkernel.a, main.o and scheme.h. Could be working, nobody has ever said otherwise. :)

Then there are the portable bytecodes! It would be possible to de-dupe those in the archive. They could be built as Architecture: all packages and be reused. Now, e.g., sparc64 and ppc64 both build threaded 64-bit big endian bytecode, so those exist at least twice in the archive.

Reproducible builds

Last, but not least, Chez Scheme builds are not reproducible. This is becoming a real problem now because Debian’s release team has made reproducible builds mandatory. Chez Scheme will not be part of future Debian releases unless this gets fixed.

Thankfully it does seem to be fixable. The root of the problem is that unique identifiers are used to support separate compilation. If anyone’s interested in the background then they can check out Oscar Waddell’s Ph.D. thesis (warning: .ps.gz file).

The implementation described in Section 3.5 supports both internal and top-level modules. For internal modules, the new names generated by the expander must be locally unique, i.e., not otherwise visible within the same top-level expression. For top-level modules within a single compilation unit, the names must be unique within the compilation unit. When multiple compilation units may be linked together, the names must be unique across compilation units.

– Oscar Waddell, Extending the Scope of Syntactic Abstraction, §3.6.1

Chez Scheme generates a UUID for each session that gets embedded into gensyms and that then gets embedded into the code. This satisfies the need for unique identifiers that are different between separate compilations. It ensures that things work smoothly when you are using the compiler yourself. But we want reproducible builds, meaning byte-for-byte identical builds, so the UUID is a problem.

When building packages for Linux distributions, things are a bit different than when you’re using the compiler yourself. Our build system can tell us what code went into the build, including the dependencies that brought in Scheme code, and if those stay the same then there is no need to use different identifiers compared to the previous time we built the same code.

I’m toying with the idea of generating a session key from the package version numbers and passing it to configure. I think it can be done without changing anything outside of the build system (the Zuo code). Conceptually we would be doing this:

(#%$set-top-level-value! '$session-key "k-")
(compile-file "s/foo.ss")

It remains to be seen if this is enough or if there are other sources of non-determinism.

by weinholt at Friday, July 31, 2026

Tuesday, July 21, 2026

jointhefreeworld

Emacs Eglot for Scala and Kotlin (JVM)

When Emacs 29 made eglot the built-in, default Language Server Protocol (LSP) client, many of us rejoiced.

It is lightweight, fast, adheres strictly to Emacs philosophy, and doesn’t try to reinvent the wheel.

However, being minimal means that when an LSP server steps out of line or acts quirky, eglot doesn’t provide a million customizable toggles to fix it out-of-the-box. Instead, it expects you to leverage the power of Emacs Lisp.

In this post, I will dissect my production-ready eglot setup (part of my heks-emacs configuration) which I use in my day-to-day work, with Scala and Kotlin (and some Java).

For reference, find my full Eglot config here: https://codeberg.org/jjba23/heks-emacs/src/branch/trunk/src/modules/eglot.el

We will walk through basic language setups, specialized workspace configuration handling, and dive deep into some advanced JSON-RPC and advice-based workarounds for Scala (Metals) and Kotlin that make development truly seamless from Emacs and liberate you from IntelliJ ☺️.

It’s not perfect, but it’s pretty darn close to perfection if you ask me, and the developer experience and speed that it enables is just wild. Thank you Emacs, thank you GNU, thank you Eglot! 🐂



Before looking at the code, let’s talk about why we are doing this. For years, the conventional wisdom stated that if you write JVM languages, especially Scala or Kotlin, you must use IntelliJ IDEA. The narrative claimed that these languages are too complex for a standard text editor.

But what do you actually get with IntelliJ? A massive, monolithic Java application that frequently hogs 8GB+ of RAM, locks up your system while “indexing pre-built binaries,” and forces you into a closed proprietary ecosystem.

Emacs turns this paradigm on its head through three core strengths:

  • The Unix Philosophy of LSP: Instead of a single IDE trying to compile, index, and render your code simultaneously, Emacs splits these duties. Eglot acts as a lean, protocol-first transport layer that talks to dedicated language servers via JSON-RPC.
  • Infinite Hackability: If IntelliJ has a bug in how it auto-completes Kotlin code, you are stuck waiting for JetBrains to issue a patch. In Emacs, you can write a 10-line Lisp advice function to intercept the network payload and patch the bug live in your editor buffer.
  • Unified Interface: You use the same text-manipulation utilities, text-jumping tools ( xref), and completion frameworks ( corfu, company, etc.) whether you are adjusting a Nix expression, editing a Markdown file, or refactoring a massive Scala service.


Hooks, Keybindings, and Initial Configurations  #

Let’s start with how eglot is initialized. I use Elpaca and use-package to manage the configuration, ensuring it doesn’t download an external package since it is built-in ( :ensure nil). Then I add some hooks to automatically start the language server for certain modes.

( use-package eglot
   :ensure nil
   :hook ((scala-ts-mode . eglot-ensure)
         (sh-mode . eglot-ensure)
         (markdown-mode . eglot-ensure)
         (markdown-ts-mode . eglot-ensure)
         (nix-ts-mode . eglot-ensure)
         (html-mode . eglot-ensure)
         (css-mode . eglot-ensure)
         (css-ts-mode . eglot-ensure)
         (html-ts-mode . eglot-ensure)
         (js-mode . eglot-ensure)
         (js-ts-mode . eglot-ensure)
         (kotlin-ts-mode . eglot-ensure)
         (yaml-mode . eglot-ensure)
         (yaml-ts-mode . eglot-ensure)
          ;;  formatting
         (before-save . eglot-format-buffer))
   ;;  ..................
   ;;  more config
  )
  • Eglot-Ensure Everywhere: I hook eglot-ensure into almost every programming mode I use, adapting both classic modes and modern Tree-sitter ( *-ts-mode) alternatives.
  • Auto-Formatting: Adding eglot-format-buffer to before-save guarantees code style compliance automatically every time a file hits the disk.

My keybindings are nested under the C-c i prefix, keeping them memorable and consistent across languages. The mnemonic keyword is “IDE” .

 :bind (( "C-c i i" . eglot-find-implementation)
       ( "C-c i e" . eglot)
       ( "C-c i k" . eglot-shutdown-all)
       ( "C-c i r" . eglot-rename)
       ( "C-c i x" . eglot-reconnect)
       ( "C-c i a" . eglot-code-actions)
       ( "C-c i m" . eglot-menu)
       ( "C-c i f" . eglot-format-buffer)
       ( "C-c i h" . eglot-inlay-hints-mode))
 :init
( setq eglot-autoshutdown t
      eglot-confirm-server-edits nil
      eglot-report-progress t
      eglot-extend-to-xref t
      eglot-sync-connect 1
      eglot-connect-timeout 60
      eglot-autoreconnect t)

Then with these :init settings:

  • eglot-autoshutdown cleans up language server processes as soon as the last buffer managed by them is killed.
  • eglot-extend-to-xref allows Emacs’ cross-referencing commands to smoothly transition into external library files outside your workspace directory.

Fine-Tuning Server Definitions and Workspaces  #

Under the :config block, we begin optimizing specific language servers. For instance, removing default configurations before re-adding custom entries prevents collisions.

 :config
( setopt eglot-code-action-indications nil)  ;;  Cleans up Emacs 31 visual noise

 ;;  Clean slate for Scala and Kotlin
( setq eglot-server-programs (assq-delete-all 'scala-mode eglot-server-programs))
( setq eglot-server-programs (assq-delete-all 'scala-ts-mode eglot-server-programs))
( setq eglot-server-programs (assoc-delete-all 'scala-ts-mode eglot-server-programs))

(add-to-list 'eglot-server-programs `(scala-ts-mode . ( "metals"
                                                        "-Xmx4G"
                                                        "-XX:+UseZGC"
                                                        "-Dmetals.http=true"
                                                        :initializationOptions ( :isHttpEnabled t))))

( setq eglot-server-programs (assoc-delete-all 'kotlin-ts-mode eglot-server-programs))
(add-to-list 'eglot-server-programs '(kotlin-ts-mode . ( "intellij-server"  "--stdio")))

Why these changes?

  • Scala (Metals): I pass specific JVM tuning flags directly to Metals (allocating a comfortable 4GB heap and utilizing the Z Garbage Collector for minimal latency). Also, enabling Metals HTTP communication via initialization options lets us hook into specialized UI features if needed.
  • Kotlin: I swap out standard options for the IntelliJ-backed Kotlin Language Server ( intellij-server --stdio).

Global Workspace Configurations  #

eglot-workspace-configuration lets you pass customized variables downstream to your language servers. This section of my configuration acts like a universal settings.json:

( setq-default eglot-workspace-configuration
              '(
                 :metals (  :autoImportBuild  "all"
                           :isHttpEnabled t
                           :superMethodLensesEnabled t
                           :showInferredType t
                           :enableSemanticHighlighting t
                           :inlayHints (  :inferredTypes ( :enable t )
                                         :implicitArguments ( :enable nil)
                                         :implicitConversions ( :enable nil )
                                         :typeParameters ( :enable t )
                                         :hintsInPatternMatch ( :enable nil ))
                           :bloopJvmProperties [ "-Xmx4G"])
                 :haskell ( :formattingProvider  "ormolu")
                 :typescript ( :format ( :baseIndentSize 0
                                                       :convertTabsToSpaces t
                                                       :indentSize 2
                                                       :semicolons  "remove"
                                                       :tabSize 2))
                 :javascript ( :format ( :baseIndentSize 0
                                                       :convertTabsToSpaces t
                                                       :indentSize 2
                                                       :semicolons  "remove"
                                                       :tabSize 2))
                 :rust-analyzer ( :check ( :command  "clippy")
                                        :cargo ( :sysroot  "discover"
                                                         :features  "all"
                                                         :buildScripts ( :enable t))
                                        :diagnostics ( :disabled [ "macro-error"])
                                        :procMacro ( :enable t))

                 :yaml (  :format ( :enable t)
                         :validate t
                         :hover t
                         :completion t
                         :schemas (
                                  https://codeberg.org/jjba23/pop-test/raw/branch/trunk/resources/json-schema/pop-test.json [ "golden-test.yaml"  "golden-test.yml"  "pop-test.yaml"  "pop-test.yml"]
                                  https://raw.githubusercontent.com/Vandebron/gh-mpyl/refs/heads/main/src/mpyl/schema/project.schema.yml [ "project.yml"]
                                  https://json.schemastore.org/yamllint.json [ "/*.yml"])
                         :schemaStore ( :enable t))
                 :nil ( :formatting ( :command [ "nixfmt"]))))

Notable Configurations here:

  • Metals: Granular inlay hints are activated specifically for inferred types and type parameters while muting implicit conversions to keep buffers readable. (more options here: https://scalameta.org/metals/docs/editors/user-configuration/)
  • YAML Schema Mapping: Maps distinct internet-hosted JSON schemas straight to patterns of YAML files automatically.


Deep Dive: The Workarounds  #

This is where things get interesting. Sometimes servers violate standard LSP expectations, requiring custom Emacs Lisp logic to bridge the gap.

Fixing Eldoc Overload  #

By default, eldoc can easily get flooded by different feedback mechanisms. This block prioritizes structural code diagnostics over generic hover data:

(add-hook 'eglot-managed-mode-hook
          ( lambda ()
             ;;  Show flymake diagnostics first.
            ( setq eldoc-documentation-functions
                  (cons #'flymake-eldoc-function
                        (remove #'flymake-eldoc-function eldoc-documentation-functions)))
             ;;  Show all eldoc feedback.
            ( setq eldoc-documentation-strategy #'eldoc-documentation-compose)))

Kotlin Source Navigation (Jar URI Translation)  #

When traversing into a dependency library using Kotlin, the server returns file references formatted as jar:///path/to/library.jar!/File.kt. Emacs can’t resolve this scheme directly out of the box, throwing errors when you try to jump to definition.

By wrapping Eglot’s URI translators with advice, we can map this custom scheme into something Emacs understands (especially alongside companion extensions like jarchive):

( defun  heks/eglot-uri-to-path-kotlin (orig-fn uri  &rest; args)
  ( if ( and (stringp uri) (string-prefix-p  "jar:///" uri))
      (apply orig-fn (replace-regexp-in-string  "^jar:///"  "jar:file:///" uri) args)
    (apply orig-fn uri args)))

( defun  heks/eglot-path-to-uri-kotlin (orig-fn path  &rest; args)
  ( if ( and (stringp path) (string-prefix-p  "jar:file:///" path))
      (replace-regexp-in-string  "^jar:file:///"  "jar:///" path)
    (apply orig-fn path args)))

( if (fboundp 'eglot-uri-to-path)
    ( progn
      (advice-add 'eglot-uri-to-path  :around #'heks/eglot-uri-to-path-kotlin)
      (advice-add 'eglot-path-to-uri  :around #'heks/eglot-path-to-uri-kotlin))
  ( progn
    (advice-add 'eglot--uri-to-path  :around #'heks/eglot-uri-to-path-kotlin)
    (advice-add 'eglot--path-to-uri  :around #'heks/eglot-path-to-uri-kotlin)))

Intercepting the Kotlin Empty newText Auto-Completion Bug  #

A notorious issue in certain Kotlin LSP releases occurs during auto-completion. The server reports matching candidates, but mistakenly attaches a textEdit field containing an empty string ( newText: ""). This causes Eglot to wipe out the word you are completing entirely.

To solve this, I intercept the incoming JSON-RPC response payloads, both synchronous and asynchronous. If a Kotlin completion candidate returns an empty string edit, we strip the `textEdit` attribute completely, forcing Eglot to fall back gracefully to standard prefix matching.

( defun  my-jsonrpc-request-kotlin-fix (orig-fn connection method params  &rest; args)
   "Fix kotlin-lsp empty newText bug by removing textEdit to trigger Eglot fallback."
  ( let ((result (apply orig-fn connection method params args)))
    ( when ( and (eq method  :textDocument/completion)
               (derived-mode-p 'kotlin-mode 'kotlin-ts-mode)
               result)
      ( let ((items ( if (vectorp result) result (plist-get result  :items))))
        (seq-do ( lambda (item)
                  ( let ((text-edit (plist-get item  :textEdit)))
                     ;;  If the server sent an empty newText, strip textEdit completely
                     ;;  so Eglot falls back to replacing the actual prefix.
                    ( when ( and text-edit (equal (plist-get text-edit  :newText)  ""))
                      (plist-put item  :textEdit nil))))
                items)))
    result))

( defun  my-jsonrpc-async-request-kotlin-fix (orig-fn connection method params  &rest; args)
   "Fix kotlin-lsp empty newText bug in asynchronous Eglot requests."
  ( if ( and (eq method  :textDocument/completion)
           (derived-mode-p 'kotlin-mode 'kotlin-ts-mode))
      ( let* ((orig-success (plist-get args  :success-fn))
             (new-success ( lambda (result)
                            ( let ((items ( if (vectorp result) result (plist-get result  :items))))
                              (seq-do ( lambda (item)
                                        ( let ((text-edit (plist-get item  :textEdit)))
                                          ( when ( and text-edit (equal (plist-get text-edit  :newText)  ""))
                                            (plist-put item  :textEdit nil))))
                                      items))
                            (funcall orig-success result)))
             (new-args (plist-put (copy-sequence args)  :success-fn new-success)))
        (apply orig-fn connection method params new-args))
    (apply orig-fn connection method params args)))

(advice-add 'jsonrpc-request  :around #'my-jsonrpc-request-kotlin-fix)
(advice-add 'jsonrpc-async-request  :around #'my-jsonrpc-async-request-kotlin-fix)

Silencing Metals Semantic Refresh Flickering  #

Scala Metals aggressively forces full buffer semantic token refreshes. In large projects, this results in visual layout flickering and unnecessary CPU strain. Disabling this also can solve some startup issues for Metals.

( defun  my/eglot-disable-metals-semantic-refresh (orig-fn server)
  ( let* ((caps (funcall orig-fn server))
         (workspace (plist-get caps  :workspace))
         (tokens (plist-get workspace  :semanticTokens)))
    ( when tokens
      (plist-put tokens  :refreshSupport  :json-false))
    caps))

(advice-add 'eglot-client-capabilities  :around #'my/eglot-disable-metals-semantic-refresh)


Companion Packages: Java and Compressed Archives  #

To complete the setup, I load complementary minor modes outside of Eglot’s core file, ensuring smooth operations for Java and deep navigation for packed jars:

( use-package eglot-java
   :ensure t
   :after (eglot)
   :hook ((java-mode . eglot-java-mode)
         (java-ts-mode . eglot-java-mode)))

( use-package jarchive
   :ensure t
   :config
  (jarchive-mode))
  • eglot-java: Provisions proper workspace configurations specifically for Eclipse JDT LS seamlessly.
  • jarchive: Works harmoniously alongside the Kotlin JAR-URI translation hack, opening zipped up source containers into regular, viewable Emacs buffers.

The way I like it on reproducibility  #

I generally don’t use the “global” system wide JDK installation, but I use isolated development reproducible shells with Nix flakes.

I’ll eventually probably move to using Guix, but for now package availability isn’t quite there for JVM world so Nix it is.

This way you can easily work on the same machine with many environments and projects (e.g. different Java versions) and no need for SDKMan or version managers, but clean isolated per-project reproducible builds.

So I create a flake.nix and add it to Git.

Kotlin development flake (TODO intellij-server via Nix):

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    systems.url = "github:nix-systems/default";
  };
  outputs = { systems, nixpkgs, ... }:
    let
      eachSystem = f:
        nixpkgs.lib.genAttrs (import systems)
        (system: f nixpkgs.legacyPackages.${system});
    in {
      devShells = eachSystem (pkgs: {
        default = pkgs.mkShell {
          buildInputs = with pkgs; [
            ktfmt
            ktlint
            kotlin
            jdk25
            nil
            just
            yaml-language-server
          ];
        };
      });
    };
}

Scala development flake.

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    systems.url = "github:nix-systems/default";
  };
  outputs = { systems, nixpkgs, ... }:
    let
      eachSystem = f:
        nixpkgs.lib.genAttrs (import systems)
        (system: f nixpkgs.legacyPackages.${system});
    in {
      devShells = eachSystem (pkgs: {
        default = pkgs.mkShell {
          buildInputs = with pkgs; [
            scala_2_13
            jdk25
            metals
            sbt
            scalafmt
            scalafix
            scala-cli
            yaml-language-server
            coursier
          ];
        };
      });
    };
}

Then I load the flake with direnv so I create a .envrc file .

use flake

This way and inside Emacs I can use emacs-direnv to dynamically switch contexts inside Emacs LSPs and have even multiple running.

I also plug direnv into my Bash shell configurations and thus complete the development environment.

Conclusion  #

Eglot’s minimal, built-in design doesn’t mean you have to settle for sub-par language server behavior. After all, you are using Emacs, so the power is infinite!

By intercepting communication at the JSON-RPC level via advice-add, you can tailor client-server behaviors exactly to your liking.

Happy hacking! ✨

Tuesday, July 21, 2026

Saturday, July 18, 2026

Scheme Requests for Implementation

SRFI 270: Hexadecimal Floating-Point Constants

SRFI 270 is now in final status.

Floating-point numbers are usually stored in radix 2, but are written by users in radix 10. This SRFI introduces Scheme syntax for hexadecimal floating point constants based on C99’s syntax. They use radix 16 for writing the integer and fractional part, and a radix 10 exponent part that raises the whole value to a power of 2.

by Peter McGoron at Saturday, July 18, 2026