diff --git a/.gitignore b/.gitignore
index 3b2743f..885013f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,12 @@
-target
 .direnv
 .envrc
-data/db.sqlite
-data/jackets
-data/songs
+
+shimmering/data
+shimmering/logs
+shimmering/assets/fonts
+shimmering/assets/songs
+shimmering/assets/b30_background.*
+
+target
 backups
 dump.sql
-logs
-cache
diff --git a/data/assets/count_background.png b/shimmering/assets/count_background.png
similarity index 100%
rename from data/assets/count_background.png
rename to shimmering/assets/count_background.png
diff --git a/shimmering/assets/diff_byd.png b/shimmering/assets/diff_byd.png
new file mode 100644
index 0000000..539955c
Binary files /dev/null and b/shimmering/assets/diff_byd.png differ
diff --git a/shimmering/assets/diff_etr.png b/shimmering/assets/diff_etr.png
new file mode 100644
index 0000000..0ece003
Binary files /dev/null and b/shimmering/assets/diff_etr.png differ
diff --git a/shimmering/assets/diff_ftr.png b/shimmering/assets/diff_ftr.png
new file mode 100644
index 0000000..4ff6a82
Binary files /dev/null and b/shimmering/assets/diff_ftr.png differ
diff --git a/shimmering/assets/diff_prs.png b/shimmering/assets/diff_prs.png
new file mode 100644
index 0000000..c7d0f1e
Binary files /dev/null and b/shimmering/assets/diff_prs.png differ
diff --git a/shimmering/assets/diff_pst.png b/shimmering/assets/diff_pst.png
new file mode 100644
index 0000000..8c9832f
Binary files /dev/null and b/shimmering/assets/diff_pst.png differ
diff --git a/data/assets/grade_background.png b/shimmering/assets/grade_background.png
similarity index 100%
rename from data/assets/grade_background.png
rename to shimmering/assets/grade_background.png
diff --git a/data/assets/name_background.png b/shimmering/assets/name_background.png
similarity index 100%
rename from data/assets/name_background.png
rename to shimmering/assets/name_background.png
diff --git a/data/assets/placeholder_jacket.jpg b/shimmering/assets/placeholder_jacket.jpg
similarity index 100%
rename from data/assets/placeholder_jacket.jpg
rename to shimmering/assets/placeholder_jacket.jpg
diff --git a/data/assets/ptt_emblem.png b/shimmering/assets/ptt_emblem.png
similarity index 100%
rename from data/assets/ptt_emblem.png
rename to shimmering/assets/ptt_emblem.png
diff --git a/data/assets/score_background.png b/shimmering/assets/score_background.png
similarity index 100%
rename from data/assets/score_background.png
rename to shimmering/assets/score_background.png
diff --git a/data/assets/status_background.png b/shimmering/assets/status_background.png
similarity index 100%
rename from data/assets/status_background.png
rename to shimmering/assets/status_background.png
diff --git a/data/assets/top_background.png b/shimmering/assets/top_background.png
similarity index 100%
rename from data/assets/top_background.png
rename to shimmering/assets/top_background.png
diff --git a/data/charts.csv b/shimmering/config/charts.csv
similarity index 100%
rename from data/charts.csv
rename to shimmering/config/charts.csv
diff --git a/data/shorthands.csv b/shimmering/config/shorthands.csv
similarity index 100%
rename from data/shorthands.csv
rename to shimmering/config/shorthands.csv
diff --git a/data/ui.txt b/shimmering/config/ui.txt
similarity index 100%
rename from data/ui.txt
rename to shimmering/config/ui.txt
diff --git a/src/arcaea/jacket.rs b/src/arcaea/jacket.rs
index 0897cf7..dc91cfd 100644
--- a/src/arcaea/jacket.rs
+++ b/src/arcaea/jacket.rs
@@ -1,11 +1,11 @@
-use std::{fs, io::Cursor, path::PathBuf};
+use std::{fs, io::Cursor};
 
 use image::{imageops::FilterType, GenericImageView, Rgba};
 use num::Integer;
 
 use crate::{
 	arcaea::chart::{Difficulty, Jacket, SongCache},
-	assets::{get_assets_dir, should_blur_jacket_art, should_skip_jacket_art},
+	assets::{get_asset_dir, should_blur_jacket_art, should_skip_jacket_art},
 	context::Error,
 	recognition::fuzzy_song_name::guess_chart_name,
 };
