How to check if ANY browser is installed in Linux (even more updated)

Updated: 2025-06-12
(If you just want to know my solution, skip to Reading .desktop Files.)

Linux is a very powerful operating system that gets more user-friendly by the year, but it still has glaring holes. Today, I discovered there is no reliable method to finding out if a browser is installed.

Searching online reveals that most people only care about knowing if specific browsers are installed, or are content with the incomplete option of only checking for the most popular browsers (currently Chrome, Firefox, and Opera). The which command works fine combined with a resource like LinuxConfig.org’s List of Browsers available on Linux, but now your script requires periodic maintenance to add more browsers to the list1.

The next most common answer online is to see if xdg-open exists, based on the assumption that a browser must be installed if xdg-open is installed – this is simply false. You also can’t query xdg-open to see if it can handle URLs without opening a URL. If you use this method, you are effectively attacking the user with a pop-up2.

I’ve also found documentation for update-alternatives, but it requires knowing exactly what name it uses for browser entries. I was told to use x-www-browser, gnome-www-browser, and just www-browser. None of these work, and the command doesn’t allow you to list all of its entries because the --all option forces you into an interactive session to configure every single option it has. I refuse to go through that mess just to find how browsers are defined.

This should be simple, shouldn’t it?

Worse, I’ve also found multiple users stating that this fails to adhere to system defaults on Ubuntu, despite it being Ubuntu’s official default method.. and it doesn’t even WORK.

There’s also Debian Wiki’s DefaultWebBrowser which is somehow too dense and too sparse, and in light of what I’ve already tried – nonsensical. Suggesting the use of a $BROWSER environment variable strikes me as particularly insane6.

The best option I can find right now is to manually scan every menu entry for programs categorized with WebBrowser3. This still excludes some options, is convoluted, and has the possibility of failing due to mistakes in menu entries. But it’s the closest to a future-facing solution I could find.

(While asking an unrelated question on the Linux Mint Forums, I did get more information about browser defaults, which you may find useful.)

Reading .desktop Files

Update: xdg-settings get default-web-browser basically has already done what I was doing here5. If anything has a .desktop file for a browser, that should return whatever default has been selected, thus telling you a browser is installed.4

Menus in Linux use a standard format to describe programs/actions so they can be automatically organized. Even some CLI apps create these entries as well. These are organized by predefined Categories, including WebBrowser. While this may still exclude some niche browsers, the users of such browsers likely will know how to handle this incompatibility, and are very very rare.

The only issue with this solution is finding the files and having to parse them yourself. This is still a little tedious, but will last longer and detect new browsers automatically. Unfortunately, the only information I found on where these files should be is conflicting slightly. The Arch wiki says they are in 3 specific locations, but the actual spec excludes one of those and specifies an environment variable where the rest are located. So I check all of these locations, scan every file, and look for two things: 1) A Categories key with “WebBrowser” in it. 2) No Hidden key.

For my specific use case, since I only need to know a browser is installed, I can exit this search on the first hit, but obviously you can use the spec to actually find which browsers are installed, and even launch them or show their icons!

For reference’ sake, here’s what a .desktop file for LibreWolf could look like:

[Desktop Entry]
Type=Application
Name=LibreWolf
Comment=Privacy-focused web browser
Exec=/path/to/librewolf
Icon=/path/to/icon.png
Categories=Network;WebBrowser;

What the hell am I doing this for anyhow?

I’m writing a script to help me automatically select and install packages on a Linux system so that I don’t have to remember or constantly look up how to install things that aren’t part of the standard package management of my system, or the names of things I may have forgotten about because I use them rarely.

The fundamental design of this script includes the ability to run installs and configurations in any order, except where dependencies require a specific order. Part of it removes Firefox due to Mozilla’s recent privacy violations7, another part adds a replacement browser, but other parts of the script require a functioning browser to function. Initially, I thought I just needed a check to see if any browser is present, but later I realized I need to be able to count how many browsers are installed. While I’m glad I found out there is a simpler way to check for any browser, I’m still using a manual scan of .desktop files because I need to count them.

