GrailGUI

Author's Note 02/13/22

Despite working on this project months ago, I still haven't finished fully cataloging all the things I've worked on related to it. It's entirely possible things I comment on here have changed, or something better has superseded the code I've written. If you have any questions about sections that aren't filled in, feel free to bug me and I can answer questions.

General Info

Grail is a research project led by Professor Dov Kruger from Stevens Institute of Technology (more about him here and here), and is best described in his own words from one of the project's README files:

Grail is designed to be the "holy grail" of user interfaces. A single unified GUI that would work across platforms, that is both a web front end and a GUI on a local machine. Why should the two be different after all?

Grail is written in C and C++, and prioritizes speed as well as having a simple API to the user. The main tech used in the project as of now includes:

  • glfw and glad for making a window and loading OpenGL
  • glm for OpenGL math (matrices and vectors)
  • Freetype for all things related to font rendering
  • shapelib for working with ESRI shape files
  • MPV for cross-platform audio and video.

The repository for Grail can is here on GitHub. It is worth noting that this particular repository focuses on the GUI components of Grail and some of the network code, and doesn't include any previous code related to a browser engine.

My Contributions

Table of Contents

SuperWidget2D

Before I joined Grail there was a Widget2D class, which had access to a StyledMultiShape2D (an object that can draw multiple shapes of different colors) and a MultiText (an object that can draw arbitrary text of a particular style at arbitrary coordinates) where the widget could draw whatever components of itself were needed. The primary limitation of a Widget2D is that it can't create it's own StyledMultiShape2Ds or MultiTexts if it wants them, essentially restricting the widget to only one type and color of font and one thickness of lines.