@@ -80,17 +80,9 @@ pub struct JacketCache {
 impl JacketCache {
 	// {{{ Generate
 	// This is a bit inefficient (using a hash set), but only runs once
-	pub fn new(data_dir: &PathBuf, song_cache: &mut SongCache) -> Result<Self, Error> {
-		let jacket_dir = data_dir.join("jackets");
-
-		if jacket_dir.exists() {
-			fs::remove_dir_all(&jacket_dir).expect("Could not delete jacket dir");
-		}
-
-		fs::create_dir_all(&jacket_dir).expect("Could not create jacket dir");
-
+	pub fn new(song_cache: &mut SongCache) -> Result<Self, Error> {
 		let jacket_vectors = if should_skip_jacket_art() {
-			let path = get_assets_dir().join("placeholder_jacket.jpg");
+			let path = get_asset_dir().join("placeholder_jacket.jpg");
 			let contents: &'static _ = fs::read(path)?.leak();
 			let image = image::load_from_memory(contents)?;
 			let bitmap: &'static _ = Box::leak(Box::new(
@@ -109,7 +101,7 @@ impl JacketCache {
 			Vec::new()
 		} else {
 			let entries =
-				fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory");
+				fs::read_dir(get_asset_dir().join("songs")).expect("Couldn't read songs directory");
 			let mut jacket_vectors = vec![];
 
 			for entry in entries {
diff --git a/src/assets.rs b/src/assets.rs
index ccfadb3..7a6c91c 100644
--- a/src/assets.rs
+++ b/src/assets.rs
@@ -1,44 +1,56 @@
-use std::{cell::RefCell, env::var, path::PathBuf, str::FromStr, sync::OnceLock, thread::LocalKey};
+use std::{
+	cell::RefCell,
+	env::var,
+	path::PathBuf,
+	str::FromStr,
+	sync::{LazyLock, OnceLock},
+	thread::LocalKey,
+};
 
 use freetype::{Face, Library};
-use image::{ImageBuffer, Rgb, Rgba};
+use image::{DynamicImage, RgbaImage};
 
 use crate::{arcaea::chart::Difficulty, timed};
 
+// {{{ Path helpers
+#[inline]
+pub fn get_var(name: &str) -> String {
+	var(name).unwrap_or_else(|_| panic!("Missing `{name}` environment variable"))
+}
+
+#[inline]
+pub fn get_path(name: &str) -> PathBuf {
+	PathBuf::from_str(&get_var(name))
+		.unwrap_or_else(|_| panic!("`{name}` environment variable is not a valid path"))
+}
+
 #[inline]
 pub fn get_data_dir() -> PathBuf {
-	PathBuf::from_str(&var("SHIMMERING_DATA_DIR").expect("Missing `SHIMMERING_DATA_DIR` env var"))
-		.expect("`SHIMMERING_DATA_DIR` is not a valid path")
+	get_path("SHIMMERING_DATA_DIR")
 }
 
 #[inline]
-pub fn get_assets_dir() -> PathBuf {
-	get_data_dir().join("assets")
+pub fn get_config_dir() -> PathBuf {
+	get_path("SHIMMERING_CONFIG_DIR")
 }
 
+#[inline]
+pub fn get_asset_dir() -> PathBuf {
+	get_path("SHIMMERING_ASSET_DIR")
+}
+// }}}
+// {{{ Font helpers
 #[inline]
 fn get_font(name: &str) -> RefCell<Face> {
 	let face = timed!(format!("load font \"{name}\""), {
 		FREETYPE_LIB.with(|lib| {
-			lib.new_face(get_assets_dir().join(name), 0)
+			lib.new_face(get_asset_dir().join("fonts").join(name), 0)
 				.expect(&format!("Could not load {} font", name))
 		})
 	});
 	RefCell::new(face)
 }
 
-thread_local! {
-pub static FREETYPE_LIB: Library = Library::init().unwrap();
-pub static SAIRA_FONT: RefCell<Face> = get_font("saira-variable.ttf");
-pub static EXO_FONT: RefCell<Face> = get_font("exo-variable.ttf");
-pub static GEOSANS_FONT: RefCell<Face> = get_font("geosans-light.ttf");
-pub static KAZESAWA_FONT: RefCell<Face> = get_font("kazesawa-regular.ttf");
-pub static KAZESAWA_BOLD_FONT: RefCell<Face> = get_font("kazesawa-bold.ttf");
-pub static NOTO_SANS_FONT: RefCell<Face> = get_font("noto-sans.ttf");
-pub static ARIAL_FONT: RefCell<Face> = get_font("arial.ttf");
-pub static UNI_FONT: RefCell<Face> = get_font("unifont.otf");
-}
-
 #[inline]
 pub fn with_font<T>(
 	primary: &'static LocalKey<RefCell<Face>>,
@@ -52,7 +64,21 @@ pub fn with_font<T>(
 		// })
 	})
 }
-
+// }}}
+// {{{ Font loading
+thread_local! {
+pub static FREETYPE_LIB: Library = Library::init().unwrap();
+pub static SAIRA_FONT: RefCell<Face> = get_font("saira-variable.ttf");
+pub static EXO_FONT: RefCell<Face> = get_font("exo-variable.ttf");
+pub static GEOSANS_FONT: RefCell<Face> = get_font("geosans-light.ttf");
+pub static KAZESAWA_FONT: RefCell<Face> = get_font("kazesawa-regular.ttf");
+pub static KAZESAWA_BOLD_FONT: RefCell<Face> = get_font("kazesawa-bold.ttf");
+pub static NOTO_SANS_FONT: RefCell<Face> = get_font("noto-sans.ttf");
+pub static ARIAL_FONT: RefCell<Face> = get_font("arial.ttf");
+pub static UNI_FONT: RefCell<Face> = get_font("unifont.otf");
+}
+// }}}
+// {{{ Asset art helpers
 #[inline]
 pub fn should_skip_jacket_art() -> bool {
 	var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1"
@@ -63,110 +89,49 @@ pub fn should_blur_jacket_art() -> bool {
 	var("SHIMMERING_BLUR_JACKETS").unwrap_or_default() == "1"
 }
 
-pub fn get_b30_background() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
-	static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
-	CELL.get_or_init(|| {
-		timed!("load_b30_background", {
-			let raw_b30_background = image::open(get_assets_dir().join("b30_background.jpg"))
-				.expect("Could not open b30 background");
-
-			raw_b30_background.blur(7.0).into_rgb8()
-		})
-	})
+macro_rules! get_asset {
+	($name: ident, $path:expr) => {
+		get_asset!($name, $path, |d: DynamicImage| d);
+	};
+	($name: ident, $path:expr, $f:expr) => {
+		pub static $name: LazyLock<RgbaImage> = LazyLock::new(move || {
+			timed!($path, {
+				let image = image::open(get_asset_dir().join($path))
+					.unwrap_or_else(|_| panic!("Could no read asset `{}`", $path));
+				let f = $f;
+				f(image).into_rgba8()
+			})
+		});
+	};
 }
+// }}}
+// {{{ Asset art loading
+get_asset!(COUNT_BACKGROUND, "count_background.png");
+get_asset!(SCORE_BACKGROUND, "score_background.png");
+get_asset!(STATUS_BACKGROUND, "status_background.png");
+get_asset!(GRADE_BACKGROUND, "grade_background.png");
+get_asset!(TOP_BACKGROUND, "top_background.png");
+get_asset!(NAME_BACKGROUND, "name_background.png");
+get_asset!(PTT_EMBLEM, "ptt_emblem.png");
+get_asset!(
+	B30_BACKGROUND,
+	"b30_background.jpg",
+	|image: DynamicImage| image.blur(7.0)
+);
 
-pub fn get_count_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
-	static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
-	CELL.get_or_init(|| {
-		timed!("load_count_backound", {
-			image::open(get_assets_dir().join("count_background.png"))
-				.expect("Could not open count background")
-				.into_rgba8()
-		})
-	})
-}
-
-pub fn get_score_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
-	static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
-	CELL.get_or_init(|| {
-		timed!("load_score_background", {
-			image::open(get_assets_dir().join("score_background.png"))
-				.expect("Could not open score background")
-				.into_rgba8()
-		})
-	})
-}
-
-pub fn get_status_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
-	static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
-	CELL.get_or_init(|| {
-		timed!("load_status_background", {
-			image::open(get_assets_dir().join("status_background.png"))
-				.expect("Could not open status background")
-				.into_rgba8()
-		})
-	})
-}
-
-pub fn get_grade_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
-	static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
-	CELL.get_or_init(|| {
-		timed!("load_grade_background", {
-			image::open(get_assets_dir().join("grade_background.png"))
-				.expect("Could not open grade background")
-				.into_rgba8()
-		})
-	})
-}
-
-pub fn get_top_backgound() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
-	static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
-	CELL.get_or_init(|| {
-		timed!("load_top_background", {
-			image::open(get_assets_dir().join("top_background.png"))
-				.expect("Could not open top background")
-				.into_rgb8()
-		})
-	})
-}
-
-pub fn get_name_backgound() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
-	static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
-	CELL.get_or_init(|| {
-		timed!("load_name_background", {
-			image::open(get_assets_dir().join("name_background.png"))
-				.expect("Could not open name background")
-				.into_rgb8()
-		})
-	})
-}
-
-pub fn get_ptt_emblem() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
-	static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
-	CELL.get_or_init(|| {
-		timed!("load_ptt_emblem", {
-			image::open(get_assets_dir().join("ptt_emblem.png"))
-				.expect("Could not open ptt emblem")
-				.into_rgba8()
-		})
-	})
-}
-
-pub fn get_difficulty_background(
-	difficulty: Difficulty,
-) -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
-	static CELL: OnceLock<[ImageBuffer<Rgba<u8>, Vec<u8>>; 5]> = OnceLock::new();
+pub fn get_difficulty_background(difficulty: Difficulty) -> &'static RgbaImage {
+	static CELL: OnceLock<[RgbaImage; 5]> = OnceLock::new();
 	&CELL.get_or_init(|| {
 		timed!("load_difficulty_background", {
-			let assets_dir = get_assets_dir();
+			let assets_dir = get_asset_dir();
 			Difficulty::DIFFICULTY_SHORTHANDS.map(|shorthand| {
 				image::open(assets_dir.join(format!("diff_{}.png", shorthand.to_lowercase())))
-					.expect(&format!(
-						"Could not get background for difficulty {:?}",
-						shorthand
-					))
+					.unwrap_or_else(|_| {
+						panic!("Could not get background for difficulty {shorthand:?}")
+					})
 					.into_rgba8()
 			})
 		})
 	})[difficulty.to_index()]
 }