(As I write this, I realize that while my current solution addresses most of my needs, I should have each browser-requiring section of the script verify a browser is present, instead of requiring the user to briefly have two browsers installed at once. While I still think it’s better to not remove a browser until its replacement is installed just in case, there is a hole in my dependency management here – especially because the user can do their own things outside of my script, obviously.)

Footnotes

  1. Any decision that adds future maintenance costs should be made very carefully and avoided if possible. Do you really want to devote a portion of the rest of your life to making sure this keeps functioning? Do you really want to limit your list of acceptable browsers to only what’s popular now?
  2. How long has it been and we still don’t force applications to open without stealing window focus at an OS level? There are always heavyweight applications, no matter how fast computers get, and computers are fundamentally designed for multitasking. We should be able to start a program and do something else while waiting for it to open, without the risk of random inputs being sent to the program when it is finally ready. Combine this with the fact that many programs steal focus before they’re even ready to accept input and it’s a garbage user experience.
  3. Thanks to Arran Ubels on StackOverflow for helping me find this answer.
  4. In theory, a browser could still be installed that has its desktop file in the wrong place, but then it is likely the system won’t recognize it for using open or similar commands anyhow. For my use case, that means it is not worth continuing to look for a browser being installed, but depending on what you’re doing, you may want to search further. Heck, if it’s really important, you might want to find every desktop file on the computer to check for a browser – though at that point you should probably just be doing something to ensure a browser is ready yourself instead of trying so hard to find an extant badly configured browser..
  5. Thanks to @CcxCZ on Telegram for telling me about this command! It’s really strange that it didn’t appear anywhere in my searches for information on this.
  6. It was pointed out to me that I should elaborate on this: Environment variables are easily manipulated and overwritten accidentally when they have simple names. (For a counter-example, I don’t worry about referencing $XDG_DATA_DIRS because it is unlikely to be screwed up.)
  7. A more aggressive take on it. While they can legally defend themselves on the basis of no evidence of violation occurring so far, so can every company that regularly destroys your privacy. Companies don’t remove assurances about privacy unless they’re going to stop protecting your privacy. Companies do not care about your privacy.

Updates

  • Updated 2025-06-12: Added a link to a thread on Linux Mint Forums that has additional information about default browsers.
  • Updated 2025-06-08: Added a section explaining why I’m doing this differently than the simple solution at the end.

Realms Half-Post-Mortem (from LD39)

I started writing this in September 2018, and didn’t get very far. It’s clear I will not finish it. That said, I do want a record of its existence here, so I’m publishing it now.


To describe it in one word, ambitious. An asynchronous web-based multi-user dungeon, that looks and feels like a terminal. Everyone starts in nullspace with 1 health. Kill players and take their souls to power up the other Realms to access them.

original welcome prompt
The original welcome message on Realms

As you can see, things were running perfectly well with no errors what-so-ever. Items were implemented, characters could see actions of others using a jury-rigged event system, and there was even a dummy that could be used to create souls!

Probably my biggest achievement was in flexibility I managed. That, or the fact that I put in a way to send me messages with the report command.

As with many of my game jam entries, I was overly ambitious and failed to deliver something complete. That said, I think it was at least an enjoyable experience to briefly search around in this pseudo-terminal.

When Open Source Maintainers Don’t Understand Community is Important

This is just to vent frustration at a thoroughly stupid experience I had recently. A portion of that stupidity is me failing to read something correctly, but I’m just really stuck on the stupidity of the response to me asking for help:

I asked for clarification, and was told to go away.

My reaction clearly indicates that I am not undrstanding something, and I even tried to give context to where I’m coming from so that it would be easier to spot what I misunderstood, but instead I was told to go ask a bot.

And then they blocked anyone from ever asking for help again.

The public is not allowed to open issues now.

What’s most frustrating to me about this is that it coincides perfectly with another issue I ran into today where I couldn’t add an important detail to an old issue. Past conversations are useful to people looking for assistance, especially when one solves their problem and explains it. When I am blocked from replying to something with a solution, anyone in the future experiencing the same issue is likewise blocked from finding the answer.

I now know what I messed up, but I’m not allowed to pass that knowledge to the future, because I was confused and made a mistake in how I asked for help.

There’s another layer to this that is often ignored: When this is the response the average newbie gets when they first try to contribute, they are encouraged to never ask again, or in the case of submitting pull requests, encouraged to never try to help again.

