diff --git a/userspace/Cargo.lock b/userspace/Cargo.lock
index 4191557a..44ac7000 100644
--- a/userspace/Cargo.lock
+++ b/userspace/Cargo.lock
@@ -2,6 +2,22 @@
 # It is not intended for manual editing.
 version = 4
 
+[[package]]
+name = "ab_glyph"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0"
+dependencies = [
+ "ab_glyph_rasterizer",
+ "owned_ttf_parser",
+]
+
+[[package]]
+name = "ab_glyph_rasterizer"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
+
 [[package]]
 name = "abi-generator"
 version = "0.1.0"
@@ -31,6 +47,19 @@ dependencies = [
  "cpufeatures",
 ]
 
+[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.2.15",
+ "once_cell",
+ "version_check",
+ "zerocopy 0.7.35",
+]
+
 [[package]]
 name = "aho-corasick"
 version = "1.1.3"
@@ -41,10 +70,37 @@ dependencies = [
 ]
 
 [[package]]
-name = "anstream"
-version = "0.6.17"
+name = "android-activity"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338"
+checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
+dependencies = [
+ "android-properties",
+ "bitflags 2.8.0",
+ "cc",
+ "cesu8",
+ "jni",
+ "jni-sys",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "num_enum",
+ "thiserror",
+]
+
+[[package]]
+name = "android-properties"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
+
+[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -57,9 +113,9 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.9"
+version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
 
 [[package]]
 name = "anstyle-parse"
@@ -81,20 +137,33 @@ dependencies = [
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.6"
+version = "3.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
+checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
 dependencies = [
  "anstyle",
+ "once_cell",
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "arrayref"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+
 [[package]]
 name = "arrayvec"
 version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
 
+[[package]]
+name = "as-raw-xcb-connection"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
+
 [[package]]
 name = "atomic-polyfill"
 version = "1.0.3"
@@ -104,6 +173,12 @@ dependencies = [
  "critical-section",
 ]
 
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
 [[package]]
 name = "autocfg"
 version = "1.4.0"
@@ -118,9 +193,15 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
 [[package]]
 name = "bitflags"
-version = "2.6.0"
+version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
 
 [[package]]
 name = "block-buffer"
@@ -132,19 +213,34 @@ dependencies = [
 ]
 
 [[package]]
-name = "bytemuck"
-version = "1.19.0"
+name = "block2"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+
+[[package]]
+name = "bytemuck"
+version = "1.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
 dependencies = [
  "bytemuck_derive",
 ]
 
 [[package]]
 name = "bytemuck_derive"
-version = "1.8.0"
+version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec"
+checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -159,19 +255,53 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
 name = "bytes"
-version = "1.8.0"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
+checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
+
+[[package]]
+name = "calloop"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
+dependencies = [
+ "bitflags 2.8.0",
+ "log",
+ "polling",
+ "rustix",
+ "slab",
+ "thiserror",
+]
+
+[[package]]
+name = "calloop-wayland-source"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
+dependencies = [
+ "calloop",
+ "rustix",
+ "wayland-backend",
+ "wayland-client",
+]
 
 [[package]]
 name = "cc"
-version = "1.1.31"
+version = "1.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
+checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9"
 dependencies = [
+ "jobserver",
+ "libc",
  "shlex",
 ]
 
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -179,10 +309,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
-name = "chrono"
-version = "0.4.38"
+name = "cfg_aliases"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
 dependencies = [
  "num-traits",
 ]
@@ -199,9 +335,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.20"
+version = "4.5.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
+checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -209,18 +345,18 @@ dependencies = [
 
 [[package]]
 name = "clap-num"
-version = "1.1.1"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e063d263364859dc54fb064cedb7c122740cd4733644b14b176c097f51e8ab7"
+checksum = "822c4000301ac390e65995c62207501e3ef800a1fc441df913a5e8e4dc374816"
 dependencies = [
  "num-traits",
 ]
 
 [[package]]
 name = "clap_builder"
-version = "4.5.20"
+version = "4.5.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
+checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
 dependencies = [
  "anstyle",
  "clap_lex",
@@ -228,9 +364,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.18"
+version = "4.5.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
+checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -240,9 +376,9 @@ dependencies = [
 
 [[package]]
 name = "clap_lex"
-version = "0.7.2"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
 
 [[package]]
 name = "cobs"
@@ -260,15 +396,39 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 name = "colors"
 version = "0.1.0"
 dependencies = [
+ "clap",
  "cross",
  "libcolors",
+ "log",
+ "logsink",
  "runtime",
  "serde",
+ "softbuffer",
  "thiserror",
  "uipc",
+ "winit",
  "yggdrasil-abi",
 ]
 
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "const-oid"
 version = "0.9.6"
@@ -276,10 +436,84 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
 
 [[package]]
-name = "cpufeatures"
-version = "0.2.14"
+name = "core-foundation"
+version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation 0.9.4",
+ "core-graphics-types 0.1.3",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
+dependencies = [
+ "bitflags 2.8.0",
+ "core-foundation 0.10.0",
+ "core-graphics-types 0.2.0",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation 0.9.4",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
+dependencies = [
+ "bitflags 2.8.0",
+ "core-foundation 0.10.0",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
 dependencies = [
  "libc",
 ]
@@ -300,13 +534,19 @@ dependencies = [
  "yggdrasil-rt",
 ]
 
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
 [[package]]
 name = "crossterm"
 version = "0.27.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
 dependencies = [
- "bitflags",
+ "bitflags 2.8.0",
  "crossterm_winapi",
  "libc",
  "mio",
@@ -346,6 +586,18 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "ctor-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b"
+
+[[package]]
+name = "cursor-icon"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
+
 [[package]]
 name = "curve25519-dalek"
 version = "4.1.3"
@@ -402,6 +654,83 @@ dependencies = [
  "crypto-common",
 ]
 
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dlib"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "dpi"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53"
+
+[[package]]
+name = "drm"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1"
+dependencies = [
+ "bitflags 2.8.0",
+ "bytemuck",
+ "drm-ffi",
+ "drm-fourcc",
+ "rustix",
+]
+
+[[package]]
+name = "drm-ffi"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53"
+dependencies = [
+ "drm-sys",
+ "rustix",
+]
+
+[[package]]
+name = "drm-fourcc"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4"
+
+[[package]]
+name = "drm-sys"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986"
+dependencies = [
+ "libc",
+ "linux-raw-sys 0.6.5",
+]
+
 [[package]]
 name = "dyn-loader"
 version = "0.1.0"
@@ -458,9 +787,9 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
 
 [[package]]
 name = "env_filter"
-version = "0.1.2"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
+checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
 dependencies = [
  "log",
  "regex",
@@ -468,9 +797,9 @@ dependencies = [
 
 [[package]]
 name = "env_logger"
-version = "0.11.5"
+version = "0.11.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
+checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
 dependencies = [
  "anstream",
  "anstyle",
@@ -479,6 +808,12 @@ dependencies = [
  "log",
 ]
 
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
 [[package]]
 name = "errno"
 version = "0.3.10"
@@ -525,6 +860,33 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
 [[package]]
 name = "form_urlencoded"
 version = "1.2.1"
@@ -581,6 +943,16 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "gethostname"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
+dependencies = [
+ "libc",
+ "windows-targets 0.48.5",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.12"
@@ -588,7 +960,7 @@ source = "git+https://git.alnyan.me/yggdrasil/getrandom.git?branch=alnyan%2Fyggd
 dependencies = [
  "cfg-if",
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
 ]
 
 [[package]]
@@ -599,7 +971,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
 dependencies = [
  "cfg-if",
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.13.3+wasi-0.2.2",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -611,6 +995,12 @@ dependencies = [
  "byteorder",
 ]
 
+[[package]]
+name = "hashbrown"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+
 [[package]]
 name = "heapless"
 version = "0.7.17"
@@ -631,6 +1021,12 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "hermit-abi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+
 [[package]]
 name = "hostname"
 version = "0.3.1"
@@ -644,9 +1040,9 @@ dependencies = [
 
 [[package]]
 name = "http"
-version = "1.1.0"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
 dependencies = [
  "bytes",
  "fnv",
@@ -678,13 +1074,152 @@ dependencies = [
 ]
 
 [[package]]
-name = "idna"
-version = "0.5.0"
+name = "icu_collections"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
 dependencies = [
- "unicode-bidi",
- "unicode-normalization",
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_locid_transform_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
+
+[[package]]
+name = "icu_normalizer"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "utf16_iter",
+ "utf8_iter",
+ "write16",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
+
+[[package]]
+name = "icu_properties"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locid_transform",
+ "icu_properties_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
+
+[[package]]
+name = "icu_provider"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_provider_macros",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_provider_macros"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
+dependencies = [
+ "equivalent",
+ "hashbrown",
 ]
 
 [[package]]
@@ -714,9 +1249,50 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 
 [[package]]
 name = "itoa"
-version = "1.0.11"
+version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "jobserver"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
 
 [[package]]
 name = "lazy_static"
@@ -729,15 +1305,16 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.161"
+version = "0.2.169"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
+checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
 
 [[package]]
 name = "libcolors"
 version = "0.1.0"
 dependencies = [
  "cross",
+ "log",
  "raqote",
  "serde",
  "thiserror",
@@ -745,12 +1322,33 @@ dependencies = [
  "yggdrasil-abi",
 ]
 
+[[package]]
+name = "libloading"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "libm"
 version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
 
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.8.0",
+ "libc",
+ "redox_syscall 0.5.8",
+]
+
 [[package]]
 name = "libterm"
 version = "0.1.0"
@@ -766,6 +1364,18 @@ version = "0.4.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7"
+
+[[package]]
+name = "litemap"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
+
 [[package]]
 name = "lock_api"
 version = "0.4.12"
@@ -778,9 +1388,17 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.22"
+version = "0.4.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
+
+[[package]]
+name = "logsink"
+version = "0.1.0"
+dependencies = [
+ "env_logger",
+ "log",
+]
 
 [[package]]
 name = "lyon_geom"
@@ -805,6 +1423,15 @@ version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
+[[package]]
+name = "memmap2"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "minimal-lexical"
 version = "0.2.1"
@@ -819,10 +1446,40 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
 dependencies = [
  "libc",
  "log",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.8.0",
+ "jni-sys",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys",
+]
+
 [[package]]
 name = "netutils"
 version = "0.1.0"
@@ -903,6 +1560,27 @@ dependencies = [
  "libm",
 ]
 
+[[package]]
+name = "num_enum"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "num_threads"
 version = "0.1.7"
@@ -913,10 +1591,231 @@ dependencies = [
 ]
 
 [[package]]
-name = "once_cell"
-version = "1.20.2"
+name = "objc-sys"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags 2.8.0",
+ "block2",
+ "libc",
+ "objc2",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
+dependencies = [
+ "bitflags 2.8.0",
+ "block2",
+ "objc2",
+ "objc2-core-location",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-contacts"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+dependencies = [
+ "bitflags 2.8.0",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-contacts",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.8.0",
+ "block2",
+ "dispatch",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-link-presentation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags 2.8.0",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags 2.8.0",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-symbols"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
+dependencies = [
+ "bitflags 2.8.0",
+ "block2",
+ "objc2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-foundation",
+ "objc2-link-presentation",
+ "objc2-quartz-core",
+ "objc2-symbols",
+ "objc2-uniform-type-identifiers",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-uniform-type-identifiers"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
+dependencies = [
+ "bitflags 2.8.0",
+ "block2",
+ "objc2",
+ "objc2-core-location",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
+
+[[package]]
+name = "orbclient"
+version = "0.3.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43"
+dependencies = [
+ "libredox",
+]
+
+[[package]]
+name = "owned_ttf_parser"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4"
+dependencies = [
+ "ttf-parser",
+]
 
 [[package]]
 name = "parking_lot"
@@ -936,7 +1835,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
 dependencies = [
  "cfg-if",
  "libc",
- "redox_syscall",
+ "redox_syscall 0.5.8",
  "smallvec",
  "windows-targets 0.52.6",
 ]
@@ -1008,10 +1907,30 @@ dependencies = [
 ]
 
 [[package]]
-name = "pin-project-lite"
-version = "0.2.15"
+name = "pin-project"
+version = "1.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
+checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
 
 [[package]]
 name = "pin-utils"
@@ -1040,6 +1959,27 @@ dependencies = [
  "spki",
 ]
 
+[[package]]
+name = "pkg-config"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
+
+[[package]]
+name = "polling"
+version = "3.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "postcard"
 version = "1.1.1"
@@ -1070,14 +2010,23 @@ dependencies = [
 
 [[package]]
 name = "prettyplease"
-version = "0.2.25"
+version = "0.2.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033"
+checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
 dependencies = [
  "proc-macro2",
  "syn",
 ]
 
+[[package]]
+name = "proc-macro-crate"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
+dependencies = [
+ "toml_edit",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.93"
@@ -1088,10 +2037,19 @@ dependencies = [
 ]
 
 [[package]]
-name = "quote"
-version = "1.0.37"
+name = "quick-xml"
+version = "0.37.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
 dependencies = [
  "proc-macro2",
 ]
@@ -1122,7 +2080,7 @@ source = "git+https://git.alnyan.me/yggdrasil/rand.git?branch=alnyan%2Fyggdrasil
 dependencies = [
  "rand_chacha 0.9.0-alpha.1",
  "rand_core 0.9.0-alpha.1",
- "zerocopy 0.8.8",
+ "zerocopy 0.8.18",
 ]
 
 [[package]]
@@ -1166,7 +2124,7 @@ version = "0.9.0-alpha.1"
 source = "git+https://git.alnyan.me/yggdrasil/rand.git?branch=alnyan%2Fyggdrasil#d78efe056f23f1896b0e4f03a62d4b6adc37ea07"
 dependencies = [
  "getrandom 0.2.12",
- "zerocopy 0.8.8",
+ "zerocopy 0.8.18",
 ]
 
 [[package]]
@@ -1187,6 +2145,12 @@ dependencies = [
  "typed-arena",
 ]
 
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
 [[package]]
 name = "rdb"
 version = "0.1.0"
@@ -1218,11 +2182,20 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.7"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
+dependencies = [
+ "bitflags 2.8.0",
 ]
 
 [[package]]
@@ -1239,9 +2212,9 @@ dependencies = [
 
 [[package]]
 name = "regex-automata"
-version = "0.4.8"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1317,22 +2290,43 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "0.38.43"
+version = "0.38.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
 dependencies = [
- "bitflags",
+ "bitflags 2.8.0",
  "errno",
  "libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.15",
  "windows-sys 0.59.0",
 ]
 
 [[package]]
-name = "ryu"
-version = "1.0.18"
+name = "rustversion"
+version = "1.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
+
+[[package]]
+name = "ryu"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
 
 [[package]]
 name = "scopeguard"
@@ -1341,25 +2335,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
-name = "semver"
-version = "1.0.23"
+name = "sctk-adwaita"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
+checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec"
+dependencies = [
+ "ab_glyph",
+ "log",
+ "memmap2",
+ "smithay-client-toolkit",
+ "tiny-skia",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
 
 [[package]]
 name = "serde"
-version = "1.0.214"
+version = "1.0.217"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
+checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.214"
+version = "1.0.217"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
+checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1368,9 +2375,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.132"
+version = "1.0.138"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
+checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
 dependencies = [
  "itoa",
  "memchr",
@@ -1471,9 +2478,75 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.13.2"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
+
+[[package]]
+name = "smithay-client-toolkit"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
+dependencies = [
+ "bitflags 2.8.0",
+ "calloop",
+ "calloop-wayland-source",
+ "cursor-icon",
+ "libc",
+ "log",
+ "memmap2",
+ "rustix",
+ "thiserror",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-csd-frame",
+ "wayland-cursor",
+ "wayland-protocols",
+ "wayland-protocols-wlr",
+ "wayland-scanner",
+ "xkeysym",
+]
+
+[[package]]
+name = "smol_str"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "softbuffer"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
+dependencies = [
+ "as-raw-xcb-connection",
+ "bytemuck",
+ "cfg_aliases",
+ "core-graphics 0.24.0",
+ "drm",
+ "fastrand",
+ "foreign-types",
+ "js-sys",
+ "log",
+ "memmap2",
+ "objc2",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "raw-window-handle",
+ "redox_syscall 0.5.8",
+ "rustix",
+ "tiny-xlib",
+ "wasm-bindgen",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-sys",
+ "web-sys",
+ "windows-sys 0.59.0",
+ "x11rb",
+]
 
 [[package]]
 name = "spin"
@@ -1506,6 +2579,12 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
+[[package]]
+name = "strict-num"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
+
 [[package]]
 name = "subtle"
 version = "2.6.1"
@@ -1520,15 +2599,26 @@ checksum = "9ac8fb7895b4afa060ad731a32860db8755da3449a47e796d5ecf758db2671d4"
 
 [[package]]
 name = "syn"
-version = "2.0.96"
+version = "2.0.98"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
+checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
 dependencies = [
  "proc-macro2",
  "quote",
  "unicode-ident",
 ]
 
+[[package]]
+name = "synstructure"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "syslog"
 version = "6.1.1"
@@ -1564,13 +2654,13 @@ dependencies = [
 
 [[package]]
 name = "tempfile"
-version = "3.15.0"
+version = "3.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
+checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
 dependencies = [
  "cfg-if",
  "fastrand",
- "getrandom 0.2.15",
+ "getrandom 0.3.1",
  "once_cell",
  "rustix",
  "windows-sys 0.59.0",
@@ -1588,18 +2678,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.66"
+version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.66"
+version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1608,9 +2698,9 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.36"
+version = "0.3.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
+checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
 dependencies = [
  "deranged",
  "itoa",
@@ -1631,28 +2721,100 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
 
 [[package]]
 name = "time-macros"
-version = "0.2.18"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
+checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
 dependencies = [
  "num-conv",
  "time-core",
 ]
 
 [[package]]
-name = "tinyvec"
-version = "1.8.0"
+name = "tiny-skia"
+version = "0.11.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
+checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
 dependencies = [
- "tinyvec_macros",
+ "arrayref",
+ "arrayvec",
+ "bytemuck",
+ "cfg-if",
+ "log",
+ "tiny-skia-path",
 ]
 
 [[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
+name = "tiny-skia-path"
+version = "0.11.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
+dependencies = [
+ "arrayref",
+ "bytemuck",
+ "strict-num",
+]
+
+[[package]]
+name = "tiny-xlib"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e"
+dependencies = [
+ "as-raw-xcb-connection",
+ "ctor-lite",
+ "libloading",
+ "pkg-config",
+ "tracing",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+
+[[package]]
+name = "toml_edit"
+version = "0.22.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+
+[[package]]
+name = "ttf-parser"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
 
 [[package]]
 name = "typed-arena"
@@ -1676,26 +2838,17 @@ dependencies = [
  "thiserror",
 ]
 
-[[package]]
-name = "unicode-bidi"
-version = "0.3.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
-
 [[package]]
 name = "unicode-ident"
-version = "1.0.13"
+version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
 
 [[package]]
-name = "unicode-normalization"
-version = "0.1.24"
+name = "unicode-segmentation"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
-dependencies = [
- "tinyvec",
-]
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
 
 [[package]]
 name = "unicode-width"
@@ -1705,15 +2858,27 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
 
 [[package]]
 name = "url"
-version = "2.5.2"
+version = "2.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
 dependencies = [
  "form_urlencoded",
  "idna",
  "percent-encoding",
 ]
 
+[[package]]
+name = "utf16_iter"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
 [[package]]
 name = "utf8parse"
 version = "0.2.2"
@@ -1726,12 +2891,231 @@ version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
 
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasi"
+version = "0.13.3+wasi-0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wayland-backend"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf"
+dependencies = [
+ "cc",
+ "downcast-rs",
+ "rustix",
+ "scoped-tls",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-client"
+version = "0.31.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f"
+dependencies = [
+ "bitflags 2.8.0",
+ "rustix",
+ "wayland-backend",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-csd-frame"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
+dependencies = [
+ "bitflags 2.8.0",
+ "cursor-icon",
+ "wayland-backend",
+]
+
+[[package]]
+name = "wayland-cursor"
+version = "0.31.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d"
+dependencies = [
+ "rustix",
+ "wayland-client",
+ "xcursor",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.32.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc"
+dependencies = [
+ "bitflags 2.8.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-plasma"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ccaacc76703fefd6763022ac565b590fcade92202492381c95b2edfdf7d46b3"
+dependencies = [
+ "bitflags 2.8.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-wlr"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2"
+dependencies = [
+ "bitflags 2.8.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.31.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484"
+dependencies = [
+ "proc-macro2",
+ "quick-xml",
+ "quote",
+]
+
+[[package]]
+name = "wayland-sys"
+version = "0.31.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615"
+dependencies = [
+ "dlib",
+ "log",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -1748,12 +3132,30 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.48.0"
@@ -1763,6 +3165,15 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.59.0"
@@ -1772,6 +3183,21 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
 [[package]]
 name = "windows-targets"
 version = "0.48.5"
@@ -1803,6 +3229,12 @@ dependencies = [
  "windows_x86_64_msvc 0.52.6",
 ]
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.48.5"
@@ -1815,6 +3247,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.48.5"
@@ -1827,6 +3265,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.48.5"
@@ -1845,6 +3289,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.48.5"
@@ -1857,6 +3307,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.48.5"
@@ -1869,6 +3325,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.48.5"
@@ -1881,6 +3343,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.48.5"
@@ -1893,6 +3361,120 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
+[[package]]
+name = "winit"
+version = "0.30.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0"
+dependencies = [
+ "ahash",
+ "android-activity",
+ "atomic-waker",
+ "bitflags 2.8.0",
+ "block2",
+ "bytemuck",
+ "calloop",
+ "cfg_aliases",
+ "concurrent-queue",
+ "core-foundation 0.9.4",
+ "core-graphics 0.23.2",
+ "cursor-icon",
+ "dpi",
+ "js-sys",
+ "libc",
+ "memmap2",
+ "ndk",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "orbclient",
+ "percent-encoding",
+ "pin-project",
+ "raw-window-handle",
+ "redox_syscall 0.4.1",
+ "rustix",
+ "sctk-adwaita",
+ "smithay-client-toolkit",
+ "smol_str",
+ "tracing",
+ "unicode-segmentation",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-protocols-plasma",
+ "web-sys",
+ "web-time",
+ "windows-sys 0.52.0",
+ "x11-dl",
+ "x11rb",
+ "xkbcommon-dl",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
+dependencies = [
+ "bitflags 2.8.0",
+]
+
+[[package]]
+name = "write16"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+
+[[package]]
+name = "writeable"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11rb"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
+dependencies = [
+ "as-raw-xcb-connection",
+ "gethostname",
+ "libc",
+ "libloading",
+ "once_cell",
+ "rustix",
+ "x11rb-protocol",
+]
+
+[[package]]
+name = "x11rb-protocol"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
+
 [[package]]
 name = "x25519-dalek"
 version = "2.0.1"
@@ -1904,6 +3486,31 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "xcursor"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61"
+
+[[package]]
+name = "xkbcommon-dl"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
+dependencies = [
+ "bitflags 2.8.0",
+ "dlib",
+ "log",
+ "once_cell",
+ "xkeysym",
+]
+
+[[package]]
+name = "xkeysym"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
+
 [[package]]
 name = "yasync"
 version = "0.1.0"
@@ -1937,6 +3544,30 @@ dependencies = [
  "yggdrasil-abi",
 ]
 
+[[package]]
+name = "yoke"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
 [[package]]
 name = "zerocopy"
 version = "0.7.35"
@@ -1949,11 +3580,11 @@ dependencies = [
 
 [[package]]
 name = "zerocopy"
-version = "0.8.8"
+version = "0.8.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a4e33e6dce36f2adba29746927f8e848ba70989fdb61c772773bbdda8b5d6a7"
+checksum = "79386d31a42a4996e3336b0919ddb90f81112af416270cff95b5f5af22b839c2"
 dependencies = [
- "zerocopy-derive 0.8.8",
+ "zerocopy-derive 0.8.18",
 ]
 
 [[package]]
@@ -1969,15 +3600,36 @@ dependencies = [
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.8.8"
+version = "0.8.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cd137b4cc21bde6ecce3bbbb3350130872cda0be2c6888874279ea76e17d4c1"
+checksum = "76331675d372f91bf8d17e13afbd5fe639200b73d01f0fc748bb059f9cca2db7"
 dependencies = [
  "proc-macro2",
  "quote",
  "syn",
 ]
 
+[[package]]
+name = "zerofrom"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
 [[package]]
 name = "zeroize"
 version = "1.8.1"
@@ -1997,3 +3649,25 @@ dependencies = [
  "quote",
  "syn",
 ]
+
+[[package]]
+name = "zerovec"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/userspace/Cargo.toml b/userspace/Cargo.toml
index 0f40ed15..019b6628 100644
--- a/userspace/Cargo.toml
+++ b/userspace/Cargo.toml
@@ -17,8 +17,10 @@ members = [
     "rsh",
     "lib/cross",
     "crypt",
-    "lib/runtime"
-, "lib/uipc"]
+    "lib/runtime",
+    "lib/uipc",
+    "lib/logsink"
+]
 exclude = ["dynload-program", "test-kernel-module", "lib/ygglibc"]
 
 [workspace.dependencies]
@@ -29,6 +31,7 @@ serde_json = "1.0.132"
 serde = { version = "1.0.214", features = ["derive"] }
 bytemuck = "1.19.0"
 thiserror = "1.0.64"
+env_logger = "0.11.5"
 sha2 = { version = "0.10.8" }
 chrono = { version = "0.4.31", default-features = false }
 postcard = { version = "1.1.1", features = ["alloc"] }
@@ -50,6 +53,7 @@ uipc.path = "lib/uipc"
 yggdrasil-rt.path = "../lib/runtime"
 yggdrasil-abi = { path = "../lib/abi", features = ["serde", "alloc", "bytemuck"] }
 abi-serde = { path = "../lib/abi-serde" }
+logsink.path = "lib/logsink"
 
 [workspace.lints.rust]
 unexpected_cfgs = { level = "allow", check-cfg = ['cfg(rust_analyzer)'] }
diff --git a/userspace/colors/Cargo.toml b/userspace/colors/Cargo.toml
index eb8c293c..bb7efdfd 100644
--- a/userspace/colors/Cargo.toml
+++ b/userspace/colors/Cargo.toml
@@ -7,13 +7,25 @@ authors = ["Mark Poliakov <mark@alnyan.me>"]
 [dependencies]
 uipc.workspace = true
 cross.workspace = true
+logsink.workspace = true
+libcolors = { workspace = true, default-features = false }
+
 serde.workspace = true
 thiserror.workspace = true
-libcolors = { workspace = true, default-features = false }
+log.workspace = true
+clap.workspace = true
 
 [target.'cfg(target_os = "yggdrasil")'.dependencies]
 yggdrasil-abi.workspace = true
 runtime.workspace = true
 
+[target.'cfg(unix)'.dependencies]
+winit = "0.30.9"
+softbuffer = "0.4.6"
+
+[dev-dependencies]
+winit = "0.30.9"
+softbuffer = "0.4.6"
+
 [lints]
 workspace = true
diff --git a/userspace/colors/src/display.rs b/userspace/colors/src/display.rs
index f70b5967..09051fe7 100644
--- a/userspace/colors/src/display.rs
+++ b/userspace/colors/src/display.rs
@@ -10,65 +10,11 @@ use std::{
 use crate::error::Error;
 
 pub struct Display<'a> {
-    #[allow(unused)]
-    mapping: FileMapping<'a>,
-    data: &'a mut [u32],
-
-    width: usize,
-    height: usize,
-    // TODO use those
-    _stride: usize,
-    _size: usize,
 }
 
 pub struct Point<T>(pub T, pub T);
 
 impl Display<'_> {
-    pub fn open(framebuffer: impl AsRef<Path>) -> Result<Self, Error> {
-        let file = OpenOptions::new().open(framebuffer)?;
-
-        let mut buffer = [0; 128];
-        device::device_request::<device::AcquireDevice>(file.as_raw_fd(), &mut buffer, &())
-            .map_err(std::io::Error::from)?;
-        let framebuffer = device::device_request::<device::GetActiveFramebuffer>(
-            file.as_raw_fd(),
-            &mut buffer,
-            &(),
-        )
-        .map_err(std::io::Error::from)?;
-
-        let width = framebuffer.width as usize;
-        let height = framebuffer.height as usize;
-
-        let mut mapping = FileMapping::new(file, 0, framebuffer.size)?;
-        let data = unsafe {
-            std::slice::from_raw_parts_mut(mapping.as_mut_ptr() as *mut u32, width * height)
-        };
-
-        Ok(Self {
-            mapping,
-            data,
-
-            width,
-            height,
-            _stride: framebuffer.stride / size_of::<u32>(),
-            _size: framebuffer.size / size_of::<u32>(),
-        })
-    }
-
-    pub fn flush(&mut self) {
-        let mut buffer = [0; 0];
-        device::device_request::<device::FlushDisplay>(self.mapping.as_raw_fd(), &mut buffer, &()).ok();
-    }
-
-    pub fn width(&self) -> usize {
-        self.width
-    }
-
-    pub fn height(&self) -> usize {
-        self.height
-    }
-
     pub fn fill(&mut self, color: u32) {
         self.data.fill(color);
     }
diff --git a/userspace/colors/src/input.rs b/userspace/colors/src/input.rs
index 34a96cd1..6112f948 100644
--- a/userspace/colors/src/input.rs
+++ b/userspace/colors/src/input.rs
@@ -1,15 +1,9 @@
-use std::{
-    fs::File,
-    io::Read,
-    os::fd::{AsRawFd, RawFd},
+use libcolors::{
+    event::{KeyInput, KeyModifiers},
+    input::Key,
 };
+// use yggdrasil_abi::io::{KeyboardKey, KeyboardKeyEvent};
 
-use libcolors::event::{KeyInput, KeyModifiers};
-use yggdrasil_abi::io::{KeyboardKey, KeyboardKeyEvent};
-
-use crate::error::Error;
-
-pub struct KeyboardInput(File);
 
 #[derive(Default)]
 pub struct InputState {
@@ -21,37 +15,15 @@ pub struct InputState {
     ralt: bool,
 }
 
-impl KeyboardInput {
-    pub fn open() -> Result<Self, Error> {
-        let file = File::open("/dev/kbd")?;
-        Ok(Self(file))
-    }
-
-    pub fn as_poll_fd(&self) -> RawFd {
-        self.0.as_raw_fd()
-    }
-
-    pub fn read_event(&mut self) -> Result<KeyboardKeyEvent, Error> {
-        let mut buf = [0; 4];
-        let len = self.0.read(&mut buf)?;
-
-        if len == 4 {
-            Ok(KeyboardKeyEvent::from_bytes(buf))
-        } else {
-            todo!()
-        }
-    }
-}
-
 impl InputState {
-    pub fn update(&mut self, key: KeyboardKey, state: bool) {
+    pub fn update(&mut self, key: Key, state: bool) {
         match key {
-            KeyboardKey::LAlt => self.lalt = state,
-            KeyboardKey::RAlt => self.ralt = state,
-            KeyboardKey::LShift => self.lshift = state,
-            KeyboardKey::RShift => self.rshift = state,
-            KeyboardKey::LControl => self.lctrl = state,
-            KeyboardKey::RControl => self.rctrl = state,
+            Key::LAlt => self.lalt = state,
+            Key::RAlt => self.ralt = state,
+            Key::LShift => self.lshift = state,
+            Key::RShift => self.rshift = state,
+            Key::LControl => self.lctrl = state,
+            Key::RControl => self.rctrl = state,
             _ => (),
         }
     }
@@ -75,15 +47,15 @@ impl InputState {
         }
     }
 
-    pub fn make_input(&self, key: KeyboardKey) -> KeyInput {
+    pub fn make_input(&self, key: Key) -> KeyInput {
         let modifiers = self.modifiers();
 
         let input = match (key, modifiers) {
-            (KeyboardKey::Char(ch), KeyModifiers::NONE) => Some(ch as _),
+            (Key::Char(ch), KeyModifiers::NONE) => Some(ch as _),
             // TODO proper shift key translation
-            (KeyboardKey::Char(ch), KeyModifiers::SHIFT) => Some(Self::translate_shift(ch) as _),
-            (KeyboardKey::Tab, KeyModifiers::NONE) => Some('\t'),
-            (KeyboardKey::Enter, KeyModifiers::NONE) => Some('\n'),
+            (Key::Char(ch), KeyModifiers::SHIFT) => Some(Self::translate_shift(ch) as _),
+            // (KeyboardKey::Tab, KeyModifiers::NONE) => Some('\t'),
+            (Key::Enter, KeyModifiers::NONE) => Some('\n'),
             (_, _) => None,
         };
 
diff --git a/userspace/colors/src/main.rs b/userspace/colors/src/main.rs
index 31f1d4b4..4888c81d 100644
--- a/userspace/colors/src/main.rs
+++ b/userspace/colors/src/main.rs
@@ -1,205 +1,93 @@
 #![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))]
+#![feature(map_many_mut, iter_chain)]
 
 use std::{
     collections::{BTreeMap, HashMap},
-    env,
-    os::{
-        fd::{AsRawFd, RawFd},
-        yggdrasil::io::poll::PollChannel,
-    },
-    path::Path,
+    marker::PhantomData,
+    os::fd::{AsRawFd, RawFd},
     process::{Command, ExitCode},
 };
 
-use cross::mem::{FileMapping, SharedMemory};
-use display::Display;
-use error::Error;
-use input::{InputState, KeyboardInput};
+use cross::mem::SharedMemory;
+use input::InputState;
 use libcolors::{
-    event::{EventData, KeyModifiers, KeyboardKey, KeyboardKeyEvent, WindowEvent, WindowInfo},
-    message::{ClientMessage, ServerMessage},
+    event::{EventData, KeyModifiers, KeyboardKeyEvent, WindowEvent, WindowInfo},
+    input::Key,
+    message::{ClientMessage, CreateWindowInfo, WindowType},
 };
-use uipc::{Channel, PeerAddress, Receiver, Sender};
+use sys::{Backend, DisplaySurface, FromClient, Point, ServerSender, WindowServer};
+use uipc::PeerAddress;
+use window::Window;
+use wm::{Direction, Workspace};
 
-// TODO rewrite and split this into meaningful components
-
-pub mod display;
-pub mod error;
 pub mod input;
+pub mod sys;
+pub mod window;
+pub mod wm;
 
-pub struct Window<'a> {
-    window_id: u32,
-    client_id: PeerAddress,
-
-    surface_mapping: FileMapping,
-    surface_data: &'a [u32],
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    #[error("{0}")]
+    Backend(#[from] sys::Error),
 }
 
-pub struct Frame {
-    x: u32,
-    y: u32,
-    w: u32,
-    h: u32,
-
-    dirty: bool,
-
-    window: Option<u32>,
-}
-
-pub struct Row {
-    frames: Vec<Frame>,
-    x: u32,
-    y: u32,
-    width: u32,
-    height: u32,
-}
-
-pub struct ServerSender(Sender<ServerMessage>);
-
-pub struct Server<'a, 'd> {
-    display: Display<'d>,
-
+pub struct Server<'a> {
     input_state: InputState,
-
     last_client_id: u32,
     client_map: HashMap<u32, PeerAddress>,
 
-    // Window management
+    workspace: Workspace<u32>,
     windows: BTreeMap<u32, Window<'a>>,
-    rows: Vec<Row>,
     last_window_id: u32,
-    focused_frame: Option<(usize, usize)>,
-    // Outer frame
-    padding: usize,
+
     background: u32,
-
-    // Event generators
-    poll: PollChannel,
-    receiver: Receiver<ClientMessage>,
-    input: KeyboardInput,
-
-    // Comms
-    sender: ServerSender,
+    _pd: PhantomData<&'a ()>,
 }
 
-impl Row {
-    pub fn new(x: u32, y: u32, w: u32, h: u32) -> Self {
-        Self {
-            frames: vec![],
-            x,
-            y,
-            width: w,
-            height: h,
-        }
-    }
-
-    pub fn balance_frames(&mut self) {
-        if self.frames.is_empty() {
-            return;
-        }
-
-        let spacing = 4;
-        let wc = self.frames.len() as u32;
-
-        let w = (self.width - spacing * (wc - 1)) / wc;
-        let h = self.height;
-        let mut x = self.x;
-        let y = self.y;
-
-        for frame in self.frames.iter_mut() {
-            frame.dirty = true;
-            frame.x = x;
-            frame.y = y;
-            frame.w = w;
-            frame.h = h;
-
-            x += w + spacing;
-        }
-    }
-
-    pub fn place_frame(&mut self) -> &mut Frame {
-        self.frames.push(Frame {
-            x: 0,
-            y: 0,
-            w: 0,
-            h: 0,
-
-            dirty: true,
-            window: None,
-        });
-
-        self.balance_frames();
-
-        self.frames.last_mut().unwrap()
-    }
-
-    pub fn remove_frame(&mut self, col: usize) {
-        self.frames.remove(col);
-
-        self.balance_frames();
-    }
-}
-
-impl<'a> Server<'a, '_> {
-    pub fn new(framebuffer: &str) -> Result<Self, Error> {
-        let mut poll = PollChannel::new()?;
-
-        let mut display = Display::open(framebuffer)?;
-        let input = KeyboardInput::open()?;
-
-        let channel = Channel::bind(libcolors::CHANNEL_NAME)?;
-        let (sender, receiver) = channel.split();
-        let sender = ServerSender(sender);
-
-        poll.add(input.as_poll_fd())?;
-        poll.add(receiver.as_raw_fd())?;
-
-        let background = 0xFFCCCCCC;
-        display.fill(background);
-        display.flush();
-
+impl<'a> Server<'a> {
+    fn new() -> Result<Self, Error> {
         Ok(Self {
-            display,
-
             input_state: InputState::default(),
 
-            poll,
-            receiver,
-            input,
-            sender,
-
-            padding: 4,
-            background,
+            background: 0xCCCCCC,
 
             last_client_id: 0,
             client_map: HashMap::new(),
 
             windows: BTreeMap::new(),
-            rows: vec![],
+            workspace: Workspace::new(800, 600),
             last_window_id: 1,
-            focused_frame: None,
+
+            _pd: PhantomData, // windows: BTreeMap::new(),
+                              // rows: vec![],
+                              // focused_frame: None,
         })
     }
 
-    fn create_window(&mut self, client_id: &PeerAddress) -> Result<(WindowInfo, RawFd), Error> {
-        if self.rows.is_empty() {
-            self.rows.push(Row::new(
-                self.padding as _,
-                self.padding as _,
-                (self.display.width() - self.padding * 2) as _,
-                (self.display.height() - self.padding * 2) as _,
-            ));
-        }
-
-        // Create a frame
-        let row = self.rows.last_mut().unwrap();
-        let frame = row.place_frame();
-
-        // Create the actual window
-        let window_id = self.last_window_id;
+    fn create_window(
+        &mut self,
+        surface: &mut DisplaySurface,
+        tx: &mut ServerSender,
+        peer: &PeerAddress,
+        info: CreateWindowInfo
+    ) -> Result<(WindowInfo, RawFd), Error> {
+        let wid = self.last_window_id;
         self.last_window_id += 1;
-
-        let mapping_size = self.display.width() * self.display.height() * 4;
+        let need_focus = match info.ty {
+            WindowType::Default => {
+                if !self.workspace.create_window(wid) {
+                    todo!()
+                }
+                true
+            }
+            WindowType::Reservation(height) => {
+                self.workspace.add_reservation(wid, height);
+                false
+            }
+        };
+        let frame = self.workspace.window_frame(wid).unwrap();
+        let mapping_size = surface.width() * surface.height() * 4;
+        // let mapping_size = self.display.width() * self.display.height() * 4;
         let surface_shm = SharedMemory::new(mapping_size).unwrap();
         let fd = surface_shm.as_raw_fd();
         let mut surface_mapping = surface_shm.map().unwrap();
@@ -211,298 +99,133 @@ impl<'a> Server<'a, '_> {
             )
         };
 
-        frame.window = Some(window_id);
-
         let window = Window {
-            window_id,
-            client_id: client_id.clone(),
+            wid,
+            peer: peer.clone(),
             surface_mapping,
             surface_data,
         };
 
-        self.windows.insert(window_id, window);
+        self.windows.insert(wid, window);
 
         let info = WindowInfo {
-            window_id,
-            surface_stride: self.display.width() * 4,
+            window_id: wid,
+            surface_stride: surface.width() * 4, // self.display.width() * 4,
             surface_mapping_size: mapping_size,
             width: frame.w,
             height: frame.h,
         };
 
-        self.display.fill(self.background);
-        self.set_focused_window(window_id)?;
-        self.flush_dirty_frames();
+        surface.fill(self.background);
+        if need_focus {
+            self.focus_window(tx, wid);
+        }
 
         Ok((info, fd))
     }
 
-    fn remove_window(&mut self, window_id: u32) {
-        // Find the window
-        if !self.windows.contains_key(&window_id) {
-            return;
+    fn focus_window(&mut self, tx: &mut ServerSender, wid: u32) {
+        let (old_wid, new) = self.workspace.focus_window(wid);
+        let old = old_wid.and_then(|wid| {
+            let window = self.windows.get(&wid)?;
+            Some((wid, window))
+        });
+        let new = new.and_then(|_| self.windows.get(&wid));
+
+        if let Some((wid, window)) = old {
+            log::info!("wid #{wid} focus=false");
+            tx.send_event(
+                EventData::WindowEvent(wid, WindowEvent::FocusChanged(false)),
+                &window.peer,
+            );
         }
-
-        // TODO this is ugly
-        let mut res = None;
-        for (i, row) in self.rows.iter().enumerate() {
-            let j = row
-                .frames
-                .iter()
-                .position(|f| f.window.map(|w| w == window_id).unwrap_or(false));
-
-            if let Some(j) = j {
-                res = Some((i, j));
-            }
+        if let Some(window) = new {
+            log::info!("wid #{wid} focus=true");
+            tx.send_event(
+                EventData::WindowEvent(wid, WindowEvent::FocusChanged(true)),
+                &window.peer,
+            );
         }
+    }
 
-        // Remove the frame
-        if let Some((row, col)) = res {
-            self.rows[row].remove_frame(col);
-            self.display.fill(self.background);
-            self.flush_dirty_frames();
+    fn move_focus(&mut self, tx: &mut ServerSender, direction: Direction) {
+        if let Some(wid) = self.workspace.window_towards_wrap(direction) {
+            self.focus_window(tx, wid);
         }
+    }
 
-        self.windows.remove(&window_id);
+    fn move_window(&mut self, surface: &mut DisplaySurface, tx: &mut ServerSender, direction: Direction) {
+        if self.workspace.move_window(direction) {
+            surface.fill(self.background);
+            self.flush_dirty_frames(tx);
+            self.workspace.all_windows().for_each(|(wid, _)| {
+                 if let Some(window) = self.windows.get(&wid) {
+                     tx.send_event(
+                         EventData::WindowEvent(wid, WindowEvent::RedrawRequested),
+                         &window.peer,
+                     );
+                 }
+            });
+        }
+    }
 
-        if self.focused_frame == res {
-            self.focused_frame = None;
-
-            let new_focus = if let Some((row, col)) = res {
-                // Focus some other frame in the same row
-                if let Some(f_row) = self.rows.get(row) {
-                    let row_len = f_row.frames.len();
-
-                    if col == 0 && row_len != 0 {
-                        Some((row, 1))
-                    } else if col > 0 {
-                        Some((row, col - 1))
-                    } else {
-                        // Empty row
-                        None
-                    }
-                } else {
-                    // No row exists
-                    None
-                }
-            } else {
-                // No frames?
-                None
+    fn flush_dirty_frames(&mut self, tx: &mut ServerSender) {
+        for (wid, rect) in self.workspace.dirty_windows() {
+            let Some(window) = self.windows.get_mut(&wid) else {
+                continue;
             };
+            log::info!("Resize #{}: {}x{}", wid, rect.w, rect.h);
 
-            self.set_focused_frame(new_focus);
+            window.resize(rect.w, rect.h);
+
+            tx.send_event(
+                EventData::WindowEvent(
+                    wid,
+                    WindowEvent::Resized {
+                        width: rect.w,
+                        height: rect.h,
+                    },
+                ),
+                &window.peer,
+            );
         }
     }
+}
 
-    fn handle_keyboard_event(&mut self, event: KeyboardKeyEvent) -> Result<(), Error> {
-        let (key, state) = event.split();
-
-        self.input_state.update(key, state);
-
-        if state {
-            let input = self.input_state.make_input(key);
-
-            // Non-window keys
-            #[allow(clippy::single_match)]
-            match (input.modifiers, input.key) {
-                (KeyModifiers::ALT, KeyboardKey::Enter) => {
-                    // TODO do something with spawned child
-                    Command::new("/bin/term").spawn().ok();
-                    return Ok(());
-                }
-                _ => (),
-            }
-
-            // Window keys
-            if let Some((row, col)) = self.focused_frame {
-                let row_len = self.rows[row].frames.len();
-
-                match (input.modifiers, input.key) {
-                    (KeyModifiers::ALT, KeyboardKey::Char(b'l')) => {
-                        if col + 1 < row_len {
-                            self.set_focused_frame(Some((row, col + 1)));
-                        } else {
-                            self.set_focused_frame(Some((row, 0)));
-                        }
-
-                        return Ok(());
-                    }
-                    (KeyModifiers::ALT, KeyboardKey::Char(b'h')) => {
-                        if col > 0 {
-                            self.set_focused_frame(Some((row, col - 1)));
-                        } else if row_len != 0 {
-                            self.set_focused_frame(Some((row, row_len - 1)));
-                        }
-
-                        return Ok(());
-                    }
-                    _ => (),
-                }
-            }
-
-            if let Some((_, window)) = self.get_focused_window() {
-                // Deliver event to the window
-                self.sender
-                    .send_event(
-                        EventData::WindowEvent(window.window_id, WindowEvent::KeyInput(input)),
-                        &window.client_id,
-                    )
-                    .ok();
-            } else {
-                self.focused_frame = None;
-            }
-        }
-
-        Ok(())
-    }
-
-    fn get_window(&self, window_id: u32) -> Option<(&Frame, &Window<'a>)> {
-        let window = self.windows.get(&window_id)?;
-        for row in self.rows.iter() {
-            if let Some(f) = row
-                .frames
-                .iter()
-                .find(|f| f.window.map(|w| w == window_id).unwrap_or(false))
-            {
-                return Some((f, window));
-            }
-        }
-        // TODO Orphaned frame/window?
-        None
-    }
-
-    fn get_focused_window(&self) -> Option<(&Frame, &Window<'a>)> {
-        let (row, col) = self.focused_frame?;
-
-        let frame = &self.rows[row].frames[col];
-        let window = frame.window.and_then(|w| self.windows.get(&w))?;
-
-        Some((frame, window))
-    }
-
-    fn set_focused_frame(&mut self, focus: Option<(usize, usize)>) {
-        if self.focused_frame == focus {
-            return;
-        }
-
-        if let Some((_, old_window)) = self.get_focused_window() {
-            self.sender
-                .send_event(
-                    EventData::WindowEvent(old_window.window_id, WindowEvent::FocusChanged(false)),
-                    &old_window.client_id,
-                )
-                .ok();
-        }
-
-        self.focused_frame = focus;
-
-        if let Some((row, col)) = focus {
-            let Some(f_row) = self.rows.get(row) else {
-                return;
-            };
-            let Some(frame) = f_row.frames.get(col) else {
-                return;
-            };
-            let Some(window) = frame.window.and_then(|w| self.windows.get(&w)) else {
-                return;
-            };
-
-            self.sender
-                .send_event(
-                    EventData::WindowEvent(window.window_id, WindowEvent::FocusChanged(true)),
-                    &window.client_id,
-                )
-                .ok();
-        }
-    }
-
-    fn set_focused_window(&mut self, window_id: u32) -> Result<(), Error> {
-        // TODO this is ugly
-        let mut res = None;
-        for (i, row) in self.rows.iter().enumerate() {
-            let j = row
-                .frames
-                .iter()
-                .position(|f| f.window.map(|w| w == window_id).unwrap_or(false));
-
-            if let Some(j) = j {
-                res = Some((i, j));
-            }
-        }
-
-        self.set_focused_frame(res);
-
-        Ok(())
-    }
-
-    fn flush_dirty_frames(&mut self) {
-        for row in self.rows.iter() {
-            for frame in row.frames.iter() {
-                if !frame.dirty {
-                    continue;
-                }
-
-                let Some(window) = frame.window.and_then(|w| self.windows.get_mut(&w)) else {
-                    // TODO handle orphaned frame
-                    continue;
-                };
-
-                let new_surface_data = unsafe {
-                    std::slice::from_raw_parts_mut(
-                        window.surface_mapping.as_mut_ptr() as *mut u32,
-                        (frame.w * frame.h) as usize,
-                    )
-                };
-
-                window.surface_data = new_surface_data;
-
-                self.sender
-                    .send_event(
-                        EventData::WindowEvent(
-                            window.window_id,
-                            WindowEvent::Resized {
-                                width: frame.w,
-                                height: frame.h,
-                            },
-                        ),
-                        &window.client_id,
-                    )
-                    .ok();
-            }
-        }
+impl WindowServer for Server<'_> {
+    fn handle_initial(&mut self, mut surface: DisplaySurface) {
+        self.workspace.resize(surface.width() as u32, surface.height() as u32);
+        surface.fill(self.background);
+        surface.present();
     }
 
     fn handle_client_message(
         &mut self,
-        client_id: PeerAddress,
-        message: ClientMessage,
-    ) -> Result<(), Error> {
-        match message {
+        mut surface: DisplaySurface,
+        tx: &mut ServerSender,
+        message: FromClient,
+    ) {
+        let peer = message.peer;
+        match message.message {
             ClientMessage::ClientHello => {
-                debug_trace!("{:?}: ClientHello", client_id);
+                log::info!("{:?}: ClientHello", peer);
                 // Echo the ID back
                 self.last_client_id += 1;
                 let id = self.last_client_id;
-                self.client_map.insert(id, client_id.clone());
-
-                self.sender.send_event(EventData::ServerHello(id), &client_id)
+                self.client_map.insert(id, peer.clone());
+                tx.send_event(EventData::ServerHello(id), &peer);
             }
-            ClientMessage::CreateWindow => {
-                debug_trace!("{:?}: CreateWindow", client_id);
-                let (info, shm_fd) = self.create_window(&client_id)?;
+            ClientMessage::CreateWindow(info) => {
+                log::info!("{:?}: CreateWindow", peer);
+                let (info, shm_fd) = self.create_window(&mut surface, tx, &peer, info).unwrap();
                 let window_id = info.window_id;
-
-                self.sender.send_event_with_file(
-                    EventData::NewWindowInfo(info),
-                    &shm_fd,
-                    &client_id,
-                )?;
-                self.sender.send_event(
+                tx.send_event_with_file(EventData::NewWindowInfo(info), &shm_fd, &peer);
+                self.flush_dirty_frames(tx);
+                tx.send_event(
                     EventData::WindowEvent(window_id, WindowEvent::RedrawRequested),
-                    &client_id,
-                )?;
-
-                Ok(())
+                    &peer,
+                );
+                surface.present();
             }
             ClientMessage::BlitWindow {
                 window_id,
@@ -511,98 +234,148 @@ impl<'a> Server<'a, '_> {
                 w,
                 h,
             } => {
-                if let Some((frame, window)) = self.get_window(window_id) {
+                log::trace!("{:?}: BlitWindow", peer);
+                if let Some(window) = self.windows.get(&window_id) {
+                    let Some(frame) = self.workspace.window_frame(window_id) else {
+                        return;
+                    };
+
                     let x = x.min(frame.w);
                     let y = y.min(frame.h);
-                    let w = w.min(frame.w - x);
-                    let h = h.min(frame.h - y);
+                    let w = w.min(frame.w.saturating_sub(x));
+                    let h = h.min(frame.h.saturating_sub(y));
 
                     if w == 0 || h == 0 {
                         // Invalid rectangle, skip it
-                        return Ok(());
+                        return;
                     }
 
-                    self.display.blit_buffer(
+                    let dst = Point(frame.x as _, frame.y as _);
+                    let src = Point(x as _, y as _);
+
+                    log::info!("Blit {src:?} {w}x{h} -> {dst:?}");
+
+                    surface.blit_buffer(
                         window.surface_data,
-                        display::Point(frame.x as _, frame.y as _),
-                        display::Point(x as _, y as _),
+                        dst,
+                        src,
                         w as _,
                         h as _,
                         frame.w as usize,
                     );
                 }
-                Ok(())
             }
-            ClientMessage::DestroyWindow(window_id) => {
-                debug_trace!("{:?}: DestroyWindow {}", client_id, window_id);
-                self.remove_window(window_id);
-                Ok(())
+            ClientMessage::DestroyWindow(wid) => {
+                log::info!("{:?}: DestroyWindow", peer);
+                let window = self.windows.remove(&wid);
+                if window.is_some() {
+                    self.workspace.remove_window(wid);
+                    self.flush_dirty_frames(tx);
+                }
+                surface.present();
             }
         }
     }
 
-    fn run_inner(mut self) -> Result<(), Error> {
-        loop {
-            match self.poll.wait(None, true)? {
-                Some((fd, Ok(_))) if fd == self.input.as_poll_fd() => {
-                    let event = self.input.read_event()?;
-                    self.handle_keyboard_event(event)?;
+    fn handle_keyboard_event(
+        &mut self,
+        mut surface: DisplaySurface,
+        tx: &mut ServerSender,
+        event: KeyboardKeyEvent,
+    ) {
+        self.input_state.update(event.key, event.state);
+
+        if event.state {
+            let input = self.input_state.make_input(event.key);
+
+            // Non-window keys
+            #[allow(clippy::single_match)]
+            match (input.modifiers, input.key) {
+                (KeyModifiers::ALT, Key::Enter) => {
+                    // TODO do something with spawned child
+                    Command::new("/bin/term").spawn().ok();
+                    return;
                 }
-                Some((fd, Ok(_))) if fd == self.receiver.as_raw_fd() => {
-                    let (data, client_id) = self.receiver.receive_from()?;
-                    self.handle_client_message(client_id, data)?;
-                }
-                Some((_, Ok(_))) => {
-                    todo!()
-                }
-                Some((_, Err(error))) => {
-                    return Err(Error::from(error));
-                }
-                None => (),
+                _ => (),
             }
-        }
-    }
 
-    pub fn run(self) -> ExitCode {
-        match self.run_inner() {
-            Ok(_) => ExitCode::SUCCESS,
-            Err(error) => {
-                debug_trace!("colors server finished with an error: {}", error);
-                ExitCode::FAILURE
+            let focus = self.workspace.focused_window().and_then(|wid| {
+                let window = self.windows.get(&wid)?;
+                Some((wid, window))
+            });
+
+            match (input.modifiers, input.key) {
+                (KeyModifiers::ALT, Key::Char(b'l')) => {
+                    self.move_focus(tx, Direction::Right);
+                    return;
+                }
+                (KeyModifiers::ALT, Key::Char(b'h')) => {
+                    self.move_focus(tx, Direction::Left);
+                    return;
+                }
+                (KeyModifiers::ALT, Key::Char(b'j')) => {
+                    self.move_focus(tx, Direction::Down);
+                    return;
+                }
+                (KeyModifiers::ALT, Key::Char(b'k')) => {
+                    self.move_focus(tx, Direction::Up);
+                    return;
+                }
+                (KeyModifiers::ALT_SHIFT, Key::Char(b'l')) => {
+                    self.move_window(&mut surface, tx, Direction::Right);
+                    surface.present();
+                    return;
+                }
+                (KeyModifiers::ALT_SHIFT, Key::Char(b'h')) => {
+                    self.move_window(&mut surface, tx, Direction::Left);
+                    surface.present();
+                    return;
+                }
+                (KeyModifiers::ALT_SHIFT, Key::Char(b'j')) => {
+                    self.move_window(&mut surface, tx, Direction::Down);
+                    surface.present();
+                    return;
+                }
+                (KeyModifiers::ALT_SHIFT, Key::Char(b'k')) => {
+                    self.move_window(&mut surface, tx, Direction::Up);
+                    surface.present();
+                    return;
+                }
+                _ => (),
             }
+
+            if let Some((wid, window)) = focus {
+                // Deliver event to the window
+                tx.send_event(
+                    EventData::WindowEvent(wid, WindowEvent::KeyInput(input)),
+                    &window.peer,
+                );
+            }
+
+            // // Window keys
+            // if let Some((row, col)) = self.focused_frame {
+            //     let row_len = self.rows[row].frames.len();
+
+            // if let Some((_, window)) = self.get_focused_window() {
+            // } else {
+            //     self.focused_frame = None;
+            // }
         }
     }
 }
 
-impl ServerSender {
-    pub fn send_event(&self, event: EventData, client_id: &PeerAddress) -> Result<(), Error> {
-        self.0.send_to(&ServerMessage::Event(event), client_id)?;
-        Ok(())
-    }
-
-    pub fn send_event_with_file<F: AsRawFd>(
-        &self,
-        event: EventData,
-        file: &F,
-        client_id: &PeerAddress,
-    ) -> Result<(), Error> {
-        self.0
-            .send_with_file_to(&ServerMessage::Event(event), file, client_id)?;
-        Ok(())
-    }
-}
-
 fn main() -> ExitCode {
-    let args = env::args().skip(1).collect::<Vec<_>>();
-    let framebuffer = args.first().map_or("/dev/fb0", |s| s.as_str());
-    let path: &Path = framebuffer.as_ref();
+    logsink::setup_logging();
+    log::info!("Colors starting");
 
-    if !path.exists() {
-        debug_trace!("{framebuffer} does not exist, colors won't start");
-        return ExitCode::FAILURE;
+    let server = Server::new().unwrap();
+    let backend = Backend::new(server).unwrap();
+    log::info!("Run backend");
+    match backend.run() {
+        Ok(()) => ExitCode::SUCCESS,
+        Err(error) => {
+            eprintln!("{error}");
+            ExitCode::FAILURE
+        }
     }
-
-    let server = Server::new(framebuffer).unwrap();
-
-    server.run()
 }
diff --git a/userspace/colors/src/sys/mod.rs b/userspace/colors/src/sys/mod.rs
new file mode 100644
index 00000000..8e7adde2
--- /dev/null
+++ b/userspace/colors/src/sys/mod.rs
@@ -0,0 +1,66 @@
+use std::os::fd::{AsRawFd, OwnedFd};
+
+use libcolors::{
+    event::{EventData, KeyboardKeyEvent},
+    message::{ClientMessage, ServerMessage},
+};
+use uipc::{PeerAddress, Sender};
+
+#[cfg(any(rust_analyzer, target_os = "yggdrasil"))]
+pub mod yggdrasil;
+#[cfg(any(rust_analyzer, unix))]
+pub mod unix;
+
+#[cfg(any(rust_analyzer, target_os = "yggdrasil"))]
+pub use yggdrasil::{Backend, DisplaySurface, Error};
+#[cfg(any(rust_analyzer, unix))]
+pub use unix::{Backend, DisplaySurface, Error};
+
+#[derive(Debug)]
+pub struct FromClient {
+    pub peer: PeerAddress,
+    pub message: ClientMessage,
+    pub file: Option<OwnedFd>,
+}
+
+pub struct ServerSender {
+    sender: Sender<ServerMessage>,
+}
+
+#[derive(Debug)]
+pub struct Point<T>(pub T, pub T);
+
+pub trait WindowServer {
+    fn handle_initial(&mut self, surface: DisplaySurface);
+    fn handle_client_message(
+        &mut self,
+        surface: DisplaySurface,
+        tx: &mut ServerSender,
+        message: FromClient,
+    );
+    fn handle_keyboard_event(
+        &mut self,
+        surface: DisplaySurface,
+        tx: &mut ServerSender,
+        event: KeyboardKeyEvent,
+    );
+}
+
+impl ServerSender {
+    pub fn send_event(&self, event: EventData, client_id: &PeerAddress) {
+        self.sender
+            .send_to(&ServerMessage::Event(event), client_id)
+            .ok();
+    }
+
+    pub fn send_event_with_file<F: AsRawFd>(
+        &self,
+        event: EventData,
+        file: &F,
+        client_id: &PeerAddress,
+    ) {
+        self.sender
+            .send_with_file_to(&ServerMessage::Event(event), file, client_id)
+            .ok();
+    }
+}
diff --git a/userspace/colors/src/sys/unix.rs b/userspace/colors/src/sys/unix.rs
new file mode 100644
index 00000000..d05ed04a
--- /dev/null
+++ b/userspace/colors/src/sys/unix.rs
@@ -0,0 +1,262 @@
+use std::{
+    cmp, fs, io,
+    num::NonZero,
+    ops::{Deref, DerefMut},
+    rc::Rc,
+};
+
+use clap::Parser;
+use libcolors::{
+    event::KeyboardKeyEvent,
+    input::Key,
+    message::{ClientMessage, ServerMessage},
+};
+use softbuffer::{Buffer, Rect, SoftBufferError};
+use uipc::{Receiver, Sender};
+use winit::{
+    dpi::LogicalSize,
+    error::{EventLoopError, OsError},
+    event::{ElementState, Event, WindowEvent},
+    event_loop::{EventLoop, EventLoopProxy},
+    keyboard::{KeyCode, PhysicalKey},
+    window::{Window, WindowAttributes},
+};
+
+use super::{FromClient, Point, ServerSender, WindowServer};
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    #[error("I/O error: {0}")]
+    Io(#[from] io::Error),
+    #[error("Event loop error: {0}")]
+    EventLoop(#[from] EventLoopError),
+    #[error("OS error: {0}")]
+    Os(#[from] OsError),
+    #[error("Display error: {0}")]
+    Softbuffer(#[from] SoftBufferError),
+}
+
+pub struct Backend<S: WindowServer> {
+    event_loop: EventLoop<FromClient>,
+    window: Rc<Window>,
+    tx: Sender<ServerMessage>,
+    server: S,
+}
+
+pub struct DisplaySurface<'a> {
+    inner: Buffer<'a, Rc<Window>, Rc<Window>>,
+    width: usize,
+    height: usize,
+}
+
+#[derive(Debug, Parser)]
+struct Args {
+    #[clap(short, help = "Framebuffer width", default_value_t = 1024)]
+    width: u32,
+    #[clap(short, help = "Framebuffer height", default_value_t = 768)]
+    height: u32,
+}
+
+impl DisplaySurface<'_> {
+    pub fn width(&self) -> usize {
+        self.width
+    }
+
+    pub fn height(&self) -> usize {
+        self.height
+    }
+
+    pub fn blit_buffer(
+        mut self,
+        source: &[u32],
+        dst: Point<usize>,
+        src: Point<usize>,
+        w: usize,
+        h: usize,
+        src_stride: usize,
+    ) {
+        let src_w = (self.width - src.0).min(w);
+        let dst_w = (self.width - dst.0).min(w);
+        let src_h = (self.height - src.1).min(h);
+        let dst_h = (self.height - dst.1).min(h);
+        let w = cmp::min(src_w, dst_w);
+        let h = cmp::min(src_h, dst_h);
+
+        for y in 0..h {
+            let dst_offset = (y + src.1 + dst.1) * self.width + dst.0 + src.0;
+            let src_offset = (y + src.1) * src_stride + src.0;
+
+            let src_chunk = &source[src_offset..src_offset + w];
+            let dst_chunk = &mut self[dst_offset..dst_offset + w];
+
+            dst_chunk.copy_from_slice(src_chunk);
+        }
+
+        self.inner
+            .present_with_damage(&[Rect {
+                x: dst.0 as _,
+                y: dst.1 as _,
+                width: NonZero::new(w as _).unwrap(),
+                height: NonZero::new(h as _).unwrap(),
+            }])
+            .unwrap();
+    }
+
+    pub fn present(self) {
+        self.inner.present().unwrap();
+    }
+}
+
+impl Deref for DisplaySurface<'_> {
+    type Target = [u32];
+
+    fn deref(&self) -> &Self::Target {
+        self.inner.deref()
+    }
+}
+
+impl DerefMut for DisplaySurface<'_> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.inner.deref_mut()
+    }
+}
+
+impl<S: WindowServer> Backend<S> {
+    pub fn new(server: S) -> Result<Self, Error> {
+        let args = Args::parse();
+        let event_loop = EventLoop::with_user_event().build()?;
+        let window = event_loop.create_window(
+            WindowAttributes::new()
+                .with_title("colors")
+                .with_resizable(false)
+                .with_inner_size(LogicalSize::new(args.width, args.height)),
+        )?;
+        fs::remove_file(libcolors::CHANNEL_NAME).ok();
+        let event_proxy = event_loop.create_proxy();
+        let listener = uipc::Channel::bind(libcolors::CHANNEL_NAME).unwrap();
+        let (tx, rx) = listener.split();
+
+        std::thread::spawn(move || {
+            Self::io_worker(rx, event_proxy);
+        });
+        Ok(Self {
+            event_loop,
+            window: Rc::new(window),
+            tx,
+            server,
+        })
+    }
+
+    fn io_worker(mut rx: Receiver<ClientMessage>, event_proxy: EventLoopProxy<FromClient>) {
+        loop {
+            let (message, file, peer) = rx.receive_with_file_from().unwrap();
+            let message = FromClient {
+                message,
+                file,
+                peer,
+            };
+            event_proxy.send_event(message).unwrap();
+        }
+    }
+
+    pub fn run(mut self) -> Result<(), Error> {
+        let mut tx = ServerSender { sender: self.tx };
+        let context = softbuffer::Context::new(self.window.clone())?;
+        let mut surface = softbuffer::Surface::new(&context, self.window.clone())?;
+
+        let size = self.window.inner_size();
+        surface.resize(NonZero::new(size.width).unwrap(), NonZero::new(size.height).unwrap()).unwrap();
+        self.server.handle_initial(DisplaySurface {
+            inner: surface.buffer_mut().unwrap(),
+            width: size.width as _,
+            height: size.height as _,
+        });
+
+        self.event_loop.run(|event, el| match event {
+            Event::WindowEvent { event, .. } => match event {
+                WindowEvent::CloseRequested => {
+                    el.exit();
+                }
+                WindowEvent::Resized(size) => {
+                    let width = NonZero::new(size.width).unwrap();
+                    let height = NonZero::new(size.height).unwrap();
+                    surface.resize(width, height).unwrap();
+                    surface.buffer_mut().unwrap().present().unwrap();
+                }
+                WindowEvent::KeyboardInput { event, .. } => {
+                    if let Some(event) = convert_key_event(event) {
+                        let size = self.window.inner_size();
+                        let display_surface = DisplaySurface {
+                            inner: surface.buffer_mut().unwrap(),
+                            width: size.width as usize,
+                            height: size.height as usize,
+                        };
+                        self.server
+                            .handle_keyboard_event(display_surface, &mut tx, event);
+                    }
+                }
+                _ => (),
+            },
+            Event::UserEvent(event) => {
+                let size = self.window.inner_size();
+                let display_surface = DisplaySurface {
+                    inner: surface.buffer_mut().unwrap(),
+                    width: size.width as usize,
+                    height: size.height as usize,
+                };
+                self.server
+                    .handle_client_message(display_surface, &mut tx, event);
+            }
+            _ => (),
+        })?;
+        Ok(())
+    }
+}
+
+fn convert_key_event(raw: winit::event::KeyEvent) -> Option<KeyboardKeyEvent> {
+    let PhysicalKey::Code(code) = raw.physical_key else {
+        return None;
+    };
+
+    let key = match code {
+        KeyCode::ShiftLeft => Key::LShift,
+        KeyCode::ShiftRight => Key::RShift,
+        KeyCode::ControlLeft => Key::LControl,
+        KeyCode::ControlRight => Key::RControl,
+        KeyCode::AltLeft => Key::LAlt,
+        KeyCode::AltRight => Key::RAlt,
+        KeyCode::KeyQ => Key::Char(b'q'),
+        KeyCode::KeyW => Key::Char(b'w'),
+        KeyCode::KeyE => Key::Char(b'e'),
+        KeyCode::KeyR => Key::Char(b'r'),
+        KeyCode::KeyT => Key::Char(b't'),
+        KeyCode::KeyY => Key::Char(b'y'),
+        KeyCode::KeyU => Key::Char(b'u'),
+        KeyCode::KeyI => Key::Char(b'i'),
+        KeyCode::KeyO => Key::Char(b'o'),
+        KeyCode::KeyP => Key::Char(b'p'),
+        KeyCode::KeyA => Key::Char(b'a'),
+        KeyCode::KeyS => Key::Char(b's'),
+        KeyCode::KeyD => Key::Char(b'd'),
+        KeyCode::KeyF => Key::Char(b'f'),
+        KeyCode::KeyG => Key::Char(b'g'),
+        KeyCode::KeyH => Key::Char(b'h'),
+        KeyCode::KeyJ => Key::Char(b'j'),
+        KeyCode::KeyK => Key::Char(b'k'),
+        KeyCode::KeyL => Key::Char(b'l'),
+        KeyCode::KeyZ => Key::Char(b'z'),
+        KeyCode::KeyX => Key::Char(b'x'),
+        KeyCode::KeyC => Key::Char(b'c'),
+        KeyCode::KeyV => Key::Char(b'v'),
+        KeyCode::KeyB => Key::Char(b'b'),
+        KeyCode::KeyN => Key::Char(b'n'),
+        KeyCode::KeyM => Key::Char(b'm'),
+        _ => return None,
+    };
+    let state = match raw.state {
+        ElementState::Pressed => true,
+        ElementState::Released => false,
+    };
+
+    Some(KeyboardKeyEvent { key, state })
+}
diff --git a/userspace/colors/src/sys/yggdrasil.rs b/userspace/colors/src/sys/yggdrasil.rs
new file mode 100644
index 00000000..94670ea2
--- /dev/null
+++ b/userspace/colors/src/sys/yggdrasil.rs
@@ -0,0 +1,278 @@
+use std::{
+    cmp,
+    fs::{File, OpenOptions},
+    io::{self, Read},
+    ops::{Deref, DerefMut},
+    os::fd::{AsRawFd, RawFd},
+    path::{Path, PathBuf},
+};
+
+use clap::Parser;
+use cross::{io::Poll, mem::FileMapping};
+use libcolors::{event::KeyboardKeyEvent, input::Key, message::ClientMessage};
+use runtime::rt::io::device;
+use uipc::{Channel, Receiver};
+use yggdrasil_abi::io::KeyboardKey;
+
+use crate::sys::FromClient;
+
+use super::{Point, ServerSender, WindowServer};
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    #[error("I/O error: {0}")]
+    Io(#[from] io::Error),
+    #[error("IPC error: {0}")]
+    Ipc(#[from] uipc::Error),
+}
+
+pub struct Backend<'a, S: WindowServer> {
+    poll: Poll,
+    rx: Receiver<ClientMessage>,
+    tx: ServerSender,
+
+    display: Display<'a>,
+    input: KeyboardInput,
+
+    server: S,
+}
+
+struct Display<'a> {
+    #[allow(unused)]
+    mapping: FileMapping,
+    data: &'a mut [u32],
+
+    width: usize,
+    height: usize,
+
+    // TODO use those
+    _stride: usize,
+    _size: usize,
+}
+
+struct KeyboardInput(File);
+
+pub struct DisplaySurface<'a, 'd> {
+    display: &'a mut Display<'d>,
+}
+
+impl DisplaySurface<'_, '_> {
+    pub fn width(&self) -> usize {
+        self.display.width
+    }
+
+    pub fn height(&self) -> usize {
+        self.display.height
+    }
+
+    pub fn blit_buffer(
+        mut self,
+        source: &[u32],
+        dst: Point<usize>,
+        src: Point<usize>,
+        w: usize,
+        h: usize,
+        src_stride: usize,
+    ) {
+        let src_w = (self.display.width - src.0).min(w);
+        let dst_w = (self.display.width - dst.0).min(w);
+        let src_h = (self.display.height - src.1).min(h);
+        let dst_h = (self.display.height - dst.1).min(h);
+        let w = cmp::min(src_w, dst_w);
+        let h = cmp::min(src_h, dst_h);
+
+        for y in 0..h {
+            let dst_offset = (y + src.1 + dst.1) * self.display.width + dst.0 + src.0;
+            let src_offset = (y + src.1) * src_stride + src.0;
+
+            let src_chunk = &source[src_offset..src_offset + w];
+            let dst_chunk = &mut self[dst_offset..dst_offset + w];
+
+            dst_chunk.copy_from_slice(src_chunk);
+        }
+
+        self.present();
+    }
+
+    pub fn present(self) {
+        self.display.flush();
+    }
+}
+
+impl Deref for DisplaySurface<'_, '_> {
+    type Target = [u32];
+
+    fn deref(&self) -> &Self::Target {
+        self.display.data
+    }
+}
+
+impl DerefMut for DisplaySurface<'_, '_> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.display.data
+    }
+}
+
+#[derive(Debug, Parser)]
+struct Args {
+    framebuffer: PathBuf,
+}
+
+impl<S: WindowServer> Backend<'_, S> {
+    pub fn new(server: S) -> Result<Self, Error> {
+        let args = Args::parse();
+        let channel = Channel::bind(libcolors::CHANNEL_NAME)?;
+        let (tx, rx) = channel.split();
+        let tx = ServerSender { sender: tx };
+        let input = KeyboardInput::open()?;
+
+        let mut poll = Poll::new()?;
+        poll.add(&rx)?;
+        poll.add(&input)?;
+        let display = Display::open(args.framebuffer)?;
+
+        Ok(Self {
+            poll,
+            tx,
+            rx,
+
+            display,
+            input,
+
+            server,
+        })
+    }
+
+    pub fn run(mut self) -> Result<(), Error> {
+        self.server.handle_initial(DisplaySurface {
+            display: &mut self.display
+        });
+
+        loop {
+            let fd = self.poll.wait(None)?.unwrap();
+
+            if fd == self.rx.as_raw_fd() {
+                let (message, file, peer) = self.rx.receive_with_file_from()?;
+                let event = FromClient {
+                    message,
+                    file,
+                    peer,
+                };
+                let surface = DisplaySurface {
+                    display: &mut self.display,
+                };
+                self.server
+                    .handle_client_message(surface, &mut self.tx, event);
+            } else if fd == self.input.as_raw_fd() {
+                let event = self.input.read_event()?;
+                if let Some(event) = convert_key_event(event) {
+                    let surface = DisplaySurface {
+                        display: &mut self.display,
+                    };
+                    self.server.handle_keyboard_event(surface, &mut self.tx, event);
+                }
+            }
+        }
+    }
+}
+
+impl Display<'_> {
+    pub fn open(framebuffer: impl AsRef<Path>) -> Result<Self, Error> {
+        let framebuffer = framebuffer.as_ref();
+        let file = OpenOptions::new().open(framebuffer)?;
+
+        let mut buffer = [0; 128];
+        device::device_request::<device::AcquireDevice>(file.as_raw_fd(), &mut buffer, &())
+            .map_err(std::io::Error::from)?;
+        let framebuffer = device::device_request::<device::GetActiveFramebuffer>(
+            file.as_raw_fd(),
+            &mut buffer,
+            &(),
+        )
+        .map_err(std::io::Error::from)?;
+
+        let width = framebuffer.width as usize;
+        let height = framebuffer.height as usize;
+
+        let mut mapping = FileMapping::map(file, framebuffer.size)?;
+        let data = unsafe {
+            std::slice::from_raw_parts_mut(mapping.as_mut_ptr() as *mut u32, width * height)
+        };
+
+        Ok(Self {
+            mapping,
+            data,
+
+            width,
+            height,
+            _stride: framebuffer.stride / size_of::<u32>(),
+            _size: framebuffer.size / size_of::<u32>(),
+        })
+    }
+
+    pub fn flush(&mut self) {
+        let mut buffer = [0; 0];
+        device::device_request::<device::FlushDisplay>(self.mapping.as_raw_fd(), &mut buffer, &())
+            .ok();
+    }
+}
+
+impl KeyboardInput {
+    pub fn open() -> Result<Self, Error> {
+        let file = File::open("/dev/kbd")?;
+        Ok(Self(file))
+    }
+
+    pub fn read_event(&mut self) -> Result<yggdrasil_abi::io::KeyboardKeyEvent, Error> {
+        let mut buf = [0; 4];
+        let len = self.0.read(&mut buf)?;
+
+        if len == 4 {
+            Ok(yggdrasil_abi::io::KeyboardKeyEvent::from_bytes(buf))
+        } else {
+            todo!()
+        }
+    }
+}
+
+impl AsRawFd for KeyboardInput {
+    fn as_raw_fd(&self) -> RawFd {
+        self.0.as_raw_fd()
+    }
+}
+
+fn convert_key_event(raw: yggdrasil_abi::io::KeyboardKeyEvent) -> Option<KeyboardKeyEvent> {
+    let (key, state) = raw.split();
+
+    let key = match key {
+        KeyboardKey::Char(ch) => Key::Char(ch),
+        KeyboardKey::Backspace => Key::Backspace,
+        KeyboardKey::Enter => Key::Enter,
+        KeyboardKey::Home => Key::Home,
+        KeyboardKey::End => Key::End,
+        KeyboardKey::PageUp => Key::PageUp,
+        KeyboardKey::PageDown => Key::PageDown,
+        KeyboardKey::Escape => Key::Escape,
+        KeyboardKey::Up => Key::Up,
+        KeyboardKey::Down => Key::Down,
+        KeyboardKey::Left => Key::Left,
+        KeyboardKey::Right => Key::Right,
+        KeyboardKey::LAlt => Key::LAlt,
+        KeyboardKey::RAlt => Key::RAlt,
+        KeyboardKey::LShift => Key::LShift,
+        KeyboardKey::RShift => Key::RShift,
+        KeyboardKey::LControl => Key::LControl,
+        KeyboardKey::RControl => Key::RControl,
+        KeyboardKey::Insert => return None,
+        KeyboardKey::Delete => return None,
+        KeyboardKey::Unknown => return None,
+        KeyboardKey::CapsLock => return None,
+        KeyboardKey::Tab => return None,
+        KeyboardKey::F(_) => return None,
+    };
+
+    Some(KeyboardKeyEvent {
+        key,
+        state
+    })
+}
diff --git a/userspace/colors/src/window.rs b/userspace/colors/src/window.rs
new file mode 100644
index 00000000..6a2c0854
--- /dev/null
+++ b/userspace/colors/src/window.rs
@@ -0,0 +1,23 @@
+use cross::mem::FileMapping;
+use uipc::PeerAddress;
+
+pub struct Window<'d> {
+    pub wid: u32,
+    pub peer: PeerAddress,
+
+    pub surface_mapping: FileMapping,
+    pub surface_data: &'d [u32],
+}
+
+impl Window<'_> {
+    pub fn resize(&mut self, w: u32, h: u32) {
+        let new_surface_data = unsafe {
+            std::slice::from_raw_parts_mut(
+                self.surface_mapping.as_mut_ptr() as *mut u32,
+                (w * h) as usize,
+            )
+        };
+
+        self.surface_data = new_surface_data;
+    }
+}
diff --git a/userspace/colors/src/wm/mod.rs b/userspace/colors/src/wm/mod.rs
new file mode 100644
index 00000000..6e0bf37a
--- /dev/null
+++ b/userspace/colors/src/wm/mod.rs
@@ -0,0 +1,691 @@
+use std::{cell::Cell, collections::HashMap, hash::Hash, iter};
+
+pub type NodeId = u32;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Direction {
+    Up,
+    Down,
+    Left,
+    Right,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Rect {
+    pub x: u32,
+    pub y: u32,
+    pub w: u32,
+    pub h: u32,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Orientation {
+    Horizontal,
+    Vertical,
+}
+
+pub enum NodeContent<T> {
+    Container(ContainerNode),
+    Window(WindowNode<T>),
+}
+
+pub struct NodeLayout {
+    rect: Cell<(Option<Rect>, bool)>,
+}
+
+pub struct Node<T> {
+    parent: Option<NodeId>,
+    layout: NodeLayout,
+
+    content: NodeContent<T>,
+}
+
+pub struct ContainerNode {
+    children: Vec<NodeId>,
+    orientation: Orientation,
+}
+
+pub struct WindowNode<T> {
+    pub wid: T,
+}
+
+pub struct Reservation<T> {
+    layout: NodeLayout,
+    size: u32,
+    wid: T,
+}
+
+pub struct Workspace<T> {
+    nodes: HashMap<NodeId, Node<T>>,
+    wid_to_nid: HashMap<T, NodeId>,
+    last_node_id: NodeId,
+
+    root: NodeId,
+    focus: Option<NodeId>,
+
+    margin: u32,
+    spacing: u32,
+
+    reservations_top: Vec<Reservation<T>>,
+    reservation_top: u32,
+
+    width: u32,
+    height: u32,
+}
+
+impl<T> Node<T> {
+    pub fn as_window(&self) -> Option<&WindowNode<T>> {
+        match &self.content {
+            NodeContent::Window(window) => Some(window),
+            _ => None,
+        }
+    }
+    pub fn as_container(&self) -> Option<&ContainerNode> {
+        match &self.content {
+            NodeContent::Container(container) => Some(container),
+            _ => None,
+        }
+    }
+    pub fn as_container_mut(&mut self) -> Option<&mut ContainerNode> {
+        match &mut self.content {
+            NodeContent::Container(container) => Some(container),
+            _ => None,
+        }
+    }
+}
+
+impl NodeLayout {
+    pub fn set(&self, new: Rect) {
+        let (old, _) = self.rect.get();
+        let dirty = old.map_or(true, |old| old != new);
+        self.rect.set((Some(new), dirty));
+    }
+
+    pub fn get(&self) -> Option<Rect> {
+        let (value, _) = self.rect.get();
+        value
+    }
+
+    pub fn clear(&self) {
+        self.rect.set((None, false));
+    }
+
+    pub fn clear_dirty(&self) -> bool {
+        let (value, dirty) = self.rect.get();
+        self.rect.set((value, false));
+        dirty && value.is_some()
+    }
+}
+
+impl<T: Eq + Hash + Copy> Workspace<T> {
+    pub fn new(width: u32, height: u32) -> Self {
+        let root = Node {
+            parent: None,
+            layout: NodeLayout {
+                rect: Cell::new((None, false)),
+            },
+            content: NodeContent::Container(ContainerNode {
+                children: vec![],
+                orientation: Orientation::Horizontal,
+            }),
+        };
+        let nodes = HashMap::from_iter([(0, root)]);
+
+        let mut this = Self {
+            nodes,
+            wid_to_nid: HashMap::new(),
+            last_node_id: 0,
+
+            margin: 4,
+            spacing: 4,
+
+            root: 0,
+            focus: None,
+
+            reservation_top: 0,
+            reservations_top: Vec::new(),
+
+            width,
+            height,
+        };
+
+        this.update_layout();
+
+        this
+    }
+
+    pub fn add_reservation(&mut self, wid: T, height: u32) {
+        self.reservations_top.push(Reservation {
+            layout: NodeLayout {
+                rect: Cell::new((None, false)),
+            },
+            size: height,
+            wid,
+        });
+        self.update_layout();
+    }
+
+    pub fn all_windows(&self) -> impl Iterator<Item = (T, &NodeLayout)> + '_ {
+        let nodes_windows = self.nodes.iter().filter_map(|(_, node)| {
+            let window = node.as_window()?;
+            Some((window.wid, &node.layout))
+            // Some(node.as_window()?.wid)
+        });
+        let reservation_windows = self
+            .reservations_top
+            .iter()
+            .map(|res| (res.wid, &res.layout));
+        iter::chain(reservation_windows, nodes_windows)
+    }
+
+    pub fn dirty_windows(&self) -> impl Iterator<Item = (T, Rect)> + '_ {
+        self.all_windows().filter_map(|(wid, layout)| {
+            if !layout.clear_dirty() {
+                return None;
+            }
+            let rect = layout.get()?;
+            Some((wid, rect))
+        })
+    }
+
+    pub fn window_towards_wrap(&self, direction: Direction) -> Option<T> {
+        let current_nid = self.focus?;
+        let current_node = self.nodes.get(&current_nid)?;
+        let parent_nid = current_node.parent?;
+        let parent_node = self.nodes.get(&parent_nid)?;
+        let parents_parent_nid = parent_node.parent;
+        let parent_container = parent_node.as_container()?;
+
+        let (orientation, delta) = direction.split();
+
+        let nid = if orientation == parent_container.orientation {
+            log::info!("Within parent {delta}, {:?}", parent_container.orientation);
+            let position_in_parent = parent_container
+                .children
+                .iter()
+                .position(|&n| n == current_nid)?;
+
+            if delta > 0 {
+                if position_in_parent < parent_container.children.len() - 1 {
+                    parent_container.children[position_in_parent + 1]
+                } else {
+                    parent_container.children[0]
+                }
+            } else {
+                if position_in_parent > 0 {
+                    parent_container.children[position_in_parent - 1]
+                } else {
+                    parent_container.children[parent_container.children.len() - 1]
+                }
+            }
+        } else if let Some(parents_parent_nid) = parents_parent_nid {
+            let parents_parent_node = self.nodes.get(&parents_parent_nid)?;
+            let parents_parent_container = parents_parent_node.as_container()?;
+            assert_eq!(parents_parent_container.orientation, orientation);
+            let position_in_parent = parents_parent_container
+                .children
+                .iter()
+                .position(|&n| n == parent_nid)?;
+            log::info!(
+                "Within parent's parent {delta}, {:?}",
+                parents_parent_container.orientation
+            );
+            log::info!("position_in_parent = {position_in_parent}");
+
+            if delta > 0 {
+                if position_in_parent < parent_container.children.len() - 1 {
+                    parents_parent_container.children[position_in_parent + 1]
+                } else {
+                    parents_parent_container.children[0]
+                }
+            } else {
+                if position_in_parent > 0 {
+                    parents_parent_container.children[position_in_parent - 1]
+                } else {
+                    parents_parent_container.children[parent_container.children.len() - 1]
+                }
+            }
+        } else {
+            return None;
+        };
+
+        self.first_window_in(nid)
+    }
+
+    pub fn first_window_in(&self, nid: NodeId) -> Option<T> {
+        let node = self.nodes.get(&nid)?;
+
+        match &node.content {
+            NodeContent::Window(window) => Some(window.wid),
+            NodeContent::Container(container) => {
+                let first = *container.children.first()?;
+                self.first_window_in(first)
+            }
+        }
+    }
+
+    pub fn move_window(&mut self, direction: Direction) -> bool
+    where
+        T: core::fmt::Debug,
+    {
+        if let Some(container_nid) = self.move_window_inner(direction) {
+            self.invalidate_layout(container_nid);
+            self.fixup_containers(self.root);
+            self.dump(self.root, 0);
+            true
+        } else {
+            false
+        }
+    }
+
+    fn dump(&self, nid: NodeId, depth: usize)
+    where
+        T: core::fmt::Debug,
+    {
+        let Some(node) = self.nodes.get(&nid) else {
+            return;
+        };
+
+        let rect = node.layout.get();
+
+        match &node.content {
+            NodeContent::Window(window) => {
+                log::info!(
+                    "{:depth$}Window #{:?} {rect:?}",
+                    "",
+                    window.wid,
+                    depth = depth * 2
+                );
+            }
+            NodeContent::Container(container) => {
+                log::info!("{:depth$}Container {rect:?} {{", "", depth = depth * 2);
+                for &child_nid in container.children.iter() {
+                    self.dump(child_nid, depth + 1);
+                }
+                log::info!("{:depth$}}}", "", depth = depth * 2);
+            }
+        }
+    }
+
+    // pub fn walk_windows<F: FnMut(T)>(&self, mut mapper: F) {
+    //     self.walk_windows_inner(&mut mapper, self.root)
+    // }
+
+    // fn walk_windows_inner<F: FnMut(T)>(&self, mapper: &mut F, nid: NodeId) {
+    //     // let Some(node) = self.nodes.get(&nid) else {
+    //     //     return;
+    //     // };
+
+    //     // match &node.content {
+    //     //     NodeContent::Container(container) => {
+    //     //         for &child_nid in container.children.iter() {
+    //     //             self.walk_windows_inner(mapper, child_nid);
+    //     //         }
+    //     //     }
+    //     //     NodeContent::Window(window) => {
+    //     //         mapper(window.wid);
+    //     //     }
+    //     // }
+    // }
+
+    fn invalidate_layout(&mut self, nid: NodeId) {
+        for reservation in self.reservations_top.iter() {
+            reservation.layout.clear();
+        }
+        self.invalidate_layout_inner(nid);
+        self.update_layout();
+    }
+
+    fn invalidate_layout_inner(&self, nid: NodeId) {
+        let Some(node) = self.nodes.get(&nid) else {
+            return;
+        };
+
+        node.layout.clear();
+
+        match &node.content {
+            NodeContent::Container(container) => {
+                for &child_nid in container.children.iter() {
+                    self.invalidate_layout_inner(child_nid);
+                }
+            }
+            _ => (),
+        }
+    }
+
+    fn fixup_containers(&mut self, nid: NodeId) {
+        let Some(node) = self.nodes.get_mut(&nid) else {
+            return;
+        };
+
+        if let NodeContent::Container(container) = &mut node.content {
+            if container.children.is_empty() {
+                let Some(parent_nid) = node.parent else {
+                    return;
+                };
+                // Remove the empty container
+                if let Some(parent_container) = self
+                    .nodes
+                    .get_mut(&parent_nid)
+                    .and_then(Node::as_container_mut)
+                {
+                    parent_container.children.retain(|&n| n != nid);
+                    self.nodes.remove(&nid);
+                    self.invalidate_layout(parent_nid);
+                }
+            } else if container.children.len() == 1 {
+                // Remove the empty container and reparent its only child
+                if let Some(parent_nid) = node.parent {
+                    let child_nid = container.children.remove(0);
+                    let [Some(child), Some(parent)] =
+                        self.nodes.get_many_mut([&child_nid, &parent_nid])
+                    else {
+                        return;
+                    };
+
+                    child.parent = Some(parent_nid);
+                    if let Some(parent_container) = parent.as_container_mut() {
+                        parent_container.children.retain(|&n| n != nid);
+                        parent_container.children.push(child_nid);
+                    }
+
+                    self.invalidate_layout(parent_nid);
+                }
+            } else {
+                // TODO borrowing stuff
+                let children = container.children.clone();
+                for child_nid in children {
+                    self.fixup_containers(child_nid);
+                }
+            }
+        }
+    }
+
+    fn move_window_inner(&mut self, direction: Direction) -> Option<NodeId> {
+        let current_nid = self.focus?;
+        let current_node = self.nodes.get(&current_nid)?;
+        let parent_nid = current_node.parent?;
+        let parent_node = self.nodes.get_mut(&parent_nid)?;
+        let parents_parent_nid = parent_node.parent;
+        let parent_container = parent_node.as_container_mut()?;
+
+        let (orientation, delta) = direction.split();
+
+        if parent_container.orientation == orientation {
+            let position_in_parent = parent_container
+                .children
+                .iter()
+                .position(|&n| n == current_nid)?;
+
+            // TODO check if this item is also a container
+
+            if delta < 0 && position_in_parent > 0 {
+                parent_container
+                    .children
+                    .swap(position_in_parent - 1, position_in_parent);
+                Some(parent_nid)
+            } else if delta > 0 && position_in_parent < parent_container.children.len() - 1 {
+                parent_container
+                    .children
+                    .swap(position_in_parent, position_in_parent + 1);
+                Some(parent_nid)
+            } else {
+                None
+            }
+        } else if let Some(parents_parent_nid) = parents_parent_nid {
+            // Go to parent's parent
+            let parents_parent_node = self.nodes.get_mut(&parents_parent_nid)?;
+            let parents_parent_container = parents_parent_node.as_container_mut()?;
+            assert_eq!(parents_parent_container.orientation, orientation);
+            todo!()
+        } else {
+            self.last_node_id += 1;
+            let new_root_nid = self.last_node_id;
+
+            // Remove child from parent
+            parent_container.children.retain(|&n| n != current_nid);
+
+            self.nodes.get_mut(&current_nid)?.parent = Some(new_root_nid);
+            self.nodes.get_mut(&self.root)?.parent = Some(new_root_nid);
+
+            let children = if delta > 0 {
+                vec![self.root, current_nid]
+            } else {
+                vec![current_nid, self.root]
+            };
+
+            // Create a new root
+            let new_root = Node {
+                parent: None,
+                layout: NodeLayout {
+                    rect: Cell::new((None, false)),
+                },
+                content: NodeContent::Container(ContainerNode {
+                    children,
+                    orientation,
+                }),
+            };
+            self.root = new_root_nid;
+            self.nodes.insert(new_root_nid, new_root);
+
+            Some(self.root)
+        }
+    }
+
+    pub fn focus_window(&mut self, wid: T) -> (Option<T>, Option<NodeId>) {
+        let old = self.focus.take().and_then(|nid| self.wid(nid));
+        let new = self.wid_to_nid.get(&wid).copied();
+        self.focus = new;
+        (old, new)
+    }
+
+    pub fn focused_window(&self) -> Option<T> {
+        let nid = self.focus?;
+        Some(self.nodes.get(&nid)?.as_window()?.wid)
+    }
+
+    pub fn window(&self, wid: T) -> Option<&Node<T>> {
+        let nid = *self.wid_to_nid.get(&wid)?;
+        self.nodes.get(&nid)
+    }
+
+    pub fn reservation(&self, wid: T) -> Option<&Reservation<T>> {
+        self.reservations_top.iter().find(|r| r.wid == wid)
+    }
+
+    pub fn window_frame(&self, wid: T) -> Option<Rect> {
+        if let Some(reservation) = self.reservation(wid) {
+            reservation.layout.get()
+        } else {
+            let node = self.window(wid)?;
+            node.layout.get()
+        }
+    }
+
+    pub fn resize(&mut self, w: u32, h: u32) {
+        self.width = w;
+        self.height = h;
+        self.update_layout();
+    }
+
+    pub fn update_layout(&mut self) {
+        let mut res_y = 0;
+        for reservation in self.reservations_top.iter() {
+            reservation.layout.set(Rect {
+                x: 0,
+                y: res_y,
+                w: self.width,
+                h: reservation.size,
+            });
+            res_y += reservation.size;
+        }
+        self.reservation_top = res_y;
+
+        self.update_layout_for(
+            self.root,
+            Rect {
+                x: self.margin,
+                y: self.margin + res_y,
+                w: self.width - self.margin * 2,
+                h: self.height - self.margin * 2 - res_y,
+            },
+        );
+    }
+
+    pub fn create_window(&mut self, wid: T) -> bool {
+        self.create_window_in(self.root, wid)
+    }
+
+    pub fn remove_window(&mut self, wid: T) -> bool {
+        let Some(nid) = self.wid_to_nid.remove(&wid) else {
+            return false;
+        };
+        if self.remove_inner(nid) {
+            self.fixup_containers(self.root);
+            self.update_layout();
+            true
+        } else {
+            false
+        }
+    }
+
+    fn create_window_in(&mut self, container_nid: NodeId, wid: T) -> bool {
+        self.last_node_id += 1;
+        let nid = self.last_node_id;
+        self.nodes.insert(
+            nid,
+            Node {
+                parent: Some(container_nid),
+                layout: NodeLayout {
+                    rect: Cell::new((None, false)),
+                },
+                content: NodeContent::Window(WindowNode { wid }),
+            },
+        );
+        self.wid_to_nid.insert(wid, nid);
+        if !self.add_child_to(container_nid, nid) {
+            self.wid_to_nid.remove(&wid);
+            self.nodes.remove(&nid);
+            return false;
+        }
+        self.update_layout();
+        true
+    }
+
+    fn wid(&self, nid: NodeId) -> Option<T> {
+        self.nodes.get(&nid).and_then(|node| match &node.content {
+            NodeContent::Window(window) => Some(window.wid),
+            _ => None,
+        })
+    }
+
+    fn update_layout_for(&self, node_id: NodeId, rect: Rect) {
+        let Some(node) = self.nodes.get(&node_id) else {
+            log::warn!("update_layout_for: no node {node_id}");
+            return;
+        };
+
+        node.layout.set(rect);
+
+        match &node.content {
+            NodeContent::Container(container) => {
+                if container.children.is_empty() {
+                    return;
+                }
+
+                match container.orientation {
+                    Orientation::Vertical => {
+                        let hstep = (rect.h - self.spacing * container.children.len() as u32)
+                            / container.children.len() as u32;
+                        let ystep = hstep + self.spacing;
+
+                        for (i, &child_nid) in container.children.iter().enumerate() {
+                            let i = i as u32;
+                            let child_rect = Rect {
+                                x: rect.x,
+                                y: rect.y + i * ystep,
+                                w: rect.w,
+                                h: hstep,
+                            };
+                            log::info!("Frame #{child_nid} size: {child_rect:?}");
+                            self.update_layout_for(child_nid, child_rect);
+                        }
+                    }
+                    Orientation::Horizontal => {
+                        let wstep = (rect.w - self.spacing * container.children.len() as u32)
+                            / container.children.len() as u32;
+                        let xstep = wstep + self.spacing;
+
+                        for (i, &child_nid) in container.children.iter().enumerate() {
+                            let i = i as u32;
+                            let child_rect = Rect {
+                                x: rect.x + i * xstep,
+                                y: rect.y,
+                                w: wstep,
+                                h: rect.h,
+                            };
+                            log::info!("Frame #{child_nid} size: {child_rect:?}");
+                            self.update_layout_for(child_nid, child_rect);
+                        }
+                    }
+                }
+            }
+            _ => (),
+        }
+    }
+
+    fn remove_inner(&mut self, node_id: NodeId) -> bool {
+        if node_id == self.root {
+            return false;
+        }
+        let Some(node) = self.nodes.remove(&node_id) else {
+            return false;
+        };
+        let container_nid = unsafe { node.parent.unwrap_unchecked() };
+        let Some(container_node) = self.nodes.get_mut(&container_nid) else {
+            return false;
+        };
+        let NodeContent::Container(container) = &mut container_node.content else {
+            return false;
+        };
+        container_node.layout.clear();
+        container.children.retain(|&c| c != node_id);
+        // Only root container can be empty
+        debug_assert!(!(container_node.parent.is_some() && container.children.is_empty()));
+        if container.children.len() == 1 && container_node.parent.is_some() {
+            // Lift the remaining node to the parent container
+            todo!()
+        }
+
+        true
+    }
+
+    fn add_child_to(&mut self, container_nid: NodeId, child_nid: NodeId) -> bool {
+        let [Some(container_node), Some(child_node)] =
+            self.nodes.get_many_mut([&container_nid, &child_nid])
+        else {
+            return false;
+        };
+        let NodeContent::Container(container) = &mut container_node.content else {
+            return false;
+        };
+
+        container_node.layout.clear();
+        child_node.layout.clear();
+        child_node.parent = Some(container_nid);
+        container.children.push(child_nid);
+
+        true
+    }
+}
+
+impl Direction {
+    pub fn split(&self) -> (Orientation, isize) {
+        match self {
+            Self::Left => (Orientation::Horizontal, -1),
+            Self::Right => (Orientation::Horizontal, 1),
+            Self::Up => (Orientation::Vertical, -1),
+            Self::Down => (Orientation::Vertical, 1),
+        }
+    }
+}
diff --git a/userspace/lib/cross/src/lib.rs b/userspace/lib/cross/src/lib.rs
index 64a26729..8186fa50 100644
--- a/userspace/lib/cross/src/lib.rs
+++ b/userspace/lib/cross/src/lib.rs
@@ -6,3 +6,4 @@ pub(crate) mod sys;
 pub mod io;
 pub mod net;
 pub mod mem;
+pub mod signal;
diff --git a/userspace/lib/cross/src/signal.rs b/userspace/lib/cross/src/signal.rs
new file mode 100644
index 00000000..9a67e22a
--- /dev/null
+++ b/userspace/lib/cross/src/signal.rs
@@ -0,0 +1,6 @@
+use crate::sys;
+
+
+pub fn set_sigint_handler(handler: fn()) {
+    sys::set_sigint_handler(handler);
+}
diff --git a/userspace/lib/cross/src/sys/unix/mem.rs b/userspace/lib/cross/src/sys/unix/mem.rs
index 4f021d03..76316a5e 100644
--- a/userspace/lib/cross/src/sys/unix/mem.rs
+++ b/userspace/lib/cross/src/sys/unix/mem.rs
@@ -1,39 +1,75 @@
 use std::{
+    ffi::c_void,
     io,
     ops::{Deref, DerefMut},
-    os::fd::{AsRawFd, OwnedFd, RawFd},
+    os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
+    ptr::null_mut,
 };
 
 use crate::sys;
 
-pub struct SharedMemoryImpl {}
+pub struct SharedMemoryImpl {
+    fd: OwnedFd,
+    size: usize,
+}
 
-pub struct FileMappingImpl {}
+pub struct FileMappingImpl {
+    fd: OwnedFd,
+    pointer: *mut c_void,
+    size: usize,
+}
 
 impl sys::SharedMemory for SharedMemoryImpl {
     type Mapping = FileMappingImpl;
 
     fn map(self) -> io::Result<Self::Mapping> {
-        todo!()
+        <FileMappingImpl as sys::FileMapping>::map(self.fd, self.size)
     }
 
     fn new(size: usize) -> io::Result<Self> {
-        let _ = size;
-        todo!()
+        let fd = unsafe { libc::memfd_create(c"cross-shm".as_ptr(), libc::MFD_CLOEXEC) };
+        if fd < 0 {
+            return Err(io::Error::last_os_error());
+        }
+        let fd = unsafe { OwnedFd::from_raw_fd(fd) };
+        if unsafe { libc::ftruncate(fd.as_raw_fd(), size as i64) } != 0 {
+            return Err(io::Error::last_os_error());
+        }
+        Ok(Self { fd, size })
     }
 }
 
 impl AsRawFd for SharedMemoryImpl {
     fn as_raw_fd(&self) -> RawFd {
-        todo!()
+        self.fd.as_raw_fd()
     }
 }
 
 impl sys::FileMapping for FileMappingImpl {
     fn map<F: Into<OwnedFd>>(file: F, size: usize) -> io::Result<Self> {
-        let _ = file;
-        let _ = size;
-        todo!()
+        let fd: OwnedFd = file.into();
+        let pointer = unsafe {
+            libc::mmap(
+                null_mut(),
+                size,
+                libc::PROT_READ | libc::PROT_WRITE,
+                libc::MAP_SHARED,
+                fd.as_raw_fd(),
+                0,
+            )
+        };
+        if pointer == libc::MAP_FAILED {
+            return Err(io::Error::last_os_error());
+        }
+        Ok(Self { fd, pointer, size })
+    }
+}
+
+impl Drop for FileMappingImpl {
+    fn drop(&mut self) {
+        unsafe {
+            libc::munmap(self.pointer, self.size);
+        }
     }
 }
 
@@ -41,18 +77,18 @@ impl Deref for FileMappingImpl {
     type Target = [u8];
 
     fn deref(&self) -> &Self::Target {
-        todo!()
+        unsafe { core::slice::from_raw_parts(self.pointer.cast(), self.size) }
     }
 }
 
 impl DerefMut for FileMappingImpl {
     fn deref_mut(&mut self) -> &mut Self::Target {
-        todo!()
+        unsafe { core::slice::from_raw_parts_mut(self.pointer.cast(), self.size) }
     }
 }
 
 impl AsRawFd for FileMappingImpl {
     fn as_raw_fd(&self) -> RawFd {
-        todo!()
+        self.fd.as_raw_fd()
     }
 }
diff --git a/userspace/lib/cross/src/sys/unix/mod.rs b/userspace/lib/cross/src/sys/unix/mod.rs
index 10df7962..0327df77 100644
--- a/userspace/lib/cross/src/sys/unix/mod.rs
+++ b/userspace/lib/cross/src/sys/unix/mod.rs
@@ -6,6 +6,8 @@ pub mod socket;
 pub mod term;
 pub mod timer;
 
+use std::{ffi::c_int, sync::Mutex};
+
 pub use mem::{FileMappingImpl, SharedMemoryImpl};
 pub use pid::PidFdImpl;
 pub use pipe::PipeImpl;
@@ -13,3 +15,17 @@ pub use poll::PollImpl;
 pub use socket::{BorrowedAddressImpl, LocalPacketSocketImpl, OwnedAddressImpl};
 pub use term::RawStdinImpl;
 pub use timer::TimerFdImpl;
+
+fn dummy_sigint() {}
+
+static SIGINT_HANDLER: Mutex<fn()> = Mutex::new(dummy_sigint);
+
+extern "C" fn sigint_proxy(_: c_int) {
+    let handler = *SIGINT_HANDLER.lock().unwrap();
+    handler();
+}
+
+pub fn set_sigint_handler(handler: fn()) {
+    *SIGINT_HANDLER.lock().unwrap() = handler;
+    unsafe { libc::signal(libc::SIGINT, sigint_proxy as usize) };
+}
diff --git a/userspace/lib/cross/src/sys/yggdrasil/mod.rs b/userspace/lib/cross/src/sys/yggdrasil/mod.rs
index 59974690..fb5e0a62 100644
--- a/userspace/lib/cross/src/sys/yggdrasil/mod.rs
+++ b/userspace/lib/cross/src/sys/yggdrasil/mod.rs
@@ -13,3 +13,6 @@ pub use pipe::PipeImpl;
 pub use term::RawStdinImpl;
 pub use socket::{LocalPacketSocketImpl, OwnedAddressImpl, BorrowedAddressImpl};
 pub use mem::{SharedMemoryImpl, FileMappingImpl};
+
+pub fn set_sigint_handler(_handler: fn()) {
+}
diff --git a/userspace/lib/libcolors/Cargo.toml b/userspace/lib/libcolors/Cargo.toml
index 0fc924e2..ef8fb018 100644
--- a/userspace/lib/libcolors/Cargo.toml
+++ b/userspace/lib/libcolors/Cargo.toml
@@ -4,6 +4,14 @@ version = "0.1.0"
 edition = "2021"
 authors = ["Mark Poliakov <mark@alnyan.me>"]
 
+[[example]]
+name = "bar"
+required-features = ["client", "client_raqote"]
+
+[[example]]
+name = "window"
+required-features = ["client", "client_raqote"]
+
 [dependencies]
 cross.workspace = true
 uipc.workspace = true
@@ -11,10 +19,14 @@ yggdrasil-abi.workspace = true
 
 serde.workspace = true
 thiserror.workspace = true
+log.workspace = true
 
 # client_raqote
 raqote = { version = "0.8.3", default-features = false, optional = true }
 
+[dev-dependencies]
+raqote = { version = "0.8.3", default-features = false }
+
 [features]
 default = []
 client_raqote = ["client", "raqote"]
diff --git a/userspace/lib/libcolors/examples/bar.rs b/userspace/lib/libcolors/examples/bar.rs
new file mode 100644
index 00000000..86ddb5aa
--- /dev/null
+++ b/userspace/lib/libcolors/examples/bar.rs
@@ -0,0 +1,66 @@
+use std::{
+    env,
+    process::ExitCode,
+    sync::atomic::{AtomicBool, Ordering},
+};
+
+use libcolors::{
+    application::{
+        window::{EventOutcome, Window},
+        Application,
+    },
+    error::Error,
+    message::{CreateWindowInfo, WindowType},
+};
+use raqote::{Color, DrawTarget, Gradient, GradientStop, Point, SolidSource, Source, Spread};
+
+fn run_bar() -> Result<ExitCode, Error> {
+    let mut app = Application::new()?;
+    let mut window = Window::new_with_info(
+        &app,
+        CreateWindowInfo {
+            ty: WindowType::Reservation(32),
+        },
+    )?;
+
+    window.set_on_redraw_requested(|surface: &mut DrawTarget<&mut [u32]>| {
+        let source = Source::new_linear_gradient(
+            Gradient {
+                stops: vec![
+                    GradientStop {
+                        position: 0.0,
+                        color: Color::new(255, 255, 0, 0),
+                    },
+                    GradientStop {
+                        position: 1.0,
+                        color: Color::new(255, 255, 255, 0),
+                    },
+                ],
+            },
+            Point::new(0.0, 0.0),
+            Point::new(0.0, surface.height() as _),
+            Spread::Pad,
+        );
+        surface.fill_rect(
+            0.0,
+            0.0,
+            surface.width() as _,
+            surface.height() as _,
+            &source,
+            &Default::default(),
+        );
+    });
+
+    app.add_window(window);
+    Ok(app.run())
+}
+
+fn main() -> ExitCode {
+    match run_bar() {
+        Ok(code) => code,
+        Err(error) => {
+            eprintln!("{error}");
+            ExitCode::FAILURE
+        }
+    }
+}
diff --git a/userspace/lib/libcolors/examples/window.rs b/userspace/lib/libcolors/examples/window.rs
new file mode 100644
index 00000000..427fb183
--- /dev/null
+++ b/userspace/lib/libcolors/examples/window.rs
@@ -0,0 +1,61 @@
+use std::{
+    env,
+    process::ExitCode,
+    sync::atomic::{AtomicBool, Ordering},
+};
+
+use libcolors::{
+    application::{
+        window::{EventOutcome, Window},
+        Application,
+    },
+    error::Error, message::{CreateWindowInfo, WindowType},
+};
+use raqote::{DrawTarget, SolidSource};
+
+fn run_window() -> Result<ExitCode, Error> {
+    static FOCUSED: AtomicBool = AtomicBool::new(false);
+
+    println!("libcolors test application");
+    let mut app = Application::new()?;
+    let mut window = Window::new(&app)?;
+
+    window.set_on_redraw_requested(|surface: &mut DrawTarget<&mut [u32]>| {
+        let color = if FOCUSED.load(Ordering::Acquire) {
+            SolidSource::from_unpremultiplied_argb(255, 255, 0, 0)
+        } else {
+            SolidSource::from_unpremultiplied_argb(255, 0, 0, 255)
+        };
+        let border = 8;
+        let border_color = SolidSource::from_unpremultiplied_argb(255, 0, 255, 0);
+
+        let w = surface.width();
+        let h = surface.height();
+        surface.clear(border_color);
+        surface.fill_rect(
+            border as _,
+            border as _,
+            (w - border * 2) as _,
+            (h - border * 2) as _,
+            &color.into(),
+            &Default::default(),
+        );
+    });
+    window.set_on_focus_changed(|focused| {
+        FOCUSED.store(focused, Ordering::Release);
+        EventOutcome::Redraw
+    });
+
+    app.add_window(window);
+    Ok(app.run())
+}
+
+fn main() -> ExitCode {
+    match run_window() {
+        Ok(code) => code,
+        Err(error) => {
+            eprintln!("{error}");
+            ExitCode::FAILURE
+        }
+    }
+}
diff --git a/userspace/lib/libcolors/src/application/connection.rs b/userspace/lib/libcolors/src/application/connection.rs
index 018fd542..b3aff314 100644
--- a/userspace/lib/libcolors/src/application/connection.rs
+++ b/userspace/lib/libcolors/src/application/connection.rs
@@ -1,12 +1,10 @@
 use std::{
     collections::VecDeque,
-    os::{
-        fd::{AsRawFd, RawFd},
-        yggdrasil::io::poll::PollChannel,
-    },
+    os::fd::{AsRawFd, RawFd},
     time::Duration,
 };
 
+use cross::io::Poll;
 use uipc::{Channel, Receiver, Sender};
 
 use crate::{
@@ -19,7 +17,8 @@ pub struct Connection {
     sender: Sender<ClientMessage>,
     receiver: Receiver<ServerMessage>,
     event_queue: VecDeque<Event>,
-    poll: PollChannel,
+    poll: Poll,
+    // poll: PollChannel,
     timeout: Duration,
 }
 
@@ -28,10 +27,10 @@ impl Connection {
         let channel = Channel::connect(crate::CHANNEL_NAME)?;
         let (sender, receiver) = channel.split();
         let timeout = Duration::from_secs(1);
-        let mut poll = PollChannel::new()?;
+        let mut poll = Poll::new()?;
         let event_queue = VecDeque::new();
 
-        poll.add(receiver.as_raw_fd())?;
+        poll.add(&receiver)?;
 
         Ok(Self {
             sender,
@@ -51,7 +50,7 @@ impl Connection {
         predicate: F,
     ) -> Result<T, Error> {
         loop {
-            let Some((_, Ok(_))) = self.poll.wait(Some(self.timeout), true)? else {
+            let Some(_) = self.poll.wait(Some(self.timeout))? else {
                 return Err(Error::CommunicationTimeout);
             };
 
@@ -74,11 +73,8 @@ impl Connection {
         }
 
         loop {
-            match self.poll.wait(Some(self.timeout), true)? {
-                Some((_, Ok(_))) => (),
-                Some((_, Err(e))) => {
-                    todo!("Poll error: {e:?}")
-                }
+            match self.poll.wait(Some(self.timeout))? {
+                Some(_) => (),
                 None => continue,
             }
 
diff --git a/userspace/lib/libcolors/src/application/mod.rs b/userspace/lib/libcolors/src/application/mod.rs
index 834d62d5..95a3f133 100644
--- a/userspace/lib/libcolors/src/application/mod.rs
+++ b/userspace/lib/libcolors/src/application/mod.rs
@@ -1,9 +1,11 @@
 use std::{
     collections::BTreeMap,
     process::ExitCode,
-    sync::{Arc, Mutex},
+    sync::{atomic::{AtomicBool, Ordering}, Arc, Mutex},
 };
 
+use cross::signal::set_sigint_handler;
+
 use crate::{
     error::Error,
     event::{Event, EventData, WindowEvent},
@@ -20,8 +22,16 @@ pub struct Application<'a> {
     windows: BTreeMap<u32, Window<'a>>,
 }
 
+static EXIT_SIGNAL: AtomicBool = AtomicBool::new(false);
+
+fn sigint_handler() {
+    EXIT_SIGNAL.store(true, Ordering::Release);
+}
+
 impl<'a> Application<'a> {
     pub fn new() -> Result<Self, Error> {
+        set_sigint_handler(sigint_handler);
+
         let mut connection = Connection::new()?;
         connection.connect()?;
 
@@ -37,9 +47,10 @@ impl<'a> Application<'a> {
     }
 
     fn run_inner(mut self) -> Result<ExitCode, Error> {
-        loop {
+        while !EXIT_SIGNAL.load(Ordering::Acquire) {
             self.poll_events()?;
         }
+        Ok(ExitCode::SUCCESS)
     }
 
     pub fn handle_event(&mut self, event: Event) -> Result<(), Error> {
@@ -47,7 +58,7 @@ impl<'a> Application<'a> {
             if let Some(window) = self.windows.get_mut(&window_id) {
                 window.handle_event(ev)?;
             } else {
-                debug_trace!("Unexpected window_id received: {:?}", window_id);
+                log::warn!("Unknown window ID received: {window_id}");
             }
         }
 
@@ -74,8 +85,8 @@ impl<'a> Application<'a> {
     pub fn run(self) -> ExitCode {
         match self.run_inner() {
             Ok(exit) => exit,
-            Err(e) => {
-                debug_trace!("Application finished with error {:?}", e);
+            Err(error) => {
+                log::error!("Application finished with error: {error}");
                 ExitCode::FAILURE
             }
         }
diff --git a/userspace/lib/libcolors/src/application/window.rs b/userspace/lib/libcolors/src/application/window.rs
index 3f984e58..cf7af87c 100644
--- a/userspace/lib/libcolors/src/application/window.rs
+++ b/userspace/lib/libcolors/src/application/window.rs
@@ -5,7 +5,7 @@ use cross::mem::FileMapping;
 use crate::{
     error::Error,
     event::{EventData, KeyInput, WindowEvent},
-    message::ClientMessage,
+    message::{ClientMessage, CreateWindowInfo},
 };
 
 use super::{connection::Connection, Application};
@@ -15,7 +15,7 @@ pub trait OnKeyInput = Fn(KeyInput) -> EventOutcome;
 pub trait OnResized = Fn(u32, u32) -> EventOutcome;
 pub trait OnFocusChanged = Fn(bool) -> EventOutcome;
 
-#[cfg(feature = "client_raqote")]
+#[cfg(any(feature = "client_raqote", rust_analyzer))]
 pub trait OnRedrawRequested = Fn(&mut raqote::DrawTarget<&mut [u32]>);
 
 #[cfg(not(feature = "client_raqote"))]
@@ -32,7 +32,7 @@ pub struct Window<'a> {
     connection: Arc<Mutex<Connection>>,
     window_id: u32,
     surface_mapping: FileMapping,
-    #[cfg(feature = "client_raqote")]
+    #[cfg(any(feature = "client_raqote", rust_analyzer))]
     surface_draw_target: raqote::DrawTarget<&'a mut [u32]>,
     #[cfg(not(feature = "client_raqote"))]
     surface_data: &'a mut [u32],
@@ -47,11 +47,11 @@ pub struct Window<'a> {
     on_focus_changed: Box<dyn OnFocusChanged>,
 }
 
-impl Window<'_> {
-    pub fn new(application: &Application) -> Result<Self, Error> {
+impl<'a> Window<'a> {
+    pub fn new_with_info(application: &Application, info: CreateWindowInfo) -> Result<Self, Error> {
         let mut connection = application.connection.lock().unwrap();
 
-        connection.send(&ClientMessage::CreateWindow)?;
+        connection.send(&ClientMessage::CreateWindow(info))?;
 
         let (create_info, surface_shm_fd) = connection.filter_events(|r| match r.data {
             EventData::NewWindowInfo(info) => {
@@ -71,7 +71,7 @@ impl Window<'_> {
             )
         };
 
-        #[cfg(feature = "client_raqote")]
+        #[cfg(any(feature = "client_raqote", rust_analyzer))]
         let surface_draw_target = raqote::DrawTarget::from_backing(
             create_info.width as _,
             create_info.height as _,
@@ -84,7 +84,7 @@ impl Window<'_> {
             width: create_info.width,
             height: create_info.height,
             surface_mapping,
-            #[cfg(feature = "client_raqote")]
+            #[cfg(any(feature = "client_raqote", rust_analyzer))]
             surface_draw_target,
             #[cfg(not(feature = "client_raqote"))]
             surface_data,
@@ -95,8 +95,9 @@ impl Window<'_> {
                 // Do nothing
                 EventOutcome::Destroy
             }),
-            #[cfg(feature = "client_raqote")]
+            #[cfg(any(feature = "client_raqote", rust_analyzer))]
             on_redraw_requested: Box::new(|dt| {
+                use raqote::SolidSource;
                 dt.clear(SolidSource::from_unpremultiplied_argb(255, 127, 127, 127));
             }),
             #[cfg(not(feature = "client_raqote"))]
@@ -104,11 +105,15 @@ impl Window<'_> {
                 dt.fill(0xFF888888);
             }),
             on_key_input: Box::new(|_ev| EventOutcome::None),
-            on_resized: Box::new(|_w, _h| EventOutcome::None),
+            on_resized: Box::new(|_w, _h| EventOutcome::Redraw),
             on_focus_changed: Box::new(|_| EventOutcome::None),
         })
     }
 
+    pub fn new(application: &Application) -> Result<Self, Error> {
+        Self::new_with_info(application, Default::default())
+    }
+
     pub fn id(&self) -> u32 {
         self.window_id
     }
@@ -138,7 +143,7 @@ impl Window<'_> {
     }
 
     pub fn redraw(&mut self) -> Result<(), Error> {
-        #[cfg(feature = "client_raqote")]
+        #[cfg(any(feature = "client_raqote", rust_analyzer))]
         {
             let dt = &mut self.surface_draw_target;
             (self.on_redraw_requested)(dt);
@@ -159,6 +164,9 @@ impl Window<'_> {
                 EventOutcome::None
             }
             WindowEvent::Resized { width, height } => {
+                self.width = width;
+                self.height = height;
+
                 let new_surface_data = unsafe {
                     std::slice::from_raw_parts_mut(
                         self.surface_mapping.as_mut_ptr() as *mut u32,
@@ -166,7 +174,7 @@ impl Window<'_> {
                     )
                 };
 
-                #[cfg(feature = "client_raqote")]
+                #[cfg(any(feature = "client_raqote", rust_analyzer))]
                 {
                     let new_draw_target =
                         raqote::DrawTarget::from_backing(width as _, height as _, new_surface_data);
@@ -197,7 +205,7 @@ impl Window<'_> {
         Ok(outcome)
     }
 
-    #[cfg(feature = "client_raqote")]
+    #[cfg(any(feature = "client_raqote", rust_analyzer))]
     pub fn as_draw_target(&mut self) -> &mut raqote::DrawTarget<&'a mut [u32]> {
         &mut self.surface_draw_target
     }
diff --git a/userspace/lib/libcolors/src/event.rs b/userspace/lib/libcolors/src/event.rs
index f034bc51..84223f1c 100644
--- a/userspace/lib/libcolors/src/event.rs
+++ b/userspace/lib/libcolors/src/event.rs
@@ -1,9 +1,11 @@
 use std::os::fd::OwnedFd;
 
-pub use yggdrasil_abi::io::{KeyboardKey, KeyboardKeyEvent};
+// pub use yggdrasil_abi::io::{KeyboardKey, KeyboardKeyEvent};
 
 use serde::{Deserialize, Serialize};
 
+use crate::input::Key;
+
 #[derive(Debug, Serialize, Deserialize)]
 pub struct WindowInfo {
     pub window_id: u32,
@@ -23,16 +25,22 @@ pub struct KeyModifiers {
 #[derive(Debug, Serialize, Deserialize)]
 pub struct KeyEvent {
     pub modifiers: KeyModifiers,
-    pub key: KeyboardKey,
+    pub key: Key
 }
 
 #[derive(Debug, Serialize, Deserialize)]
 pub struct KeyInput {
     pub modifiers: KeyModifiers,
-    pub key: KeyboardKey,
+    pub key: Key,
     pub input: Option<char>,
 }
 
+#[derive(Debug, Serialize, Deserialize)]
+pub struct KeyboardKeyEvent {
+    pub key: Key,
+    pub state: bool
+}
+
 #[derive(Debug, Serialize, Deserialize)]
 pub enum WindowEvent {
     KeyPressed(KeyEvent),
@@ -90,6 +98,12 @@ impl KeyModifiers {
         alt: true,
     };
 
+    pub const ALT_SHIFT: Self = Self {
+        shift: true,
+        ctrl: false,
+        alt: true,
+    };
+
     pub const NONE: Self = Self {
         shift: false,
         ctrl: false,
diff --git a/userspace/lib/libcolors/src/input.rs b/userspace/lib/libcolors/src/input.rs
new file mode 100644
index 00000000..5f115e19
--- /dev/null
+++ b/userspace/lib/libcolors/src/input.rs
@@ -0,0 +1,24 @@
+use serde::{Deserialize, Serialize};
+
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum Key {
+    Char(u8),
+    LControl,
+    RControl,
+    LShift,
+    RShift,
+    LAlt,
+    RAlt,
+    PageUp,
+    PageDown,
+    Left,
+    Right,
+    Up,
+    Down,
+    Escape,
+    Enter,
+    Home,
+    End,
+    Backspace,
+}
diff --git a/userspace/lib/libcolors/src/lib.rs b/userspace/lib/libcolors/src/lib.rs
index 4788be90..3d555d5e 100644
--- a/userspace/lib/libcolors/src/lib.rs
+++ b/userspace/lib/libcolors/src/lib.rs
@@ -13,3 +13,4 @@ pub mod error;
 
 pub mod event;
 pub mod message;
+pub mod input;
diff --git a/userspace/lib/libcolors/src/message.rs b/userspace/lib/libcolors/src/message.rs
index bdeadab6..b9862257 100644
--- a/userspace/lib/libcolors/src/message.rs
+++ b/userspace/lib/libcolors/src/message.rs
@@ -7,10 +7,22 @@ pub enum ServerMessage {
     Event(EventData),
 }
 
+#[derive(Serialize, Deserialize, Debug, Default)]
+pub enum WindowType {
+    #[default]
+    Default,
+    Reservation(u32),
+}
+
+#[derive(Serialize, Deserialize, Debug, Default)]
+pub struct CreateWindowInfo {
+    pub ty: WindowType,
+}
+
 #[derive(Serialize, Deserialize, Debug)]
 pub enum ClientMessage {
     ClientHello,
-    CreateWindow,
+    CreateWindow(CreateWindowInfo),
     BlitWindow {
         window_id: u32,
         x: u32,
diff --git a/userspace/lib/logsink/Cargo.toml b/userspace/lib/logsink/Cargo.toml
new file mode 100644
index 00000000..b8618b53
--- /dev/null
+++ b/userspace/lib/logsink/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "logsink"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+log.workspace = true
+
+[target.'cfg(unix)'.dependencies]
+env_logger.workspace = true
+
+[dev-dependencies]
+env_logger.workspace = true
+
+[lints]
+workspace = true
diff --git a/userspace/lib/logsink/src/lib.rs b/userspace/lib/logsink/src/lib.rs
new file mode 100644
index 00000000..2f194b6d
--- /dev/null
+++ b/userspace/lib/logsink/src/lib.rs
@@ -0,0 +1,11 @@
+#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os))]
+
+#[cfg(any(rust_analyzer, target_os = "yggdrasil"))]
+pub mod yggdrasil;
+#[cfg(any(rust_analyzer, target_os = "yggdrasil"))]
+pub use yggdrasil::*;
+
+#[cfg(any(rust_analyzer, unix))]
+pub fn setup_logging() {
+    env_logger::init();
+}
diff --git a/userspace/lib/logsink/src/yggdrasil.rs b/userspace/lib/logsink/src/yggdrasil.rs
new file mode 100644
index 00000000..8c0ed46c
--- /dev/null
+++ b/userspace/lib/logsink/src/yggdrasil.rs
@@ -0,0 +1,30 @@
+use std::io::stdout;
+
+use log::LevelFilter;
+
+struct LogSink;
+
+impl log::Log for LogSink {
+    fn enabled(&self, _metadata: &log::Metadata) -> bool {
+        true
+    }
+
+    fn log(&self, record: &log::Record) {
+        debug_trace!("[{}] {}", record.level(), record.args());
+
+        use std::io::Write;
+        let mut stdout = stdout();
+        writeln!(stdout, "[{}] {}", record.level(), record.args()).ok();
+    }
+
+    fn flush(&self) {
+
+    }
+}
+
+static SINK: LogSink = LogSink;
+
+pub fn setup_logging() {
+    log::set_max_level(LevelFilter::Debug);
+    log::set_logger(&SINK).unwrap();
+}
diff --git a/userspace/term/src/main.rs b/userspace/term/src/main.rs
index 7c9ab187..80a401cb 100644
--- a/userspace/term/src/main.rs
+++ b/userspace/term/src/main.rs
@@ -29,7 +29,7 @@ use libcolors::{
         window::{EventOutcome, Window},
         Application,
     },
-    event::{KeyModifiers, KeyboardKey},
+    event::KeyModifiers, input::Key,
 };
 use state::{Cursor, State};
 
@@ -85,7 +85,6 @@ impl DrawState {
             dt.fill(default_bg);
         }
 
-
         if cursor_dirty {
             state.buffer.set_row_dirty(self.old_cursor.row);
         }
@@ -232,32 +231,32 @@ impl Terminal<'_> {
                 need_redraw = s.scroll_end();
             } else {
                 match (ev.modifiers, ev.key) {
-                    (KeyModifiers::NONE, KeyboardKey::Escape) => {
+                    (KeyModifiers::NONE, Key::Escape) => {
                         pty_master.write_all(b"\x1B").unwrap();
                         need_redraw = s.scroll_end();
                     }
-                    (KeyModifiers::NONE, KeyboardKey::Backspace) => {
+                    (KeyModifiers::NONE, Key::Backspace) => {
                         pty_master.write_all(&[termios.chars.erase]).unwrap();
                         need_redraw = s.scroll_end();
                     }
-                    (KeyModifiers::CTRL, KeyboardKey::Char(b'c')) => {
+                    (KeyModifiers::CTRL, Key::Char(b'c')) => {
                         pty_master.write_all(&[termios.chars.interrupt]).unwrap();
                         need_redraw = s.scroll_end();
                     }
-                    (KeyModifiers::CTRL, KeyboardKey::Char(b'd')) => {
+                    (KeyModifiers::CTRL, Key::Char(b'd')) => {
                         pty_master.write_all(&[termios.chars.eof]).unwrap();
                         need_redraw = s.scroll_end();
                     }
-                    (KeyModifiers::SHIFT, KeyboardKey::PageUp) => {
+                    (KeyModifiers::SHIFT, Key::PageUp) => {
                         need_redraw = s.scroll_up();
                     }
-                    (KeyModifiers::SHIFT, KeyboardKey::PageDown) => {
+                    (KeyModifiers::SHIFT, Key::PageDown) => {
                         need_redraw = s.scroll_down();
                     }
-                    (KeyModifiers::SHIFT, KeyboardKey::Home) => {
+                    (KeyModifiers::SHIFT, Key::Home) => {
                         need_redraw = s.scroll_home();
                     }
-                    (KeyModifiers::SHIFT, KeyboardKey::End) => {
+                    (KeyModifiers::SHIFT, Key::End) => {
                         need_redraw = s.scroll_end();
                     }
                     _ => (),
diff --git a/userspace/term/src/state.rs b/userspace/term/src/state.rs
index 96940a7c..2f08fa29 100644
--- a/userspace/term/src/state.rs
+++ b/userspace/term/src/state.rs
@@ -181,6 +181,9 @@ impl Buffer {
     }
 
     pub fn set_row_dirty(&mut self, row: usize) {
+        if row >= self.rows.len() {
+            return;
+        }
         self.rows[row].dirty = true;
     }