+// }}}
diff --git a/src/bitmap.rs b/src/bitmap.rs
index 330026c..b2070cf 100644
--- a/src/bitmap.rs
+++ b/src/bitmap.rs
@@ -12,7 +12,7 @@ use freetype::{
 	ffi::{FT_Set_Var_Design_Coordinates, FT_GLYPH_BBOX_PIXELS},
 	Bitmap, BitmapGlyph, Face, Glyph, StrokerLineCap, StrokerLineJoin,
 };
-use image::GenericImage;
+use image::{GenericImage, RgbImage, RgbaImage};
 use num::traits::Euclid;
 
 use crate::{assets::FREETYPE_LIB, context::Error};
@@ -184,7 +184,8 @@ impl BitmapCanvas {
 			((alpha * color.2 as u32 + (255 - alpha) * self.buffer[index + 2] as u32) / 255) as u8;
 	}
 	// }}}
-	// {{{ Draw RBG image
+	// {{{ Draw RGB image
+	/// Draws a bitmap image with no alpha channel.
 	pub fn blit_rbg(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) {
 		let iw = iw as i32;
 		let ih = ih as i32;
@@ -242,8 +243,8 @@ impl BitmapCanvas {
 		}
 	}
 	// }}}
-	// {{{ Draw scaled up RBG image
-	pub fn blit_rbg_scaled_up(
+	// {{{ Draw scaled up RBGA image
+	pub fn blit_rbga_scaled_up(
 		&mut self,
 		pos: Position,
 		(iw, ih): (u32, u32),
@@ -269,11 +270,12 @@ impl BitmapCanvas {
 				// but would not perform division.
 				let dx = (x - pos.0) / scale;
 				let dy = (y - pos.1) / scale;
-				let r = src[(dx + dy * iw) as usize * 3];
-				let g = src[(dx + dy * iw) as usize * 3 + 1];
-				let b = src[(dx + dy * iw) as usize * 3 + 2];
+				let r = src[(dx + dy * iw) as usize * 4];
+				let g = src[(dx + dy * iw) as usize * 4 + 1];
+				let b = src[(dx + dy * iw) as usize * 4 + 2];
+				let a = src[(dx + dy * iw) as usize * 4 + 3];
 
-				let color = Color(r, g, b, 0xff);
+				let color = Color(r, g, b, a);
 
 				self.set_pixel((x as u32, y as u32), color);
 			}
@@ -712,11 +714,21 @@ impl LayoutDrawer {
 		self.canvas.set_pixel((pos.0 as u32, pos.1 as u32), color);
 	}
 	// }}}
-	// {{{ Draw RGB image
+	// {{{ Draw images
+	/// Draws a bitmap image taking with no alpha channel.
 	#[inline]
-	pub fn blit_rbg(&mut self, id: LayoutBoxId, pos: Position, dims: (u32, u32), src: &[u8]) {
+	pub fn blit_rbg(&mut self, id: LayoutBoxId, pos: Position, image: &RgbImage) {
 		let pos = self.layout.position_relative_to(id, pos);
-		self.canvas.blit_rbg(pos, dims, src);
+		self.canvas
+			.blit_rbg(pos, image.dimensions(), image.as_raw());
+	}
+
+	/// Draws a bitmap image taking care of the alpha channel.
+	#[inline]
+	pub fn blit_rbga(&mut self, id: LayoutBoxId, pos: Position, image: &RgbaImage) {
+		let pos = self.layout.position_relative_to(id, pos);
+		self.canvas
+			.blit_rbga(pos, image.dimensions(), image.as_raw());
 	}
 
 	#[inline]
@@ -729,15 +741,7 @@ impl LayoutDrawer {
 		scale: u32,
 	) {
 		let pos = self.layout.position_relative_to(id, pos);
-		self.canvas.blit_rbg_scaled_up(pos, dims, src, scale);
-	}
-	// }}}
-	// {{{ Draw RGBA image
-	/// Draws a bitmap image taking care of the alpha channel.
-	#[inline]
-	pub fn blit_rbga(&mut self, id: LayoutBoxId, pos: Position, dims: (u32, u32), src: &[u8]) {
-		let pos = self.layout.position_relative_to(id, pos);
-		self.canvas.blit_rbga(pos, dims, src);
+		self.canvas.blit_rbga_scaled_up(pos, dims, src, scale);
 	}
 	// }}}
 	// {{{ Fill
diff --git a/src/commands/stats.rs b/src/commands/stats.rs
index 122f55a..09e9c6e 100644
--- a/src/commands/stats.rs
+++ b/src/commands/stats.rs
@@ -24,9 +24,9 @@ use crate::{
 	},
 	assert_is_pookie,
 	assets::{
-		get_b30_background, get_count_background, get_difficulty_background, get_grade_background,
-		get_name_backgound, get_ptt_emblem, get_score_background, get_status_background,
-		get_top_backgound, with_font, EXO_FONT,
+		get_difficulty_background, with_font, B30_BACKGROUND, COUNT_BACKGROUND, EXO_FONT,
+		GRADE_BACKGROUND, NAME_BACKGROUND, PTT_EMBLEM, SCORE_BACKGROUND, STATUS_BACKGROUND,
+		TOP_BACKGROUND,
 	},
 	bitmap::{Align, BitmapCanvas, Color, LayoutDrawer, LayoutManager, Rect},
 	context::{Context, Error},
@@ -300,7 +300,7 @@ async fn best_plays(
 	let mut drawer = LayoutDrawer::new(layout, canvas);
 	// }}}
 	// {{{ Render background
-	let bg = get_b30_background();
+	let bg = &*B30_BACKGROUND;
 
 	let scale = (drawer.layout.width(root) as f32 / bg.width() as f32)
 		.max(drawer.layout.height(root) as f32 / bg.height() as f32)
@@ -325,8 +325,8 @@ async fn best_plays(
 			.layout
 			.edit_to_relative(item_with_margin, item_grid, origin.0, origin.1);
 
-		let top_bg = get_top_backgound();
-		drawer.blit_rbg(top_area, (0, 0), top_bg.dimensions(), top_bg);
+		let top_bg = &*TOP_BACKGROUND;
+		drawer.blit_rbga(top_area, (0, 0), top_bg);
 
 		let (play, song, chart) = if let Some(item) = plays.get(i) {
 			item
@@ -335,11 +335,11 @@ async fn best_plays(
 		};
 
 		// {{{ Display index
-		let bg = get_count_background();
+		let bg = &*COUNT_BACKGROUND;
 		let bg_center = Rect::from_image(bg).center();
 
 		// Draw background
-		drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg.dimensions(), bg);
+		drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg);
 		with_font(&EXO_FONT, |faces| {
 			drawer.text(
 				item_area,
@@ -359,8 +359,8 @@ async fn best_plays(
 		// }}}
 		// {{{ Display chart name
 		// Draw background
-		let bg = get_name_backgound();
-		drawer.blit_rbg(bottom_area, (0, 0), bg.dimensions(), bg.as_raw());
+		let bg = &*NAME_BACKGROUND;
+		drawer.blit_rbga(bottom_area, (0, 0), bg);
 
 		// Draw text
 		with_font(&EXO_FONT, |faces| {
@@ -403,12 +403,7 @@ async fn best_plays(
 		})?;
 
 		drawer.fill(jacket_with_border, Color::from_rgb_int(0x271E35));
-		drawer.blit_rbg(
-			jacket_area,
-			(0, 0),
-			jacket.bitmap.dimensions(),
-			&jacket.bitmap.as_raw(),
-		);
+		drawer.blit_rbg(jacket_area, (0, 0), jacket.bitmap);
 		// }}}
 		// {{{ Display difficulty background
 		let diff_bg = get_difficulty_background(chart.difficulty);
@@ -417,12 +412,7 @@ async fn best_plays(
 			(drawer.layout.width(jacket_with_border) as i32, 0),
 		);
 
-		drawer.blit_rbga(
-			jacket_with_border,
-			diff_bg_area.top_left(),
-			diff_bg.dimensions(),
-			&diff_bg.as_raw(),
-		);
+		drawer.blit_rbga(jacket_with_border, diff_bg_area.top_left(), diff_bg);
 		// }}}
 		// {{{ Display difficulty text
 		let x_offset = if chart.level.ends_with("+") {
@@ -453,7 +443,7 @@ async fn best_plays(
 		})?;
 		// }}}
 		// {{{ Display score background
-		let score_bg = get_score_background();
+		let score_bg = &*SCORE_BACKGROUND;
 		let score_bg_pos = Rect::from_image(score_bg).align(
 			(Align::End, Align::End),
 			(
@@ -462,12 +452,7 @@ async fn best_plays(
 			),
 		);
 
-		drawer.blit_rbga(
-			jacket_area,
-			score_bg_pos,
-			score_bg.dimensions(),
-			&score_bg.as_raw(),
-		);
+		drawer.blit_rbga(jacket_area, score_bg_pos, score_bg);
 		// }}}
 		// {{{ Display score text
 		with_font(&EXO_FONT, |faces| {
@@ -491,7 +476,7 @@ async fn best_plays(
 		})?;
 		// }}}
 		// {{{ Display status background
-		let status_bg = get_status_background();
+		let status_bg = &*STATUS_BACKGROUND;
 		let status_bg_area = Rect::from_image(status_bg).align_whole(
 			(Align::Center, Align::Center),
 			(
@@ -500,12 +485,7 @@ async fn best_plays(
 			),
 		);
 
-		drawer.blit_rbga(
-			jacket_area,
-			status_bg_area.top_left(),
-			status_bg.dimensions(),
-			&status_bg.as_raw(),
-		);
+		drawer.blit_rbga(jacket_area, status_bg_area.top_left(), status_bg);
 		// }}}
 		// {{{ Display status text
 		with_font(&EXO_FONT, |faces| {
@@ -543,18 +523,13 @@ async fn best_plays(
 		// }}}
 		// {{{ Display grade background
 		let top_left_center = (drawer.layout.width(top_left_area) as i32 + jacket_margin) / 2;
-		let grade_bg = get_grade_background();
+		let grade_bg = &*GRADE_BACKGROUND;
 		let grade_bg_area = Rect::from_image(grade_bg).align_whole(
 			(Align::Center, Align::Center),
 			(top_left_center, jacket_margin + 140),
 		);
 
-		drawer.blit_rbga(
-			top_area,
-			grade_bg_area.top_left(),
-			grade_bg.dimensions(),
-			&grade_bg.as_raw(),
-		);
+		drawer.blit_rbga(top_area, grade_bg_area.top_left(), grade_bg);
 		// }}}
 		// {{{ Display grade text
 		with_font(&EXO_FONT, |faces| {
@@ -614,13 +589,12 @@ async fn best_plays(
 		})?;
 		// }}}
 		// {{{ Display ptt emblem
-		let ptt_emblem = get_ptt_emblem();
+		let ptt_emblem = &*PTT_EMBLEM;
 		drawer.blit_rbga(
 			top_left_area,
 			Rect::from_image(ptt_emblem)
 				.align((Align::Center, Align::Center), (top_left_center, 115)),
-			ptt_emblem.dimensions(),
-			ptt_emblem.as_raw(),
+			ptt_emblem,
 		);
 		// }}}
 	}
diff --git a/src/context.rs b/src/context.rs
index d836d32..6089aab 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -1,10 +1,10 @@
-use std::{fs, path::PathBuf};
+use std::fs;
 
 use sqlx::SqlitePool;
 
 use crate::{
 	arcaea::{chart::SongCache, jacket::JacketCache},
-	assets::{EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT},
+	assets::{get_data_dir, EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT},
 	recognition::{hyperglass::CharMeasurements, ui::UIMeasurements},
 };
 
@@ -14,9 +14,6 @@ pub type Context<'a> = poise::Context<'a, UserContext, Error>;
 
 // Custom user data passed to all command functions
 pub struct UserContext {
-	#[allow(dead_code)]
-	pub data_dir: PathBuf,
-
 	pub db: SqlitePool,
 	pub song_cache: SongCache,
 	pub jacket_cache: JacketCache,
@@ -31,14 +28,14 @@ pub struct UserContext {
 
 impl UserContext {
 	#[inline]
-	pub async fn new(data_dir: PathBuf, cache_dir: PathBuf, db: SqlitePool) -> Result<Self, Error> {
-		fs::create_dir_all(&cache_dir)?;
-		fs::create_dir_all(&data_dir)?;
+	pub async fn new(db: SqlitePool) -> Result<Self, Error> {
+		fs::create_dir_all(get_data_dir())?;
 
 		let mut song_cache = SongCache::new(&db).await?;
-		let jacket_cache = JacketCache::new(&data_dir, &mut song_cache)?;
-		let ui_measurements = UIMeasurements::read(&data_dir)?;
+		let jacket_cache = JacketCache::new(&mut song_cache)?;
+		let ui_measurements = UIMeasurements::read()?;
 
+		// {{{ Font measurements
 		static WHITELIST: &str = "0123456789'abcdefghklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ";
 
 		let geosans_measurements = GEOSANS_FONT
@@ -49,11 +46,11 @@ impl UserContext {
 			.with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, None))?;
 		let exo_measurements = EXO_FONT
 			.with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, Some(700)))?;
+		// }}}
 
 		println!("Created user context");
 
 		Ok(Self {
-			data_dir,
 			db,
 			song_cache,
 			jacket_cache,
diff --git a/src/logs.rs b/src/logs.rs
index a278ff1..745a6ba 100644
--- a/src/logs.rs
+++ b/src/logs.rs
@@ -6,11 +6,11 @@
 //! allows for a convenient way to throw images into a `logs` directory with
 //! a simple env var.
 
-use std::{env, ops::Deref, sync::OnceLock, time::Instant};
+use std::{env, ops::Deref, path::PathBuf, sync::OnceLock, time::Instant};
 
 use image::{DynamicImage, EncodableLayout, ImageBuffer, PixelWithColorType};
 
-use crate::context::Error;
+use crate::{assets::get_path, context::Error};
 
 #[inline]
 fn should_save_debug_images() -> bool {
@@ -19,6 +19,11 @@ fn should_save_debug_images() -> bool {
 		.unwrap_or(false)
 }
 
+#[inline]
+fn get_log_dir() -> PathBuf {
+	get_path("SHIMMERING_LOG_DIR")
+}
+
 #[inline]
 fn get_startup_time() -> Instant {
 	static CELL: OnceLock<Instant> = OnceLock::new();
@@ -28,10 +33,10 @@ fn get_startup_time() -> Instant {
 #[inline]
 pub fn debug_image_log(image: &DynamicImage) -> Result<(), Error> {
 	if should_save_debug_images() {
-		image.save(format!(
-			"./logs/{:0>15}.png",
+		image.save(get_log_dir().join(format!(
+			"{:0>15}.png",
 			get_startup_time().elapsed().as_nanos()
-		))?;
+		)))?;
 	}
 
 	Ok(())
@@ -45,10 +50,10 @@ where
 	C: Deref<Target = [P::Subpixel]>,
 {
 	if should_save_debug_images() {
-		image.save(format!(
+		image.save(get_log_dir().join(format!(
 			"./logs/{:0>15}.png",
 			get_startup_time().elapsed().as_nanos()
-		))?;
+		)))?;
 	}
 
 	Ok(())
diff --git a/src/main.rs b/src/main.rs
index 9e6acaa..445882d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,6 +4,7 @@
 #![feature(array_try_map)]
 #![feature(async_closure)]
 #![feature(try_blocks)]
+#![feature(thread_local)]
 
 mod arcaea;
 mod assets;
@@ -21,7 +22,7 @@ use assets::get_data_dir;
 use context::{Error, UserContext};
 use poise::serenity_prelude::{self as serenity};
 use sqlx::sqlite::SqlitePoolOptions;
-use std::{env::var, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
+use std::{env::var, sync::Arc, time::Duration};
 
 // {{{ Error handler
 async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) {
@@ -37,13 +38,10 @@ async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) {
 
 #[tokio::main]
 async fn main() {
-	let data_dir = get_data_dir();
-	let cache_dir = var("SHIMMERING_CACHE_DIR").expect("Missing `SHIMMERING_CACHE_DIR` env var");
-
 	let pool = SqlitePoolOptions::new()
 		.connect(&format!(
 			"sqlite://{}/db.sqlite",
-			data_dir.to_str().unwrap()
+			get_data_dir().to_str().unwrap()
 		))
 		.await
 		.unwrap();
@@ -89,7 +87,7 @@ async fn main() {
 			Box::pin(async move {
 				println!("Logged in as {}", _ready.user.name);
 				poise::builtins::register_globally(ctx, &framework.options().commands).await?;
-				let ctx = UserContext::new(data_dir, PathBuf::from_str(&cache_dir)?, pool).await?;
+				let ctx = UserContext::new(pool).await?;
 
 				Ok(ctx)
 			})
diff --git a/src/recognition/ui.rs b/src/recognition/ui.rs
index 2c6061a..8886820 100644
--- a/src/recognition/ui.rs
+++ b/src/recognition/ui.rs
@@ -1,8 +1,8 @@
-use std::{fs, path::PathBuf};
+use std::fs;
 
 use image::GenericImage;
 
-use crate::{bitmap::Rect, context::Error};
+use crate::{assets::get_config_dir, bitmap::Rect, context::Error};
 
 // {{{ Rects
 #[derive(Debug, Clone, Copy)]
@@ -94,11 +94,11 @@ pub struct UIMeasurements {
 
 impl UIMeasurements {
 	// {{{ Read
-	pub fn read(data_dir: &PathBuf) -> Result<Self, Error> {
+	pub fn read() -> Result<Self, Error> {
 		let mut measurements = Vec::new();
 		let mut measurement = UIMeasurement::default();
 
-		let path = data_dir.join("ui.txt");
+		let path = get_config_dir().join("ui.txt");
 		let contents = fs::read_to_string(path)?;
 
 		// {{{ Parse measurement file