When open source maintainers discourage newbies, they cannibalize the future of their software.


Okay, that’s my entire point, but I also encounted some funny things as part of this.

What is a contribution? GitHub doesn’t know!

I think it’s interesting that GitHub says the repo limited opening issues / commenting on issues to past contributers, but I am a past contributer. GitHub clearly considers issues to be contributions, as every profile has a graph showing issues as part of their contributions:

My contributions: 89% commits, 11% issues.

AI tools can be very powerful, but they can also be very stupid

Earlier today, I tested Perplexity AI’s capability to answer a few basic questions easily answered through traditional search engines, such as which insect has the largest brain and which country is the current leader in development of thorium-based reactors. The results? It doesn’t know ants are insects, thinks fruit flies have large brains just because they have been the subject of a large number of studies, and ignores India in favor of China because western media reports on China a lot more.

But you know what, I wanted to test this asshole’s suggestion to ask ChatGPT about my problem, and surprisingly, it gave a very clear and accurate response!

Note (2024-10-02): Open AI has since removed the ability to access web sites from ChatGPT, and dumbed it down significantly. It is no longer a viable tool for most use cases.

ChatGPT points out what I misread: I have to clone the repo AND run NPM, not just run NPM.

When you offer binaries for a project, they have to actually exist..

To be fair, this is a fairly recent change to the ReadMe, but maybe you should publish binaries before advertising that you publish binaries?

Getting Started: Download a release binary and run it. Simple.
The advertised binaries don't exist.

Installation and usage aren’t the same thing

It’s understandable to be confused about whether someone has correctly installed something, but after confirming that installation has worked, ignoring the question asked is unhelpful to say the least.

After confirming that I've installed it, my question is ignored.

LuaJIT Strange & Unpredictable Behavior in table.sort

A quick story about discovering where Lua’s table abstraction fails to work.

In Lua, arrays should always be initialized sequentially, and should never have a nil inserted.

(The code behind this adventure starts on line 128 in one commit, where a crazy work-around was used to get what I wanted. This was eventually fixed in commit 8b8f36b (though accidentally sorting the wrong direction at that point). I provide these links to remind myself what happened, and so that if someone more knowledgeable than me wants to figure this out, hopefully they can. I’ve also made sure those links have been archived by the Internet Archive so they remain available even if the project is gone.)

While working on a prototype, I needed to sort a list of integers so that I could count the sum of the top thousand items in an array of ~40 thousand items. This list had empty indexes (or holes) within it, so I wrote a for loop to fill them with zeros and then called table.sort. It errored from trying to compare with a nil. This was confusing because I had a well-defined length that was used to fill holes, and the way Lua works, any hole in the list should be the end of what Lua recognizes as a list. As far as I knew, standard functions don’t allow holes in lists by ending at the first hole encountered.

for i = 0, list_length do
  if not list[i] then
    list[i] = 0
  end
end

I was further confused when I added debug code to check the list for nil values by iterating over it and printing every value in it: No nil values existed. The strangest part was when I passed my own sorting function to table.sort specifically to handle nil values and it created the same error. I worked around this by brute-forcing it with an incredibly inefficient thousand traversals of the entire list, removing items as I found the maximum each time. It’s a prototype, so whatever.

It didn’t sit right with me, especially because I couldn’t figure out how this was occurring. It’s incredibly rare to find an actual bug in highly used production software like programming languages, so I doubted I’d found an actual problem. Later, I realized I had added a zeroth element to this list, and Lua starts counting at 1, unlike a lot of programming languages. Removing this changed nothing, so I double-checked my assumptions and found that now nil values were showing up – and this is after no code changes except adding a for loop to print values.

I wrote a new sorting function that checks more thoroughly for nil values (I think the previous attempts at that failed because I’d made some simple error in how I was thinking about it). It worked. But then I found the loop whose code I’ve shared above, where holes are filled with zeroes. It had been a while, so I was very surprised to find what should have fixed things right there doing nothing.. somehow. I mentioned this in a group chat, and someone suggested replacing list_length with #list (the # operator returns length). The problem is that Lua stops counting at the first nil, so that should do nothing.

