Progress Update For GNOME 43

GNOME 43 is out the door now, and I want to use this post to share what I’ve done since my post about my plans.

Adaptive Nautilus

The main window of Nautilus is now adaptive, working at mobile sizes. This change required multiple steps:

  • I moved the sidebar from the old GtkPaned widget to the new AdwFlap widget.
  • I added a button to reveal the sidebar.
  • I refactored the toolbar widgetry to seamlessly support multiple toolbars (allowing me to add a bottom toolbar without code duplication).
  • I ported most of the message dialogs to the new AdwMessageDialog widget.
  • I made the empty views use AdwStatusPage.

There are a few issues left before I can call Nautilus fully adaptive, though. The biggest issue is that the Other Locations view does not scale down to mobile sizes. The Other Locations review is currently in the process of being redesigned, so that should be resolved in the near future. Next, the new Properties dialog does not get small enough vertically for landscape mode. Finally, a few message dialogs don’t use AdwMessageDialog and will require special care to port.

Screenshot of Nautilus with a narrow width
Screenshot of Nautilus with a narrow width

In addition to the adaptive widgetry, I also landed some general cleanups to the codebase after the GTK4 port.


Since my post in April, Loupe has received many changes. Allan Day provided a new set of mockups for me to work from, and I’ve implemented the new look and a sidebar for the properties. There are some open questions about how the properties should be shown on mobile sizes, so for now Loupe doesn’t fit on phones with the properties view open.

Screenshot of Loupe with the properties sidebar open
Screenshot of Loupe with the properties sidebar open


I’ve also reworked the navigation and page loading. Back in April, Loupe only loaded one image at a time, and pressing the arrow keys would load the next image. This could lead to freezes when loading large images. Now Loupe uses AdwCarousel and buffers multiple images on both sides of the current image, and loads the buffered images on a different thread.

Loupe also now has code for translations in place, so that once it’s hooked up to GNOME’s translation infrastructure contributors will be able to translate the UI.


Some exciting new widgets landed in libadwaita this cycle: AdwAboutWindow, AdwMessageDialog, AdwEntryRow, and AdwPasswordEntryRow. I made an effort to have these new widgets adopted in core applications where possible.

I ported the following apps to use AdwAboutWindow:

  • Text Editor
  • Weather
  • Disk Usage Analyzer
  • Font Viewer
  • Characters
  • Nautilus
  • Calendar
  • Clocks
  • Calculator
  • Logs
  • Maps
  • Extensions
  • Music

Now every single core app that uses GTK4 uses AdwAboutWindow.

Screenshot of Text Editor's about window
Screenshot of Text Editor’s about window

I ported Nautilus and Maps to AdwMessageDialog where possible, and adjusted Contacts and Calendar to use AdwEntryRow. Contacts needed some extra properties on AdwEntryRow, so I implemented those.

I also started work on a new widget, AdwSpinRow. Hopefully it will land this cycle.


In addition to the changes mentioned in the libadwaita section, I also made Calendar fit at small widths with AdwLeaflet. The app received a large redesign already, and it was only a few small changes away from being mobile-ready. There are still a few issues with fit, but those should hopefully be resolved soon.

Calendar 44 will hopefully use AdwMessageDialog and a new date selector in the event editor – I have open merge requests for both changes.

Misc. Changes

  • Minor fixups for GNOME Music’s empty state
  • Updated core app screenshots for Disk Usage Analyzer, Text Editor, Contacts, Calendar, and Nautilus
  • Ported Sound Recorder to Typescript


Overall I made a lot of progress, and I hope to make much more this cycle. The GNOME 43 cycle overlapped a very busy time in my life, and now things have cooled down. With your help, I would love to be able to focus more of my time on implementing things you care about.

I have three places you can support me:

That’s all for now. Thank you for reading to the end, and I look forward to reporting more progress at the end of the GNOME 44 cycle.

