All Projects
Bar Daemon
A daemon which uses tokio::UnixStream sockets to monitor system resources.
It creates notifications via dunstify when resources change, as well as sending updates to all the UnixListener processes, so they are up to date without needing to wait for polling.
Default polling rate is 2.0s, when each resource is updated or polled, the entire JSON for all resources is sent to all listeners.
The CLI for this application is written using Clap (Command Line Argument Parser), so each command and subcommand can be followed by help to learn more about it.
Aliases are provided for all commands and subcommands, meaning they can be shortened.
bar_daemon g v p ≡ bar_daemon get volume percent
Monitored System Resources
Each monitored resource is programmed using traits so they all implement Monitored, any polled resources implement Polled, and there's also a Notify trait which is implemented by all types, but without a definition of notify() no notification is sent.
The design for acquiring data is modular, such that, multiple sources can be separately implemented, e.g: instead of using wpctl for getting the volume, pactl could be implemented instead.
All the resources' code is split into value.rs and source.rs, so that the acquisition of data and the manipulating of data is done seperately (This means that nothing in value.rs will change when different sources are implemented.)
| Resource | Command Name | Description | Notifies | Polled |
|---|---|---|---|---|
| Volume | wpctl |
Used to get and set the volume and mute state, and get the current icon. Performs a conversion from the internal linear volume to a displayed perceptual (logarithmic) volume. | Yes | No |
| Brightness | brightnessctl |
Used to get and set the brightness of both the monitor and the keyboard, and get the current icon. Device IDs are hardcoded within the source code. | Yes | No |
| Battery | acpi |
Used to get the current battery percentage, charging state, time remaining, and icon. | Yes, at 5%, 15%, 20%, and 30% | Yes |
| Bluetooth | bluetooth( bluetoothctl) |
Used to get and set the bluetooth state, and the current icon. | Yes | No |
| Fan Speed | asusctl |
Used to get and set the current Asus fan profile, and get the fan icon. Can be set as particular profile, or cycled using next or prev. |
Yes | No |
| Memory | free |
Used to get the currently used bytes, total bytes, used percentage of ram, and the icon. | No | Yes |
Usage
| Command | Description |
|---|---|
bar_daemon daemon |
Runs the daemon which will respond to get, set, and listen commands. Outputs errors and logs |
bar_daemon listen |
Listens for changes in resources, and polled resources: receives JSON output of all resources on change. |
bar_daemon get [RESOURCE] [VALUE_TYPE]bar_daemon get volume percentbar_daemon get ram iconbar_daemon get brightness
|
Gets the value of the resource's value_type, or all values for that resource if no value_type is supplied. If no resource is provided, gets all values for all resources. |
bar_daemon set <RESOURCE> <VALUE_TYPE> <VALUE>bar_daemon set volume percent -20bar_daemon set fan profile nextbar_daemon set brightness monitor 50 |
Sets the value of the requested resource. For brightness and volume delta values can be provided. |
Log to Linear
The volume is modified so that each percentage step feels like the same increase in volume, while keeping the range as [0-100]. This fixes the issue wherepipewire has imperceptible volume at low percentages, but unbearably high volume at high percentages.
After trying various exponential values for the conversion between the perceptual volume and linear volume, I found that the best was actually 1⁄2.
#[must_use]
pub fn linear_to_logarithmic(linear_percent: f64) -> f64 {
if linear_percent <= 0.0 {
return 0.0;
}
if linear_percent >= 100.0 {
return 100.0;
}
let normalized = linear_percent / 100.0;
100.0 * (normalized.sqrt())
}
#[must_use]
pub fn logarithmic_to_linear(log_percent: f64) -> f64 {
if log_percent <= 0.0 {
return 0.0;
}
if log_percent >= 100.0 {
return 100.0;
}
let normalized = log_percent / 100.0;
100.0 * normalized.powi(2)
}
Chess Bevy
Chess GUI
A chess GUI written using a custom-built chess library, chess_core, which intended for future use in a chess engine.
The GUI currently uses stockfish as its chess engine, communicating via UCI to get the best move for the black pieces to make.
The engine provides information about moves, and using this, the GUI can classify moves into categories (Best Move, Excellent, Good, Mistake, Blunder).
An evaluation bar is also provided via stockfish and updates after each move.
The GUI, via the chess library, is aware of legal moves, so it prevents moving pieces to illegal positions. Possible moves for each piece are shown when the piece is picked up, and the user can also undo/redo moves since move history is implemented in the chess library. The GUI shows an indicator for the last move which was made, making it clearer what is happening.
Promotion is mostly implemented; currently, pawns can only promote to queens. En passant, and castling are fully implemented, and all moves work with the move history feature, including remembering the state of castling rights and the en passant square.
The board is created using a FEN starting position, and internally represented via bitboards. There are keybinds to show the piece bitboards, the attacked squares for each player, as well as the en passant square.
Chess Core
Board
There is a Board struct which keeps track of the board state, along with the move history.
boards is an array of bitboards for each piece type, meaning that there are twelve u64 values representing the positions.
Each bitboard can be manipulated using bitwise operations to get or set certain positions, as well as more complicated tests like finding if the space between two positions is empty.
struct Board {
boards: [BitBoard; PIECE_AMT * COLOUR_AMT],
en_passant_tile: u64,
castling_rights: [(bool, bool); COLOUR_AMT],
player: Player,
half_move_counter: usize,
full_move_counter: usize,
move_history: PieceMoveHistory,
}
Move History
When adding moves to move_history, there is a check to see if this move matches the next move in the history, and if it is, the history is cleared before adding pushing move; if the move is the same as the current HistoryMove, the index is simply incremented.
fn make_move(
&mut self,
piece_move: PieceMove,
captured_piece: Option<Piece>,
en_passant_tile: Option<TilePos>,
castling_rights: [(bool, bool); COLOUR_AMT],
) {
if piece_move.show {
// Clear history depending on where current_idx is (if the move is different from the history)
if let Some(current_idx) = self.current_idx {
// If the suggested move is different to the current move in history, and is not the last move in the history
if piece_move != self.moves[current_idx].piece_move && current_idx + 1 < self.moves.len() {
self.clear_excess_moves();
}
} else if !self.moves.is_empty() {
self.clear_excess_moves();
}
self.moves
.push(HistoryMove::new(piece_move, captured_piece, en_passant_tile, castling_rights));
let _ = self.increment_index();
}
}
Dotfile Templater
A program which acts on a minimal templating language which can replace parts of the config, based on a provided pattern.
The templating language is written within commented code, on the same line that it will act upon.
For each file which will be acted on, a marker_char is defined within the TOML config.
When marker_char is repeated marker_repetition_num times, the text after this point will be parsed as a list of the templating functions.
To ensure safe replacing, the templater will only replace text with text of the same length, this ensures that only the correct part of the file is modified.
Each function usually acts on the first match for its pattern; if there are multiple consecutive functions on a line, each function acts on its nth match, based on its order relative to the first function on the line.
Config text which matches the provided pattern is replaced by a keyword.
keyword-
A string that will replace the matched pattern.
-
A variable in the TOML config, which holds a string that will replace the pattern.
Function Usages
| Function | Arguments | Description | |
|---|---|---|---|
@replace |
keyword pattern |
Replace first text which matches pattern with the keyword. |
|
@replace‑col |
keyword |
Replace first text which matches #[A‑Za‑z\d]{6} with the keyword. |
|
@replace‑pattern |
pattern_1 keyword pattern_2 |
Find first text which matches pattern_1, then find first match within this match based on pattern_2 and replace that with the keyword. |
|
@replace‑pattern‑col |
keyword pattern_2 |
Find first text which matches #[A‑Za‑z\d]{6}, then find first match within this match based on pattern and replace that with the keyword. |
TOML Config
The config file is located at $XDG_CONFIG_HOME/dotfile-templater/config.toml
marker_repetition_num can be optionally defined to adjust how many marker_char need to be placed before template code, its default value is 3.
Each file in files can be relative to the .config folder, or an absolute path.
Every object in files must have a marker_char defined, so that the templating code can be found.
Themes can be defined in the themes section, every theme must include name, but all other variables can have any name, so long as it is consistent across your themes.
theme = "purple-night"
marker_repetition_num = 3
files = [
{file = "eww/eww.scss", marker_char = "//"},
{file = "eww/bar/bar.yuck", marker_char = ";"},
{file = "wofi/style.css", marker_char = ";"},
{file = "hypr/hyprland.conf", marker_char = "%"},
]
[[themes]]
name = "purple-night"
primary_col = "#9549FF"
secondary_col = "#FF4958"
tertiary_col = "#B3FF49"
quaternary_col = "#49FFF0"
bg_col = "#1A1B26"
bg_col_light = "#24283B"
fg_col = "#A9B1D6"
[[themes]]
name = "blue-bannana"
primary_col = "#48FFD1"
secondary_col = "#4876FF"
tertiary_col = "#FF4876"
quaternary_col = "#FFD148"
bg_col = "#0A0A40"
bg_col_light = "#11116C"
fg_col = "#9999F8"
Test.conf
This test file shows the functions in action within a test configuration file.
test = This wont be configured % Even if there are comments
$primary: rgb(9549FF); %%% @replace-pattern('rgb\([A-Za-z\d]{6}\)', primary_col, '[A-Za-z\d]{6}')
$secondary: #FF4958; %%% @replace-pattern-col(secondary_col, '[A-Za-z\d]{6}')
$tertiary: #B3FF49; #123456; %%% @replace-col(fg_col) @replace-col(bg_col)
$quaternary: #49FFF0; %%% @replace-col(quaternary_col)
$background: #1A1B26; %%% @replace-col(bg_col)
$background-lighter: #24283B; %%% @replace-col(bg_col_light)
$foreground: #A9B1D6; %%% @replace-col(fg_col)
Raymarch Bevy
Using Bevy, a custom shader material is applied to a rectangular texture which is resized to the full screen size whenever the window is changed.
The shader material accounts for the screen width, so textures are not stretched when differernt window sizes are provided, only the aspect ratio changes.
There is a camera controller which can be moved with mouse and keyboard, and it includes moving faster when holding the shift key.
Shape Struct
| Shape Variable | Type | Possible values | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
shape_type |
u32 |
|
Defines the type of shape which will be drawn. Portals are currently not working, but the hope is that the rays will be able to traverse them as if they don't exist in the SDF, and will instead return the distance to the objects on the other side of the portal. | ||||||||||||
pos |
Vec3<f32> |
World-space position of this shape. | The position of this shape (Used to offset the SDF). | ||||||||||||
size |
Vec3<f32> |
Allows positive f32 values. Negative values also sometimes work, but can lead to undefined behaviour. |
The 3D scale of this shape (Used to scale the SDF). |
Shape Material
| Material Variable | Type | Possible Values | Description | ||||||
|---|---|---|---|---|---|---|---|---|---|
shapes |
Vec<Shape> |
A vector with all the shapes which will be drawn in the scene. | |||||||
union_type |
u32 |
|
Defines whether the shapes will be shown by taking the smooth minimum of all shapes, or the regular maximum. | ||||||
smoothness_val |
f32 |
Larger values mean that the shapes will be smoothed together from a greater distance. Negative values cause shapes to become inside out. | Used when the smooth minimum union_type is selected, combines the shapes by smoothing their SDFs. | ||||||
light |
|
A starting position for the scene's light, and its colour in normalised [0-1] rgb values. | Defines the information about the scene's lighting, which will be used in the shader to adjust the colour of scene objects. | ||||||
camera |
|
pos defines the world-space position of the camera.
Zoom defines the field of view.
rotation is a quaternion matrix.
forward, right, and up define the axes of the camera, and are all orthogonal.
|
Provides information about the camera, which is rotated using mouse controls and translated using keyboard controls. The control scheme is a fly camera setup, similar to flying movement in many games. | ||||||
time |
f32 |
Values above 0.0 | The amount of time (in seconds) which has elapsed since the rendering began. | ||||||
shapes_len |
u32 |
Positive Integers | The amount of shapes which will be rendered from the shapes Vec. |
Fragment Shader
The co-ordinates are scaled so that changing the aspect ratio of the window doesn’t stretch the outputted image. The ray-marching loop is completed, and the resulting point which the ray has collided with is calculated. The lighting of the scene at this point is calculated, and the final colour is gamma corrected.
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let coords = centre_and_scale_uv_positions(in.position.xy, view.viewport.zw);
let camera_pos = material.camera.pos;
let ray_dir = get_ray_dir(material.camera, coords);
let get_dist_input = GetDistanceInput(material.union_type, material.smoothness_val, material.time);
let ray_march_out = ray_march(camera_pos, ray_dir, get_dist_input);
let point_on_surface: vec3<f32> = camera_pos + ray_dir * ray_march_out.dist;
let light_strength = get_light(point_on_surface, -ray_dir, material.light.pos, get_dist_input);
var colour: vec3<f32> = ray_march_out.object_colour * material.light.colour * light_strength;
// Gamma correction
let gamma = 2.2;
colour = pow(colour, vec3<f32>(1.0 / gamma));
return vec4<f32>(colour, 1.0);
}
Ray March Loop
The ray checks the distance to the closest object, and marches forward that distance, this continues until either the ray has collided with a shape, or the maximum distance/steps have been reached. The closest distance reached is remembered so that outlines can be given to pixels where the ray has missed the an object by a small amount. If the ray neither collided with a shape nor went sufficiently close to a shape, then a background colour, which is a mix of the sky and bottom sky colours centered at the horizon, is shown.
fn ray_march(ray_origin: vec3<f32>, ray_dir: vec3<f32>, get_dist_input: GetDistanceInput) -> RayMarchOutput {
var ray = Ray(ray_origin, ray_dir);
// Keep track of the minimum distance that the ray reached
var min_dist = max_dist;
var ray_dist = 0.;
var total_ray_dist = ray_dist;
var march_steps = 0;
while(total_ray_dist < max_dist) {
march_steps++;
let dist_output = get_distance(ray.origin, get_dist_input);
var dist = dist_output.dist;
let object_col = dist_output.colour;
let shape_type = dist_output.shape_type;
// Set the minimum distance reached if this distance is smaller
if dist < min_dist {
min_dist = dist;
}
// Exit the loop if we have traversed for too many iterations
if march_steps > max_steps {
break;
}
// Have intersected something
if dist <= epsilon {
// Intersected Portal
if shape_type == 4 {
ray.dir = vec3<f32>(0., 0., -1.);
ray.origin = vec3<f32>(2., 0., 2.) + ray_dir * 0.5;
min_dist = 0.15;
continue;
// return RayMarchOutput(vec3<f32>(0., 0., 1.), 1,0.001);
}
return RayMarchOutput(object_col, ray_dist, min_dist);
}
// Move the ray
ray.origin += ray.dir * dist;
ray_dist += dist;
total_ray_dist += dist;
}
// Draws an outline of shapes where the ray missed by only a small amount
if min_dist < 0.1 {
return RayMarchOutput(vec3<f32>(1., 1., 1.), ray_dist, min_dist);
}
let sky_col = vec3<f32>(0.1, 0.2, 0.7);
let bottom_sky_col = vec3<f32>(0.3, 0.2, 0.5);
let background = mix(bottom_sky_col, sky_col, (1.5 + dot(vec3<f32>(0.,1.,0.), ray.dir)));
return RayMarchOutput(background, ray_dist, min_dist);
}
Sound Themer
A sound theming program which can play sounds from a given sound theme folder. Configured using TOML, allows for different themes to have different internal file structures and sound file extensions, as well as have sounds named differently (the mapping between keywords and sound file names is changed in the config). The intended use case is to provide sounds for system notifications, which differ based on the type of notification.
The CLI for this application is written using Clap (Command Line Argument Parser), so each command and subcommand can be followed by help to learn more about it.
Aliases are provided for all commands, meaning they can be shortened.
sound_themer p audio-change ≡ sound_themer play audio-change ≡ sound_themer play audio-volume-change
Usage
| Command | Description |
|---|---|
sound_themer play <SOUND_NAME>sound_themer p <SOUND_NAME>
|
Plays a sound from the theme folder, mapping from sound_name to its associated value for this theme (if one exists). |
sound_themer listsound_themer ls
|
Lists the sound files in the currently selected theme's folder. |
sound_themer --theme <THEME_NAME> <COMMAND>sound_themer -t <THEME_NAME> <COMMAND> |
Overrides the theme which is selected in config.toml |
Config
The configuration is in TOML, and allows for setting a theme_name, along with defining how each theme works.
Inner directories of the theme folder can be provided, along with the extension for sound files in this theme.
Mapping keyword values to their file name counterparts is also configured here, allowing for using the same commands to play sounds from any theme.
If the provided keyword doesn't exist in the mapping, the program will attempt to play a sound from the theme matching the provided keyword with no mapping.
# Name of the selected sound theme
theme_name = "freedesktop"
[[themes]]
# Name of the sound theme folder
name = "freedesktop"
# Extension on the sound files
sound_ext = "oga"
# Directories where the sounds are found
directories = ["stereo"]
# Provide a mapping between certain phrases and their respective sound file name
mapping = {
audio-change = "audio-volume-change",
login = "service-login",
logout = "service-logout",
message = "message",
power-plug = "power-plug",
power-unplug = "power-unplug",
dialog-info = "dialog-information",
dialog-warning = "dialog-warning",
dialog-error = "dialog-error",
screen-capture = "screen-capture",
device-added = "device-added",
device-removed = "device-removed",
camera-shutter = "camera-shutter",
trash-empty = "trash-empty",
complete = "complete"
}
Phone
I am not comfortable putting my personal phone number on this public site, please use my email, or contact via LinkedIn.Address
Newcastle Upon Tyne, NE5United Kingdom