There’s also the possibility of an off-by-one error, those are very common, but I had assumed this would happen at some point and made sure all of my functions and loops checked one element earlier than necessary and one element later, so that if I checked too much – just extra zeroes, but I couldn’t check too little.

I wasn’t satisfied by all the nil values being there after a loop that replaces them with zero, so I tried making a loop at the top of the code that prefills a much longer list than I actually needed with zeroes. Suddenly, I didn’t need a custom sorting function. So why does filling the list with zeroes work in one place, but not another?

How Objects Work in Lua

In Lua all objects are called tables, and can contain an array-like section and arbitrary key-value pairs in a hashmap. It doesn’t really distinguish between these except as an optimization, under the hood it tries to guess how you intend to use the object and assigns values internally differently depending on what index you’re using. I thought that in practice, this means integer indexes are handled like arrays, and non-integer indexes are in the hashmap. I thought that maybe because my code was selecting integer indexes in a completely random order, it treated these as a hashmap instead, somehow leading to ignoring attempting to fill the holes – because there weren’t any holes, or the holes persisted because only part of the indexes were treated as an array.

Lua’s tables are an abstraction meant to make writing code easier, and for my years of working with them, they do that quite well, but maybe this is where the abstraction leaks? (All abstractions leak: They have some condition(s) where you end up needing to know how they work underneath, even though the point of abstraction is to not need to know how it works underneath.) Perhaps arrays are only recognized when they’re initialized sequentially, whereas my code initialized them in a random order? Either way, the error I started with manifests as “An array of integers can’t be sorted because part of it is undefined – even though every element is defined when iterated over.” which is not supposed to be possible.

There’s also the detail that in Lua there is not supposed to be difference between something having a nil value and it not existing at all. There is a difference, but it should be impossible to tell the difference or for the difference to matter in your code. It is possible that this alone is why I had this problem. Changing a value to nil inside an array table can cause all values within it to be converted to a hashmap, or some of them, or none of them. Again, this shouldn’t matter (other than a difference in speed which is usually unnoticeable), but it might matter.

I wrote a minimum test case to see if my hunch was correct, but it works when I expect it not to. I’m lost. I have an explanation that informs how I should code so that it works – and it does work, but because that explanation fails when tested, it seems incorrect. I want to know what’s really going on, but it requires a depth of understanding that I lack, and effort that I do not have the time and energy to go into.

I also learned that Lua defines this kind of behavior as “strange and unpredictable behavior” instead of undefined behavior, because undefined behavior means that an entire program can fail to function correctly do to one element with undefined behavior (even when that element is not accessed), whereas in Lua, a table exhibiting strange and unpredictable behavior doesn’t affect the rest of the program. So, if you can work around it, it’s okay.

At this point, I’m happy enough telling myself that arrays should always be initialized sequentially, and shouldn’t have nil elements ever. If I follow that, I probably won’t run into this. But.. if you’re reading this and understand what’s really going on here, please tell me.

YouTube Censorship Made Me Write a Script

Updated 2024-10-27: Recent blocking attempts have made downloading videos more difficult. I recommend downloading videos outside of the USA. I also recommend looking at alternative clients for watching videos such as Invidious and GrayJay.


YouTube’s been forcing creators to censor their works more and more, and often times after a successful publish of said content. More history and valuable information is being lost every day because a corporation controls the largest source of video content freely available.

At the same time, I’ve been running commands using yt-dlp over and over again for my own purposes, aside from this censorship. The syntax is relatively easy to forget despite being very clearly defined, so I finally made a script to handle it for me.

It’s in Lua because that’s what I prefer to use, and available on GitHub. Because it is based on yt-dlp, it works for any website supported by yt-dlp. Here’s how to use it:

Usage:
  ./video-dl.lua [action] <url>
[action]: What is desired.
  video (default): Highest quality video (maximum 720p).
  backup, clone, copy: English subtitles (including automatic subtitles), thumbnail, description, highest quality video (maximum 720p).
  music, audio: Highest quality audio only.
  metadata, meta: English subtitles (including automatic subtitles), thumbnail, description.
<url>: Source. YouTube URL expected, but should work with anything yt-dlp works with.

Information wants to be free. Help it.