Janet's Shenanigans

Yes, that title is too long and I know it.

If my previous blog post didn't make it clear, I don't like dealing with XML. Obtuse to write, obtuse to read. Given that I wrote a program so that I wouldn't need to write XML for an application menu protocol, it only makes sense that I would do the same for reading Wayland protocols. And thus, ReadWay and its non-web cousin ilo Welenko were born.

Parsing the XML

If you're familiar with Wayland, you're probably familiar with the XML files you can find in /usr/share/wayland and /usr/share/wayland-protocols. What you may not have noticed is the /usr/share/wayland/wayland.dtd file lurking alongside the core Wayland protocol. This is a document type definition file, which defines what a valid XML document looks like. Thankfully, this is a fairly simple DTD to write Go structures for. This DTD definition:

<!ELEMENT description (#PCDATA)>
  <!ATTLIST description summary CDATA #REQUIRED>

becomes this Go code:

type Description struct {
    Summary string `xml:"summary,attr"`
    Body    string `xml:",chardata"`
}

And this:

<!ELEMENT protocol (copyright?, description?, interface+)>
  <!ATTLIST protocol name CDATA #REQUIRED>

becomes

type Protocol struct {
    Name        string      `xml:"name,attr"`
    Copyright   string      `xml:"copyright"`
    Description Description `xml:"description"`
    Interfaces  []Interface `xml:"interface"`
}

Fairly simple, eh?

To unmarshal a protocol XML into a Go structure, you just xml.Unmarshal like this:

data, err := ioutil.ReadFile(path)
// handle error
proto := Protocol{}
err = xml.Unmarshal(data, &proto)
// handle error
// do something with proto

Templates

Of course, Go structs aren't particularly easy to read for documents even compared to XML. This is when Go's html/template package comes into play. You can throw a Protocol and a template at it like so:

<h1>{{ .Name }} <small class="text-muted">protocol</small></h1>

<p>
    {{ .Description.Body }}
</p>

{{ range $iface := .Interfaces }}
    <h2>{{ $iface.Name }} <small class="text-muted">interface version {{ $iface.Version }}</small></h2>

    <!-- finish rendering interfaces -->

{{ end }}

Of course, you have the more generic text/template package, which is what ilo Welenko uses. Same concept applies:

Kirigami.Page {
    title: "{{ .Name }}"
    ColumnLayout {
        {{ range $iface := .Interfaces }}
        Kirigami.Heading {
            text: "{{ $iface.Name }} version {{ $iface.Version }}"
        }
        {{ end }}
    }
}

(And yes, I am statically generating QML code in Go and loading it instead of marshalling it into Qt data types and using model/views/repeaters.)

See Also:

  • ReadWay hosted: ReadWay hosted on the internet. The “special thing that might happen when you drag an XML file onto [the] paragraph” is a Wayland protocol being rendered in your browser using WASM. The future is now. And it don't need no cookies.
  • ReadWay source: The static generator for ReadWay.
  • ilo Welenko: The desktop counterpart to ReadWay that renders into QML rather than HTML. At the time of this post, it's very incomplete compared to the web version.

Contact Me

Have any thoughts/comments/concerns about this post, or want to tell me that I shouldn't statically render QML? Here's how you can contact me:

  • Telegram: @pontaoski
  • Discord: pontaoski blackquill 🏳🌈#8758
  • Matrix: pontaoski@tchnics.de
  • IRC: appadeia_
  • Email: uhhadd@gmail.com

Tags: #libre

Go is one of the best languages to write a parser and tools that need some form of parsing in. This is mainly due to:

  • Great string and regexp functions in the stdlib for parsing
  • Easy and safe introspection for blank interfaces (Go's equivalent of a QVariant or a void pointer)
  • Labels. You have both gotos and the ability to break and continue deeply nested loops, which is great for handwritten parsers.
  • Fast compilation makes for fast iteration.

Screw XML

XML is unwieldy to write and obtuse to read. Unfortunately, things like Wayland use it for protocol descriptions. Fortunately, Go can be used to author tools that generate XML from a more human-readable format.

Introducing the Participle

Participle is a Go library that makes writing and parsing data into ASTs extremely easy. I'll demonstrate a simple usage of it for authoring a better Wayland protocol syntax that can transpile to XML.

One: Designing a syntax

This is mostly up to your opinion: I like the aesthetic of Go, so I went with a very Go-like aesthetic:

protocol appmenu

interface zxdg_appmenu_v1 {
    version 1

    request set_address(service_name string, object_path string)
}

Simple, yet descriptive.

Two: Building trees

Participle by default uses the tokens that form the Go language itself, which is important to know. A grammar has to play by Go rules if you stick with the default tokens.

Let's start by defining a simple protocol struct:

type Protocol struct {
}

It's empty, which isn't very useful. Let's give it a name element since we want to be able to name our protocol.

type Protocol struct {
    Name string
}

This looks like a nice start to our tree, but how does the parsing work? We add some metadata.

type Protocol struct {
    Name string `"protocol" @Ident`
}

This will tell Participle two things:

  1. It should look for the string protocol in our protocol grammar
  2. It should grab the next Identifier token and put it into the field

Now, we probably want to add a hook for an interface, as a protocol without interfaces is useless. Let's write that in:

type Protocol struct {
    Name       string      `"protocol" @Ident`
    Interfaces []Interface `{ @@ }`
}

The { @@ } will instruct the parser to capture as many interfaces as it can and stuff them into the array.

Now let's write a description for what we want an interface to look like, starting with a name.

type Interface struct {
    Name   string     `"interface" @Ident "{"`
    // Put the goodies here!
    Ending struct{}   `"}"`
}

The purpose of the Ending field is to make sure that our interfaces end with a closing bracket.

An interface is composed of requests. Let's take a closer look at what our design looked like:

request set_address(service_name string, object_path string)
^
|
| always "request"
request set_address(service_name string, object_path string)
        ^
        |
        | Must be a valid identifier
request set_address(service_name string, object_path string)
                    ^^^^^^^^^^^^^^^^^^^
                    |
                    | One unit with two parts: identifier and type
request set_address(service_name string, object_path string)
                   ^                                       ^
                   |                                       |
                   | these surround our arguments          |
request set_address(service_name string, object_path string)
                                       ^
                                       |
          this separates our arguments |

Describing this will roughly look like this:

"request" @Ident "(" argument, argument ")"

Let's put that into a struct:

type Request struct {
    Name      string     `"request" @Ident "("`
    Arguments []Argument `{ @@ [","] } ")"`
}

{ @@ [","] } is a fancy way of making the Arguments field say “capture as many of me as possible, and we might have a comma separating us.”

Now let's write an Argument struct.

type Argument struct {
    Name string `@Ident`
    Type string `@Ident`
}

Since this is basically just a tuple of identifiers, that's exactly what we made this struct.

Because an interface can have multiple requests, we add the following field to our Interface struct: Requests []Request `{ @@ } Like above, { @@ } will try and capture as many Requests as possible.

Put together, all our structs look like this:

type Interface struct {
    Name     string       `"interface" @Ident "{"`
    Requests []Request    `{ @@ }`
    Ending   struct{}     `"}"`
}
type Protocol struct {
    Name string `"protocol" @Ident`
}
type Request struct {
    Name      string     `"request" @Ident "("`
    Arguments []Argument `{ @@ [","] } ")"`
}
type Argument struct {
    Name string `@Ident`
    Type string `@Ident`
}

Three: parsing trees

Now that we have our AST designed, let's hook it up to Particple.

parser := participle.MustBuild(&ProtocolDescription{})
protocol := Protocol{}
parser.Parse(os.Stdin, &protocol)

That's easy, eh? Since building XML output is fairly straightforward (just build structs corresponding to the XML output and marshal the AST into them and marshal the structs into XML), I won't be covering that here.

From Here

Some links you may find useful:

blankInterface: A more complete Wayland protocol parser and XML generator.

Participle: The parser library used.

encoding/xml: XML library in Go's stdlib.

Tags: #libre

Note: acceptable from the perspective of a Tetris fanatic who regularly uses jargon like SRS, lock delay, DAS, ARR, etc. For the casual player, these games are perfectly fine. Albeit, I would recommend Quadrapassel over KBlocks to casuals because of the better rotation.

Errata: I mention that KBlocks can only repeat in one direction. It can actually rotate in both directions, it just breaks the norm with its default keybindings and that confused me.

the heck is a “DAS”?

  • DAS: delayed auto start: how long it takes for a piece to start flying to the wall
  • ARR: auto repeat rate: how fast a piece flies to the wall
  • SRS: super rotation system: the guidelines defining how pieces rotate.
  • lock delay: how long you have to move a piece before it locks after it touches a surface.

Why other open source implementations suck

Quadrapassel

The board is the wrong size. That's all you need to know to avoid this one.

Besides the incorrect size, Quadrapassel is barely SRS conformant (albeit the rotation handling is much better than that of KBlocks, which I'll get onto in a bit.)

Timing is also way off, with no lock delay, too much DAS, and not enough ARR.

KBlocks

The board is the correct size, but somehow the rotation handling is even worse than Quadrapassel, because pieces rotate around the center of their occupied region and not around the center of the pieces themselves.

There is only one correct rotation method:

Chart of rotations

Additionally, you can only rotate in one direction.

Like Quadrapassel, timing is off: no lock delay, too much DAS, not enough ARR.

What Nullpomino does right

Nullpomino offers one thing hardcore Tetris fans love: absurd fine-tuning. Each and every aspect can be configured, from DAS, ARR, lock delay, etc.

Additionally, there's a ton of gamemodes that exercise every skill a Tetris player can exercise. From plain single-player Tetris to all sorts of specialty training modes to multiplayer, Nullpomino has it all.

Also, Nullpomino is the fan game that you see in Tetris communities.

You can tell that it was made by Tetris fans for other Tetris fans.

Tags: #libre

wan: kama jo e ilo Ako lon poka CD

o kepeken ilo Pgp tan ni: kama sona e ni: poka CD li pona ala pona?

tu: pona e ilo pi pana sona

o kama sona e ni kepeken ni:

# ping archlinux.org

tuli: pona e ilo tenpo

timedatectl set-ntp true

po: pona e tomo pi ilo sina

# fdisk -l

lukin ike la o pona e ona.

luka: kama sona

pipi: pana tomo e tomo pi ilo sina

# mkfs.ext4 /dev/sdX1

site: pana tomo e sona pi ilo sina

# mount /dev/sdX1 /mnt

ete: pana ilo e ilo sina

# pacstrap /mnt base linux linux-firmware

nen: pana e sona tomo tawa ilo sina

# genfstab -U /mnt >> /mnt/etc/fstab

ten: pana pona e ilo sina

ten wan: pona mute a!

reboot

doze: wtf

porque usou este? lmao

rust is quite a neat language, isn't it? gigantic library ecosystem, memory safety, tons of developer-friendly tools in it. for Ikona, I decided to utilise this language, and instead of relying on binding generators that hide half the magic away from you, I wrote all bindings by hand.

rust –> C++ by hand: how?

obviously, rust and C++ are different programming languages and neither of them have language-level interop with each other. what they do both have is C. C—the lingua franca of the computing world. unfortunately, C is a very bad lingua franca. something as basic as passing arrays between programming languages becomes boilerplate hell fast. however, it is possible and once you set up a standardised method of passing arrays, it becomes far easier.

rust to C

so, in order to start going from rust to C++, you need to stop at C first. for Ikona, I put C API bindings in a separate crate in the same workspace. you have a few best friends when writing rust to C here: – #[no_mangle]: keeps rustc from mangling your symbols from pure C – unsafe: because C is ridiculously unsafe and Rust hates unsafety unless you tell it that you know what you're doing – extern "C": makes rust expose a C ABI that can be eaten by the C++ half – #[repr(C)]: tells rust to lay out the memory of a thing like C does – Box: pointer management – CString: char* management

memory management

Box and CString are your friends for memory management when talking to C. the general cycle looks like this:

pub unsafe extern "C" new_thing() -> *mut Type {
    Box::into_raw(thing) // for non-rustaceans, the lack of a semicolon means this is returned
}
pub unsafe extern "C" free_thing(ptr: *mut Type) {
    assert!(!ptr.is_null());
    Box::from_raw(ptr);
}

into_raw tells rust to let C have fun with the pointer for a while, so it won't free the memory. when C is done playing with the pointer, it returns it to Rust so it can from_raw the pointer to free the memory.

structs

for Ikona, I didn't bother attempting to convert Rust structs into C structs, instead opting for opaque pointers, as they're a lot easier to deal with on the Rust side.

an average function for accessing a struct value in Ikona looks like this:

#[no_mangle]
pub unsafe extern "C" fn ikona_theme_get_root_path(ptr: *const IconTheme) -> *mut c_char {
    assert!(!ptr.is_null()); // make sure we don't have a null pointer

    let theme = &*ptr; // grab a reference to the Rust value the pointer represents

    CString::new(theme.root_path.clone()).expect("Failed to create CString").into_raw() // return a char* from the field being accessed
}

this is very similar to how calling methods on structs is bridged to C in Ikona.

#[no_mangle]
pub unsafe extern "C" fn ikona_icon_extract_subicon_by_id(
    ptr: *mut Icon,
    id: *mut c_char,
    target_size: i32,
) -> *mut Icon {
    assert!(!ptr.is_null()); // gotta make sure our Icon isn't null
    assert!(!id.is_null()); // making sure our string isn't null

    let id_string = CStr::from_ptr(id).to_str().unwrap(); // convert the C string into a Rust string, and explicitly crash instead of having undefined behaviour if something goes wrong

    let icon = &*ptr; // grab a reference to the Rust object from the pointer

    // now let's call the method C wanted to call
    let proc = match icon.extract_subicon_by_id(id_string, target_size) {
        Ok(icon) => icon,
        Err(_) => return ptr::null_mut::<Icon>(),
    };

    // make a new Box for the icon
    let boxed: Box<Icon> = Box::new(proc);

    // let C have fun with the pointer
    Box::into_raw(boxed)
}

enums

enums are very simple to bridge, given they aren't the fat enums Rust has. just declare them like this:

#[repr(C)]
pub enum IkonaDirectoryType {
    Scalable,
    Threshold,
    Fixed,
    None
}

and treat them as normal. no memory management shenanigans to be had here.

ABI? what about API?

C has header files, and we need to describe the C API for human usage.

structs

since Ikona operates on opaque pointers, C just needs to be told that the type for a struct is a pointer.

typedef void* IkonaIcon;

enums

enums are ridiculously easy.

#[repr(C)]
pub enum IkonaDirectoryType {
    Scalable,
    Threshold,
    Fixed,
    None
}

becomes

typedef enum {
  ScalableType,
  ThresholdType,
  FixedType,
  NoType,
} IkonaDirectoryType;

not much to it, eh?

methods

methods are the most boilerplate-y part of writing the header, but they're fairly easy. it's just keeping track of which rust thing corresponds to which C thing.

this declaration

pub unsafe extern "C" fn ikona_icon_new_from_path(in_path: *mut c_char) -> *mut Icon {

becomes

IkonaIcon ikona_icon_new_from_path(const char* in_path);

C to C++

once a C API is done being written, you can consume it from C++. you can either write a wrapper class to hide the ugly C or consume it directly. here in the KDE world where the wild Qt run free, you can use smart pointers and simple conversion methods to wrangle with the C types.

advantages

the big advantage for Ikona here is the library ecosystem for Rust. librsvg and resvg are both Rust SVG projects that Ikona can utilise, and both are better in many ways compared to the simplistic SVG machinery available from Qt. heck, resvg starts to near browser-grade SVG handling with a huge array of things to do to SVGs as well as general compatibility. Ikona barely taps into the potential of the Rust world currently, but future updates will leverage the boilerplate laid in 1.0 in order to implement new features that take advantage of the vibrant array, high performance, and fast speed of available Rust libraries.

what I would have done differently

writing a bunch of rust to C boilerplate isn't fun, especially with arrays. since glib-rs is already in the dependency chain of Ikona, I should have utilized the GList instead of writing my own list implementation.

tags: #libre

tenpo ni li tawa sitelen e ni.

ilo Ikona lon. ona li kama e ale (pi sitelen Ikon).

wan lili

ni li wan tawa mi tan ni: mi pali e ona. ona li pona mute.

ona li ilo KDE li ilo Rust li nanpa wan. mi sona e rust-qt-binding-generator. taso mi lukin ala e ilo pi li kepeken ona.

ilo

Ikona's home screen

ilo Ikona li pana e pali tu tawa sina: lipu kule anu lukin pi sitelen Ikon.

Ikona's colour palette.

ilo Ikona li jo e lipu kule pona. lipu kule li tawa sitelen Breeze.

Ikona's preview screen, light Ikona's preview screen, dark

kepeken ilo Ikona la sina ken e ni: lukin e sitelen sina lon lipu pona.

tenpo nanpa wan la sitelen Ikon ona li sama lukin e sitelen Ikon Ikona. taso ona li ken pona mute. ona li ken pali e ijo .ikona.app.svg. tan ona la ona li ken pali e ijo lili. sina pali e sitelen Ikona sina la sitelen Ikon pi ilo Ikona li sama lukin e ona.

Export screen

kepeken e ni la sina ken toki e ni: “ilo Ikona o, pali e ijo lili tan ijo suli.”

ilo Ikona li ken sitelen e lipu pi sitelen Ikon sina tan ni: sina ken pana e ona tawa jan ante.

ilo Cli

 ➜ ikona-cli
ikona-cli 1.0
Carson Black <uhhadd@gmail.com>
Command-line interface to Ikona

USAGE:
    ikona-cli [SUBCOMMAND]

SUBCOMMANDS:
    class       Class your icon
    convert     Convert your icon from light <-> dark
    extract     Extract icons from an Ikona template file
    optimize    Optimize your icon

ilo lon ilo Ikona Cli li ni: – class: pali lukin. – convert: suno tawa mun en mun tawa suno. – extract: ijo suli li pana e ijo lili. – optimize: sitelen ike li kama sitelen pona.

tenpo kama la...

ilo Ikona li ken pali e sitelen Ikon mute mute mute.

ilo Ikona li ken pali e sitelen Ikona pi li jo ala e kule.

Tags: #tpolibre

welp, looks like it's finally time to write this :D

so, ikona 1.0 is here and ready to take on the world (of helping icon designers).

some firsts

so, this is a personal first for me. it's the first time I've released a GUI application that I feel like is actually thoroughly polished.

I believe this is also the first KDE application to be released that's predominantly programmed in Rust—I'm aware of rust-qt-binding-generator, but I haven't seen any KDE apps consume it.

the application itself

would be heretical to write a blog post about the 1.0 release of Ikona without talking about what it actually is, ja?

Ikona is a companion application to a vector editor like Inkscape, providing utilities for wrangling with icons and an icon preview.

Ikona's home screen

Ikona opens up to a fairly unassuming screen, giving users two options: the colour palette or the icon view.

before we get to the meat of Ikona, let's look at the colour palette.

Ikona's colour palette.

Ikona's colour palette is fairly simple—it shows a bunch of colours, and clicking them copies the hex code. the colour palette was designed to offer icon designers a vibrant and large array of colours that fit into the Breeze style.

Ikona's preview screen, light Ikona's preview screen, dark

this is where Ikona's meat lies—the application icon view. it displays application icons at a pixel-perfect size in an environment similar to a Plasma desktop.

by default, it just shows Ikona's icon. the real meat is when you press “Create Icon.” this exports a special type of SVG with the suffix .ikona.app.svg.

the .ikona.app.svg is a special type of input SVG that ikona knows how to process. normally, multiple sizes of an icon are stored as different files, making managing all of them cumbersome. however, the .ikona.app.svg combines all sizes of an application's icon into a single file, making it easier to cross-reference elements shared between sizes in the same file. this also allows Ikona to intelligently split and place icons in the correct locations on export.

Ikona can also support regular SVG files, however only one size of icon can be previewed at a time and Ikona cannot export optimized icons from this format.

saving the icon will cause Ikona to instantly update its preview of the icon.

once you're done designing your icon, you use the export screen to export your icon.

Export screen

you can select which sizes to export, and how to export the icon (to one folder with different names, or to folders per size with same name).

you can also take montages of your icon using Ikona. for ease of sharing, the montages are copied directly into your clipboard for pasting into your favourite chat application.

that's it for the GUI application, but not for Ikona.

ikona-cli

Ikona isn't just a GUI application—there's also a fully independent command line interface to its functionality.

 ➜ ikona-cli
ikona-cli 1.0
Carson Black <uhhadd@gmail.com>
Command-line interface to Ikona

USAGE:
    ikona-cli [SUBCOMMAND]

SUBCOMMANDS:
    class       Class your icon
    convert     Convert your icon from light <-> dark
    extract     Extract icons from an Ikona template file
    optimize    Optimize your icon

There are four subcommands: – class — Injects stylesheets and replaces colours with stylesheet colours. – convert — Converts light icons to dark and dark icons to light. – extract — Allows splitting .ikona.app.svg icons into multiple files on the command line. – optimize — Optimizes your icon with a variety of methods. Unlike more commonly used SVG optimizers, Ikona is able to optimize for ease of rendering, reducing the work SVG libraries have to do to render an icon. this translates to faster rendering and better performance, despite a slightly larger file size.

for the next release

for the next release, two features are planned:

­— wrangling with icon themes. icon themes are a pain to deal with, and a tool like Ikona can be scaled to wrangle with hundreds or thousands of icons instead of just a few being designed. — monochromatic icon preview stylesheet injection and classing are perfect for dealing with monochromatic icons, and Ikona will be able to preview them.

for the packagers

yes, rust sucks to deal with.

if your distro mandates that you aren't allowed to bundle dependencies, most of Ikona's dependencies are dependencies of librsvg, a package that most distros should have. this means only a few new packages are needed if they're not already used by other applications.

if your distro is fine with you bundling dependencies, then you're in for happy days. just rename the cargo vendor tarball to ikona.cargo.vendor.tar.xz and plonk it in the source root alongside CMakeLists.txt. CMake will take care of the rest of the job for you.

Tags: #libre

Taigo

I've recently started work on an app called Taigo (ilo Tajiko). It's basically Tamagotchi, but GNOME-ified and on your dekstop. Among its features are an extreme aversion to theming. So strong, that I had to remove some of its aversion as it got in the way of me developing it. I may re-add the removed blocks if I feel like it. In particular, I really like the icon that I made w/ the help of LCP. (jan Jesepi)

openSUSE Welcome

So like, I've been working on openSUSE Welcome some. Mostly in the Xfce area, as a quick customisation screen is in the process of being added. Some language-specific social medias have been added as well.

Fluent Stylesheet for GTK (win32, updated)

This WIP stylesheet is what I'm working on when I have nothing else to do. It has one goal: Replace gtk's current win32 stylesheet as the stylesheet of choice for GTK apps running on Windows. As its name implies, it's modeled after Fluent design.

kulupu Linu

The Linux discord nobody asked for! :D Basically your run-of-the-mill Linux discord, but since I can't get any active members, I've been using it as my testing place.

Link for the (probably not) interested.

Pali Pona

This project is where I'm stashing all of my Toki Pona apps. It's currently home to lon (weather) and ilo pona (dictionary).

My to-do list:

Here are some things I'll (probably) work on:

  • Breeze GTK gradients. I dropped these when giving Breeze GTK the ability to respect the user's colourscheme. Now that all the infrastructure is in place for it, I can add it back.
  • libdnf version lock plugin. Little experience with the library, little experience with the programming language, and absolutely no idea how to test what you're doing is working? sign me the eff up
  • GObject state machine library. I need one of these for Taigo, so might as well make it public.

Shameless plug for /r/mi_lon – the /r/me_irl for Toki Ponans

Toki! \o

There are two types of Discord servers: those with less than 100 members, and those with 100 or more members.

As you near 100 members, things change – enough people are in your server to where you no longer have to actively go out of your way to advertise to get the server to grow. Your issues will change from those related to too few people to those related to too many people.

Pre-100 Preparations

There are some things you absolutely need before hitting 100 people:

  • At least one or two other top-level administrators.
  • At least one moderation bot to assist moderation.
  • A well-defined channel layout that will not be changed in the near future.

Getting past 100 members

The Ideal Method

Ideally, you would have an existing community of people that were looking for something unfulfilled by existing servers. If you have that community of people, you should do the following things:

  • Tailor the server to meet the fulfillment that your existing community desires, without over tailoring.
  • Getting a concrete staffing and setup ASAP is critical here – moreso than when you're starting from true scratch.

The From Scratch Method

If you don't know anyone interested in what server you're doing, you face a different set of arguably harder challenges than a server with a preexisting community.

  • You need to get a critical mass of a few active members- these mates will be your lifeblood for a long time.
  • You're more free to make changes until your server's concrete starts drying, so to speak. Enjoy this freedom and find what works before you have to start minding the community having to adapt.
  • Partnerships are critical- partner with other servers your size.

After 100

Don't be afraid to celebrate hitting 100 people- you made the most critical milestone of a server, and you should be proud of it.

I would explain what to do after 100 people, but that's outside the scope of this blog post. Mi tawa! \o

So, out of boredom one day, I decided to try elementaryOS. Here are my thunks.

Installer

Elementary Installer Good old Ubiquity. Not fancy, but does the job well. Keyboard Layout Selection Updates and other software Installation type

The Destkop

Desktop ElementaryOS starts up to a desktop showing off the wallpaper, the top panel, and the dock. The wallpaper is similar to macOS's “Antelope Canyon.” Of course, the desktop doesn't make the distro, the apps do.

The Apps

Mail

Mail It's okay, I guess? User interface isn't particularly well organized and the top bar feels messy.

Terminal

Terminal This is all the customization you get. 0/10 needs something other than Solarized variants.

Unsplash Browser

Unsplash Browser I forgot what this application was called, but it's kinda neat I guess? Lets you browse Unsplash, albeit the interface stands out from the other AppCenter curated apps.

Applets and AppCenter

Applets and AppCenter AppCenter is pretty neat. It looks pretty decent, the curation is nice, and it's respectable. The applets are kinda neat. Only issue with them is that there doesn't seem to be an easy way to get to your open applets besides mashing the minimize keycombo.

Other Things

Keyboard Shortcuts

Keyboard Shortcuts This key listing looks pretty nice. It's odd seeing this pop up when you hit Super (Win key) instead of an app launcher, but it's convenient.

General Thoughts

elementaryOS is overall pretty neat, but visually inconsistent and some of the apps are eh. Even so, the maturity of the app ecosystem for elementaryOS-specific apps is well beyond that of apps specific to other desktop Linux platforms.