Trying TypeScript for GNOME Apps

JavaScript is an excellent language for getting things done quickly. However, flexibility and speed come at the cost of not catching certain classes of errors before your code runs. For example, you can easily miss type errors and syntax errors. Developers can address these issues via type annotations and static analyzers like ESLint, but that requires a significant amount of effort on the developer’s part – which contrasts with the ability to write a program in a flexible language quickly.

Generally, I prefer to work in a language that watches my back – the tooling is my partner as I write my code. Rust is an exemplary language in this department, but certain design decisions make it slower to write an app in Rust than in JS. I figured it wasn’t the right choice for the JS apps I maintain, but I also wasn’t fond of continuing to work in plain JS. These reasons led me to look into TypeScript, and after some research, I decided I would port an app as an experiment: GNOME Sound Recorder. As far as I know, this is the first complete application in the GNOME ecosystem written in TypeScript.

I ported the app in 6 main steps:

  • Porting to ES modules
  • Using type-checked JS
  • Porting to TypeScript in full
  • Enabling strict type checking
  • Re-adding ESLint
  • Fixing the flatpak manifest and our CI

With some minor cleanups and style changes sprinkled in. Before we dive in, I want to say thank you to everyone who has helped with this effort, particularly:

  • Zander Brown
  • Philip Chimento
  • Sonny Piers
  • Andy Holmes
  • Evan Welsh
  • Michael Murphy

Everyone here provided invaluable advice, tooling, or code that helped me in the process of working on this.

Porting to ES Modules

While Sound Recorder may be the first TypeScript app, I am not the first person in GNOME to work with TypeScript. The developers at System76 are using it to write their tiling shell extension. Evan Welsh created a type definition generator and published type definitions on npm. 2 years ago, Michael Murphy was posting issues on a few GJS repositories about porting to TypeScript, and I noticed one such issue. I was curious, so I wrote an email, and he sent some helpful information about how their shell extension worked.

When I decided to give TypeScript a try recently, I remembered that information. I noticed that System76 has a script to change the module syntax of their TypeScript to the style of imports and exports that GJS supported at the time. Thankfully GJS now supports modules – so there’s no script required. Or rather, we wouldn’t need a script if we transitioned the Sound Recorder codebase to use modules. So I did precisely that. Most GJS code already uses modules, so it was something I needed to do as a general housekeeping task anyway.

Type-checked JavaScript

Type-checking JavaScript is the use case that most developers have for using the GJS type definitions, but it’s also a first step in integrating TypeScript tooling with your JS code. The docs have a chapter on setting it up. At this stage, I received completion in my text editor and a few warnings for mistakes that no one had noticed. For example, I forgot to have a return statement in a non-void virtual function.

It wasn’t all sunny at first – GJS has some built-in functions and objects that it allows you to access globally. The TypeScript checker wasn’t aware of those at all. Philip Cemento helped me learn how to define pkg and the _() helper for translations in an ambient type definition file. I also needed to re-declare modules like gtk as gi://Gtk so that I could import them in the way that GJS would expect.

In addition to the module pains, there was another issue: The GJS-provided GObject.registerClass() function sets up property and template children fields. If you have a child named button, GJS will allow you to refer to it as _button. To get type-checking for fields, I needed to declare them at the beginning of the class. However, re-declaring the fields set them to null, breaking the application. So I came up with an ugly hack:

// @ts-ignore
/** @type {Gtk.Stack} */ _mainStack = this._mainStack;
// @ts-ignore
/** @type {Adw.StatusPage} */ _emptyPage = this._emptyPage;
// @ts-ignore
/** @type {Adw.Clamp} */ _column = this._column;
// @ts-ignore
/** @type {Gtk.Revealer} */ _headerRevealer = this._headerRevealer;
// @ts-ignore
/** @type {Adw.ToastOverlay} */ _toastOverlay = this._toastOverlay;