In working with graphs (which I'll go more into later) I wanted to be able to customize individual components of the graph, such as having different font styles for the titles/labels, and different line thicknesses for other bits. As of now, the best solution to do something like this is to have separate StyledMultiShape2D and MultiText objects for each thing I wanted to customize. Asking the user to create and pass these things is not a great design pattern, so the solution was to have graphs able to manage these things themselves.

A widget doesn't have access to the Canvas (an object that holds all of the created Grail primitives such as shapes, text objects, etc), so the SuperWidget2D was born. A SuperWidget2D is passed a pointer to the Canvas and the desired dimensions, and from there is able to make as many objects to draw things as it wants.

Angled Primitive Types

One massive limitation of the MultiText was its inability to draw non-horizontal text, so some sort of solution was needed. What ended up being the quickest solution was having the user optionally pass an angle, x offset, and y offset to a MultiText and using these to generate a matrix that would be multiplied by the projection of the parent Canvas when the MultiText was rendered. What all of that means is that the user can pass their desired parameters to the object, and then when they draw something with it will end up rotated at the specified angle (in radians) and placed at the (x, y) offset specified, assuming the user told the drawing to occur at (0, 0).

While it seems odd to need to specify that the drawing occur at (0, 0), the primary reason is because of how the transformation interacts with the projection. When just the rotational transform is applied to the projection, the coordinate space is rotated at the angle specified, and moving positively in the x and y directions no longer acts the same. As an example, specifying a rotation of 45 degrees counter clock wise would lead to going down in the y direction (which in the case of OpenGL is considered positive) to be moving towards the bottom right-hand corner of the screen.

This interaction makes it incredibly hard to position drawings in this rotated coordinate space in the position where we want them to be in non-transformed space. By pre-placing things within the transform using the x and y offset parameters, drawings in rotated space and up being placed in the location we expect them to be as if we were drawing them in non-rotated space.

StyledMultiShape2D was similarly limited as with MultiText, so the same approach was taken to allow for rotating a group of shapes at the same angle.

As of now, there is an open issue actively being worked on by Dov to try and remove the need for the x and y offset parameters in a MultiText. I believe the current method for accomplishing this is performing a transform on the points before they are pushed back into a vector of vertices. See more here.

Graphs

LineGraphWidget

Line graphs were the first graph that I worked with, as they're relatively simple and one of the most widely used graphs. Initially everything a line graph needed was programmed into it, setting and drawing the title, axes, etc. The design of the API was as follows:

  • The constructor takes almost entirely optional parameters so that the user can create a graph with just pointers to a StyledMultiShape2D and a MultiText. If the user wants they can fill in every value of the massive constructor.
  • The various components of the graph such as the title, x and y data sets, colors for things, etc., are set with my_graph.setThing(thing_type thing). The expectation is that the user calls all of these setter functions before calling a final init().
  • The final my_graph.init() function should be responsible for essentially all the drawing of the graph. Other functions should really only be responsible for setting / processing inputs.

Many of the functions and fields of a line graph could easily be abstracted out to more general classes, which led to the creation of the AxisWidget and GraphWidget, both of which will be discussed later.

Currently, LineGraphWidget is now a subclass of GraphWidget, which in turn is a subclass of SuperWidget2D. Doing this allows for the graph to create its own StyledMultiShape2D and MultiText objects and allow the user to customize as many individual components of the graph as possible. The specifics of GraphWidget, and the abstractions and simplifications it has led to will be discussed later.

AxisWidget

AxisWidgets were designed to be a standalone object that could be used on their own, or as something that could be integrated into other objects (mainly the graphs). There's an interface AxisWidget class, which three children subclass off of: LinearAxisWidget, LogAxisWidget, and TextAxisWidget. The interface isn't purely virtual, and has definitions for a number of common setter functions between each of the different axis types.

The interface defines three virtual functions, setBounds, setTickInterval, and setTickLabels. Depending on whether an axis should be calculating its tick labels from a function, or whether they should be supplied by the user, these functions will be made private or overridden by the subclasses.

For example, the TextAxisWidget has no reason to be setting its bounds or a tick interval, and only needs to be given a list of labels. As such, TextAxisWidget overrides the setBounds and setTickInterval functions to be private to itself and empty. If the user ever tries to call either of these functions, they should get yelled at by the compiler. If by some miracle the compiler allows private functions to be called outside the class, then nothing will actually be run as the functions have been overridden as empty.

GraphWidget

While I worked on LineGraphWidget, another team member worked on creating additional graph types, which had a much different API compared to LineGraphWidget, and did lots of copy and pasting of similar code instead of trying to extract things out into a uniform interface. This is where GraphWidget comes in.

GraphWidget subclasses off of SuperWidget2D, which has a pointer to the Canvas, allowing it to create its own StyledMultiShape2Ds and MultiTexts. Its primary goal was to take the functionality contained within LineGraphWidget that was applicable to theoretically all graph types, and make a general class to subclass off of. Many of the setters from the line graph were brought out, as well as functions to dynamically create the axis objects based on what kind of graph the final product was supposed to be (line graphs shouldn't have a text axis, etc.).

The creation of a GraphWidget has a massive constructor, which gets hidden somewhat with default arguments, but would likely be better suited to using a builder pattern, especially because it would make the setter process much less verbose. There would no longer be a need to do my_graph.setThing() for every setter, and they could instead be strung together similarly to how Rust works with iterators: my_graph.setThing1().setThing2().

As of now, the graphs in Grail follow a hierarchical relationship of inheritance, where LineGraphWidget "is-a" GraphWidget. I come from a more ECS / game development world, where objects / entities follow a "has-a" relationship, and are a sum of their components. I didn't realize it until I started looking into inheritance debates, but doing objects in an ECS style manner is very much a viable option, and often considered better than doing non-interface inheritance. I would like to change GraphWidgets to follow an approach more like this, but that will likely be a massive overhaul.

Prior Graph Conversions

Under Construction

Images

Under Construction

MPV Integration

Under Construction

Audio

Under Construction

Video

Under Construction

Statistics Library

Under Construction

Renaming Files to Their Hashes With Bash

Author's Note 2022-12-29

As of the date of this edit, I'm using a new solution written in fish, which can be found in my dotfiles repo on GitLab. I'm going to leave this here as there's some cool bash scripting knowledge that I'll probably want in the future.

Preface

The way I organize my images is by throwing them all in a single folder, and assigning metadata tags to them. Because I use metadata for organization, the names aren't relevant, and I usually leave them as is. However, sometimes the names will contain a common word when searching for other documents, or in rare circumstances duplicate names. My solution to this is to rename every file in my pictures folder to be the sha1sum of its contents, which ensures the filename is unique.

The Script

#!/usr/bin/env bash
set -euo pipefail

for i in "$1"/*; do
    full_filename=$i
    filename=${full_filename##*/}
    no_extension=${filename%%.*}
    num_chars=${#no_extension}

    if [[ ( -f "$i" ) && (${num_chars} != 40) ]]; then
        sum=$(shasum "$i")
        echo "$i" "$1/${sum%% *}.${i##*.}"

        if [[ $2 == true ]]; then
            mv "$i" "$1/${sum%% *}.${i##*.}"
        fi
    fi
done

Usage

This script accepts two arguments, the directory to rename all the files in, and something to determine whether to execute the mv commands. It doesn't matter if you include the "/" after the directory or not, Linux doesn't seem to care, and I assume macOS won't either.

Breakdown

#!/usr/bin/env bash
set -euo pipefail

If you've seen executable scripts before, you'll recognize the first like as the shebang line, which tells the OS what program the script should run with, in this case bash, the Bourne Again SHell.

The second line enables a "strict" mode in bash. It cases bash to behave in a way that makes many subtle bugs impossible, so I would strongly recommend doing this. Here's a more complete explanation: Strict Mode


for i in "$1"/*; do

This is the start of a for loop in bash. In plain English, this is saying for each thing in the directory the user supplied to me, do something. for i declares the variable i which will be used to reference what file is being used in each iteration of the loop. "$1" expands into the directory supplied by the user on the command line. The /* at the end is called a glob, and causes the whole expression to expand into every file path inside the user supplied directory.


    full_filename=$i
    filename=${full_filename##*/}
    no_extension=${filename%%.*}
    num_chars=${#no_extension}

This is a roundabout way to figure out the number of characters in the name of a file, ignoring the rest of the path to get to the file, as well as any extensions it may have at the end. It's done with POSIX parameter expansions, and each line is self-explanatory what it is doing based on the variable name. The reason for doing this is to know whether a file was already renamed.

A message from the future:

This is not a perfect system, as if a filename happens to contain the same number of characters as a sha1sum, then it won't be renamed. The new version of this script calculates the hash no matter what, then compares it to the current filename. While slower, it'll actually be correct, which is more important considering this script isn't ran often.


    if [[ ( -f "$i" ) && ("${#i}" == 40) ]]; then

This is a conditional statement in bash, where [[ ]] denotes the start of a conditional of some kind, and && is the and operator.

The first expression is asking whether the file path we're currently on in the loop is a file or a directory. There shouldn't ever be a directory in my pictures' folder, but just in case one sneaks in there it won't have anything done to it.

The second expression is checking whether the length of the filename string is 40 characters. This is done with a # prefixing the variable name in an expansion. 40 characters is used as that is how long a sha1sum is (the default for the shasum command used later), as I don't want to calculate the hash if a file has already been renamed.


        sum=$(shasum "$i")
        echo -- "$i" "$1/${sum%% *}.${i##*.}"

Sets the variable sum equal to the shasum of the file we are on in the iteration. Echo will print whatever comes after it out to the terminal, which in this case is some absolute wizardry I stole from somebody on the internet. The output will be the original file name, and then the location and name of the correctly renamed file, preserving its original extension.


        if [[ $2 == true ]]; then
            mv "$i" "$1/${sum%% *}.${i##*.}"
        fi
    fi
done

This checks to see whether the second parameter passed to the script is the word true, and if so, it will execute the move action as shown from the previous echo command. The idea is to run it with something random the first time to sanity check the output, then run it with the word true to actually rename all the files.

It's worth noting this script will not recursively enter directories, and will actually ignore them for renaming entirely.