Yeah, that’s not pretty at all. I was scared to continue after writing such hacky code – I wasn’t sure if the final product would be this awful. Still, I pressed on.

Initial TypeScript Port

This part was surprisingly uneventful. I mainly edited the code to provide type annotations for functions and closures. I already annotated fields in the previous step – I just needed to switch to TypeScript syntax. The new syntax came with a big boon: I could clean up the messy hack I had before. TypeScript has a ! operator that can be used on fields, telling the linter that you know it may seem like a field isn’t there, but it really is there. So the above code became:

_mainStack!: Gtk.Stack;
_emptyPage!: Adw.StatusPage;
_column!: Adw.Clamp;
_headerRevealer!: Gtk.Revealer;
_toastOverlay!: Adw.ToastOverlay;

After this, I was pretty happy with the app’s state – until my friend Zander Brown told me I could make TypeScript even stricter.

Strict Type Checking

Strict type checking enables null safety in TypeScript. That means that for every value that can be undefined or null, you must declare and handle those states explicitly. Typescript has some neat shorthand for this. For example:

level?: Gst.Element;

Here the ? means that level can either be a Gst.Element or undefined. You only need this for fields you don’t set when declared or in the constructor.

let audioCaps = Gst.Caps.from_string(profile.audioCaps);
audioCaps?.set_value('channels', this._getChannel());

? can also be used as a quick null check. Gst.Caps.from_string() returns Gst.Caps | null. So in the following line, audioCaps?.set_value() is saying “if audioCaps exists, set a value.”

This stage helped me catch areas where we simply forgot to check whether or not a value could be null before using it.


ESLint caught a bunch of formatting and convention issues, but it also let me know that a “clever trick” I thought of to get around some troubling code blocks wasn’t so clever.

In JS I had this segment:

_init() {
    this._peaks = [];

    let srcElement, audioConvert, caps;
    try {
        this.pipeline = new Gst.Pipeline({ name: 'pipe' });
        srcElement = Gst.ElementFactory.make('pulsesrc', 'srcElement');
        audioConvert = Gst.ElementFactory.make('audioconvert', 'audioConvert');
        caps = Gst.Caps.from_string('audio/x-raw');
        this.level = Gst.ElementFactory.make('level', 'level');
        this.ebin = Gst.ElementFactory.make('encodebin', 'ebin');
        this.filesink = Gst.ElementFactory.make('filesink', 'filesink');
    } catch (error) {
        log(`Not all elements could be created.\n${error}`);

    try {
    } catch (error) {
        log(`Not all elements could be addded.\n${error}`);
    audioConvert.link_filtered(this.level, caps);

Many of these functions could return null, so I had no idea how to rework this segment nicely. I cheated and used the ! operator to tell TS to ignore the nullability of these items:

constructor() {
    this._peaks = [];

    let srcElement: Gst.Element;
    let audioConvert: Gst.Element;
    let caps: Gst.Caps;

    this.pipeline = new Gst.Pipeline({ name: 'pipe' });

    try {
        srcElement = Gst.ElementFactory.make('pulsesrc', 'srcElement')!;
        audioConvert = Gst.ElementFactory.make('audioconvert', 'audioConvert')!;
        caps = Gst.Caps.from_string('audio/x-raw')!;
        this.level = Gst.ElementFactory.make('level', 'level')!;
        this.ebin = Gst.ElementFactory.make('encodebin', 'ebin')!;
        this.filesink = Gst.ElementFactory.make('filesink', 'filesink')!;
    } catch (error) {
        log(`Not all elements could be created.\n${error}`);

    try {
    } catch (error) {
        log(`Not all elements could be addded.\n${error}`);

    audioConvert!.link_filtered(this.level!, caps!);

Using that operator was awful, and ESLint refused to let me get away with it. Thankfully Philip Chimento helpfully provided a solution with tuples and destructuring:

constructor() {
    this._peaks = [];

    let srcElement: Gst.Element;
    let audioConvert: Gst.Element;
    const caps = Gst.Caps.from_string('audio/x-raw');

    this.pipeline = new Gst.Pipeline({ name: 'pipe' });

    const elements = [
        ['pulsesrc', 'srcElement'],
        ['audioconvert', 'audioConvert'],
        ['level', 'level'],
        ['encodebin', 'ebin'],
        ['filesink', 'filesink']
    ].map(([fac, name]) => {
        const element = Gst.ElementFactory.make(fac, name);
        if (!element)
            throw new Error('Not all elements could be created.');
        return element;

    [srcElement, audioConvert, this.level, this.ebin, this.filesink]  = elements;;
    audioConvert.link_filtered(this.level, caps);

Fixing Flatpak & CI

Since TypeScript is part of the general JS ecosystem that uses npm.js, I somehow needed to integrate the npm sources with my own. Thankfully, a handy manifest generator provides a list of sources in a format flatpak knows how to handle. I couldn’t find a good way for my build scripts to access the directory where flatpak installed the sources, so I needed to set up a module with steps to move the sources somewhere visible to my app module:

    "name" : "yarn-deps",
    "buildsystem" : "simple",
    "build-commands" : [
        "mkdir -p /app",
        "cp -r $FLATPAK_BUILDER_BUILDDIR/flatpak-node/yarn-mirror/ /app"
    "sources" : [

Then I set up meson to take the mirror dir as an option. After verifying that my build still worked locally and within flatpak, fixing my CI was as simple as adding two lines to the YAML:

    - flatpak --user install -y org.freedesktop.Sdk.Extension.node18//22.08beta

Next Steps

There are still two remaining tasks before I consider this port fully finished:

  • Make Sound Recorder use Promises and async/await syntax
  • Remove the type definitions from the repo and use them from npm

Promises have a significant papercut, making them difficult to use in the current state. There are workarounds, but none that I’ve gotten to work. For the type definitions, having less code to maintain in the Sound Recorder repo will be helpful.

Porting Sound Recorder to TypeScript has been a positive experience overall. I want to continue using TypeScript in place of plain JavaScript, including in core apps like Weather. There are a few questions that the community needs to answer before I would feel comfortable using JavaScript in core components:

  • How should distros (or component developers) handle sources from NPM?
  • Are we as a community comfortable depending on the NPM ecosystem?

I look forward to seeing how discussions about these questions pan out.

If you’re interested in following and supporting my future work, please consider sponsoring me on Patreon, GitHub Sponsors, or with one-time donations via PayPal:

In the next few days, I plan to share what I’ve done since my previous planning post.

Plans for GNOME 43 and Beyond

GNOME 42 is fresh out the door, bringing some exciting new features – like the new dark style preference and the new UI for taking screenshots and screen casts. Since we’re right on the heels of a release, I want to keep some momentum going and share my plans for features I want to implement during the upcoming release cycles.

Accent Colors and Libadwaita Recoloring API

The arrival of libadwaita allows us to do a few new things with the GNOME platform, since we have a platform library to help developers implement new platform features and implement them properly. For example, libadwaita gave us the opportunity to implement a global dark style preference with machinery that allows developers to choose whether they support it and easily adjust their app’s styling when it’s enabled. Alexander Mikhaylenko spent a long time reworking Adwaita so that it works with recoloring, and I want to take full advantage of that with the next two features: global accent colors and a recoloring API.

Libadwaita makes it simple to implement a much-wanted personalization feature: customizable accent colors. Global accent colors will be opt-in for app developers. For the backend I want accent colors to be desktop- and platform-agnostic like the new dark style preference. I plan to submit a proposal for this to xdg-desktop-portal in the near future. In GNOME it’d probably be best to show only a few QA-tested accents in the UI, but libadwaita would support arbitrary colors so that apps from KDE, GNOME, elementary OS, and more all use the same colors if they support the preference.

Developers using the recoloring API will be able to programmatically change colors in their apps and have dependent colors update automatically. They’ll be able to easily create presets which can be used, for example, to recolor the window based on a text view’s color scheme. Technically this is already possible with CSS in libadwaita 1.0, but the API will make it simpler. Instead of having to consider every single color, they’ll only need to set a few and libadwaita will handle the rest properly. The heuristics used here will also be used to ensure that accent colors have proper contrast against an app’s background.

There’s no tracking issue for this, but if you’re interested in this work you may want to track the libadwaita repository:

Adaptive Nautilus and Improved File Chooser

The GTK file chooser has a few issues. For example, it doesn’t support GNOME features like starred files, and it needs to be patched by downstream vendors (e.g. PureOS, Mobian) to work on mobile form factors. In order to keep up with platform conventions, ideally the file chooser should become part of GNOME’s core rather than part of GTK. There’s some discussion to be had on solutions, but I believe that it would be smart to keep the file chooser and our file browser in sync by making the file chooser a part of the file browser.

With all of that in mind I plan to make Nautilus adaptive for mobile form factors and add a new file chooser mode to it. The file chooser living in Nautilus instead of GTK allows us support GNOME platform features at GNOME’s pace rather than GTK’s pace, follow GNOME design patterns, and implement features like a grid view with thumbnails without starting from scratch.

If you’re interested in seeing this progress, keep track of the Nautilus repository:

Loupe (Image Viewer)

For a while now I’ve been working on and off on Loupe, a new image viewer written in Rust using GTK4 and libadwaita. I plan for Loupe to be an adaptive, touch pad and touchscreen friendly, and easy to use. I also want it to integrate with Nautilus, so that Loupe will follow the sorting settings you have for a folder in Nautilus.

In the long term we want Loupe to gain simple image editing capabilities, namely cropping, rotation, and annotations. With annotations Loupe can integrate with the new screenshot flow, allowing users to take screenshots and annotate them without needing any additional programs.

If Loupe sounds like an interesting project to you, feel free to track development on GitLab:

Rewrite Baobab in Rust, and Implement new Design

Baobab (a.k.a. Disk Usage Analyzer) is written in Vala. Vala does not have access to a great ecosystem of libraries, and the tooling has left something to be desired. Rust, however, has a flourishing library ecosystem and wonderful tooling. Rust also has great GTK bindings that are constantly improving. By rewriting Baobab in Rust, I will be able to take full advantage of the ecosystem while improving the performance of it’s main function: analyzing disk usage. I’ve already started work in this direction, though it’s not available on GitLab yet.

In addition to the rewrite, I also plan to implement a redesign based on Allan Day’s mockups. The new design will modernize the UI, using new patterns and fixing a few major UI gripes people have with the current design.

You can keep track of progress on Baobab on GitLab:

Opening Neighboring Files from FileChooser Portal

The xdg-desktop-portal file picker doesn’t allow opening neighboring files when you choose a file. Apps like image browsers, web browsers, and program runners all need a sandbox hole if they want to function without friction. If you use a web browser as a flatpak you may have run into this issue: opening an html file won’t load associated HTML files or media files. If you are working on a website locally, you need to serve it with a web server in order to preview it – e.g. with python -m http.server.

I want to work on a portal that allows developers to request access to neighboring files when opening one file. With this portal I could ship Loupe as a flatpak without requiring any sandbox holes, and apps like Lutris or Bottles would also be more viable as flatpaks.

If you want to learn more and follow along with progress on this issue, see the xdg-desktop-portal issue on GitHub:

Accessibility Fixups

GTK4 makes accessibility simpler than ever. However, there are still improvements to be made when it comes to making core apps accessible. I want to go through our core app set, test them with the accessibility tooling available, and document and fix any issues that come up.

Sponsoring my Work

I hope to be able to work on all of these items (and more I haven’t shared) this year. However, I am currently looking for work. Right now I would need to be looking for work full-time or working on something else full-time instead of working on these initiatives – I don’t have the mental bandwidth to do both. If you want to see this work get done, I could really use your support. I have three places you can support me:

If I get enough sponsorships, I plan to start posting regular (bi-weekly) updates for GitHub sponsors and Patrons. I also may start streaming some of my work on Twitch or YouTube, since people seem to be interested in seeing how things get done in GNOME.

That’s all for now – thanks for reading until the end, and I hope we can get some or all of this done by the time of the next update.

Glade Not Recommended

If you are starting out with GTK development, you may have heard of a tool called Glade. Glade is a UI designer application for GTK projects, that allows you to create, modify, and preview UI files before writing code. In that sense, Glade is a very useful tool for GTK apps.

With that said, I must implore that you do not use Glade.

Why? Glade was built for it’s own format, before the advent of
GtkBuilder. It does not know certain properties, does not know
of certain features, and does not know modern GTK practices.

Instead of ignoring what it does not know, it aggressively “corrects” it. Glade will re-write your UI files to its whims,
including but not limited to:

  • Removing property bindings
  • Removing unknown properties
  • Adding child properties to GtkBox children everywhere
    • These are gone in GTK 4, which adds extra work to the porting process
  • Adding explicit GtkViewports that cause a double-border
  • Forcing minimum width and height on GtkListBoxRows
  • Re-arranging objects within the UI file
  • Changing the format of all properties in an edited UI file

This makes Glade harmful in different ways. Your UI files will be bloated, due to Glade adding placeholders, child properties,
and extra widgets where they aren’t needed. When you make a
contribution to an app, you may be asked to re-do it by hand
because Glade made unnecessary changes. When porting to GTK 4 you
will need to do more work, as Glade doesn’t know to avoid
deprecated widgets, properties, and child properties.

All of these elements combine to make Glade more harmful than helpful. So please, do not use Glade. If you are working with UI
files write them by hand. An editor with code folding and syntax
highlighting can be very helpful. If you need to mock something
up, you can try sketching on a piece of paper or using our
mockup resources. Whatever you choose, don’t use Glade.

What is a Platform?

Often when looking for apps on Linux, one might search for something “cross-platform”. What does that mean? Typically it refers to running on more than one operating system, e.g. Windows, macOS, and GNU/Linux. But, what are developers really targeting when they target GNU/Linux, since there’s such diverse ecosystem of environments with their own behaviors? Is there really a “Linux Desktop” platform at all?

The Prerequisites

When developing an app for one platform, there are certain elements you can assume are there and able to be relied on. This can be low-level things like the standard library, or user-facing things like the system tray. On Windows you can expect the Windows API or Win32, and on macOS you can expect Cocoa. With GNU/Linux, the only constants are the GNU userspace and the Linux kernel. You can’t assume systemd, GLib, Qt, or any of the other common elements will be there for every system.

What about freedesktop? Even then, not every desktop follows all of the specifications within freedesktop, such as the Secrets API or the system tray. So making assumptions based on targeting freedesktop as a platform will not work out.

To be a true platform, the ability to rely on elements being stable for all users is a must. By this definition, the “Linux Desktop” itself is not a platform, as it does not meet the criteria.

Making Platforms Out of “The Linux Desktop”

It is possible to build fully realized platforms on top of GNU/Linux. The best example of this is elementary OS. Developers targeting elementary OS know that different elements like Granite will be present for all users of elementary OS. They also know elements that won’t be there, such as custom themes or a system tray. Thus, they can make decisions and integrate things with platform specifics in mind. This ability leads to polished, well-integrated apps on the AppCenter and developers need not fear a distro breaking their app.

To get a healthy app development community for GNOME, we need to be able to have the same guarantees. Unfortunately, we don’t have that. Because GNOME is not shipped by upstream, downstreams take the base of GNOME we target and remove or change core elements. This can be the system stylesheet or something even more functional, like Tracker (our file indexer). By doing this, the versions of GNOME that reach users break the functionality or UX in our apps. Nobody can target GNOME if every instance of it can be massively different from another. Just as no one can truly target the “Linux Desktop” due to the differences in each environment.

How do we solve this, then? To start, the community idea of the “Linux Desktop” as a platform needs to be moved past. Once it’s understood that each desktop is target that developers aim for, it will be easier for users to find what apps work best for their environment. That said, we need to have apps for them to find. Improving the development experience for various platforms will help developers in making well-integrated apps. Making sure they can safely make assumptions is fundamental, and I hope that we get there.

Developing GNOME: The Basics

I’ve been working in the GNOME community for a little under a year and a half now. During my time contributing to the project, I’ve seen a lot of misunderstandings from the community about how we work. So, I’ve decided to write a multi-part series on how development on GNOME works.

This first post will cover the basics. In future I’ll explain our tooling, how apps are created, and how changes happen across the ecosystem.

The Release Cycle

At the center of GNOME development is the release cycle. Every 6 months we put out a major release, with patch releases in-between. Major releases typically happen in September and March, and are named after the city of the most recent conference. GNOME 3.30 was named after the city we held GUADEC in, and 3.32 is named after where GNOME.Asia took place.

At different intervals in the cycle we have freeze periods, after which no changes can be made to certain parts of the code. Core apps, such as Web, Files, and Settings all strictly follow the release cycle and freezes. Apps outside of the core set like Polari, Builder, and Fractal can follow their own schedules or versioning schemes, but tend to put out releases alongside the GNOME core.

The freeze deadlines often determine what contributions make it into a release. For example, if a UI change is submitted after the UI freeze, maintainers need to seek approval from the Design and Release teams before the change can be merged. Freezes are staggered, and come in the following order:

  • UI, Feature, and API/ABI Freezes
  • String Freeze
  • Hard Code Freeze

The hard code freeze ends once the major release for the cycle is out. If you want to apply a change that violates the other freezes, you need to create a branch for the latest stable release. So, if I need to merge a UI change after the 3.32 release is out, I need to first create a gnome-3-32 branch before I accept the change onto master. This branch will then be used to cherry-pick changes for the 3.32.X releases.

How Apps Are Maintained

Each project within GNOME has its own developers. The Music developers aren’t necessarily the same people working on the Shell, and the Shell developers generally aren’t the same people working on GTK. While many developers work across the ecosystem on different projects, there is no one team of developers. This is why “GNOME decided such and such” is often inaccurate.

The maintainers of a project have full say over what contributions are or are not accepted. While certain things can be proposed, maintainers have the right to reject proposals. This is, for example, is why Terminal did not have a HeaderBar until 3.32 and doesn’t enable it by default. Nobody is forced to do anything, but often developers and designers will agree on a direction for an app, or the ecosystem at large.

Contrary to popular belief, most maintainers are not paid by Red Hat although some core components like Files and GNOME Shell do have Red Hat employees employed to work on them. Other companies such as Canonical, Purism, and Endless employ developers to work on the parts of the stack that matter to them. That said, most contributors are not on company time even if they are employed by the likes of Red Hat. And of course those that are employed to work on GNOME still aren’t paid for all of their work on GNOME. Most of our work is done on our own time, as limited by the rest of our lives.

It’s also worth noting that GNOME is built with a wide range of technologies; while GTK is written exclusively in C, Music is a Python project and Calculator is implemented with Vala. The Python developers working on Music are great with Python and GStreamer, but they aren’t going to be much help fixing a rounding error in Calculator as a general rule, and as volunteers it wouldn’t be fair to expect them to be, either.

tl;dr: GNOME is a community of individuals each with their own motivations and vision for their own part of the project doing their best to build a great platform for users.