git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commite85a30d1a14bbc36235081ea6fc0be319d316b7c
parenta107294ecc
authorLucas Galante <[email protected]>
date2026-06-04 13:33
Integrate online accounts manager settings under Accounts tab with Google OAuth2 and iCloud setup

 Cargo.lock            | 867 +++++++++++++++++++++++++++++++++++++++++++++++++-
 Cargo.toml            |   1 +
 Makefile              |  14 +
 src/app.rs            |  22 +-
 src/main.rs           | 191 ++++++++++-
 src/pages/accounts.rs | 635 ++++++++++++++++++++++++++++++++++++
 src/pages/mod.rs      |   6 +-
 src/pages/typeface.rs | 127 +++++++-
 8 files changed, 1842 insertions(+), 21 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 289184a..66b1a38 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -334,6 +334,7 @@ dependencies = [
  "glyphon",
  "pollster",
  "raw-window-handle",
+ "reqwest",
  "serde",
  "serde_json",
  "smithay-client-toolkit",
@@ -402,6 +403,16 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.7"
@@ -415,7 +426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
 dependencies = [
  "bitflags 1.3.2",
- "core-foundation",
+ "core-foundation 0.9.4",
  "libc",
 ]
 
@@ -488,6 +499,17 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
 
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "dlib"
 version = "0.5.3"
@@ -518,6 +540,15 @@ version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
 
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "endi"
 version = "1.1.1"
@@ -638,6 +669,12 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
 
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
 [[package]]
 name = "foldhash"
 version = "0.1.5"
@@ -676,6 +713,15 @@ dependencies = [
  "ttf-parser 0.20.0",
 ]
 
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.1",
+]
+
 [[package]]
 name = "foreign-types"
 version = "0.5.0"
@@ -683,7 +729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
 dependencies = [
  "foreign-types-macros",
- "foreign-types-shared",
+ "foreign-types-shared 0.3.1",
 ]
 
 [[package]]
@@ -697,12 +743,27 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
 [[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
 [[package]]
 name = "futures"
 version = "0.3.32"
@@ -804,6 +865,17 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.4.2"
@@ -923,6 +995,25 @@ dependencies = [
  "bitflags 2.11.1",
 ]
 
+[[package]]
+name = "h2"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.15.5"
@@ -962,12 +1053,231 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
 
+[[package]]
+name = "http"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
 [[package]]
 name = "id-arena"
 version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
 
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
 [[package]]
 name = "imagesize"
 version = "0.12.0"
@@ -986,6 +1296,12 @@ dependencies = [
  "serde_core",
 ]
 
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
 [[package]]
 name = "itoa"
 version = "1.0.18"
@@ -1106,6 +1422,12 @@ version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
 
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
 [[package]]
 name = "litrs"
 version = "1.0.0"
@@ -1184,12 +1506,18 @@ dependencies = [
  "bitflags 2.11.1",
  "block",
  "core-graphics-types",
- "foreign-types",
+ "foreign-types 0.5.0",
  "log",
  "objc",
  "paste",
 ]
 
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
 [[package]]
 name = "miniz_oxide"
 version = "0.8.9"
@@ -1233,6 +1561,23 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "native-tls"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
 [[package]]
 name = "ndk-sys"
 version = "0.5.0+25.2.9519653"
@@ -1266,6 +1611,49 @@ version = "1.21.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
 
+[[package]]
+name = "openssl"
+version = "0.10.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
+dependencies = [
+ "bitflags 2.11.1",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "libc",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.116"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "ordered-float"
 version = "4.6.0"
@@ -1320,6 +1708,12 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
 
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
 [[package]]
 name = "pico-args"
 version = "0.5.0"
@@ -1382,6 +1776,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
 
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
 [[package]]
 name = "presser"
 version = "0.3.1"
@@ -1509,6 +1912,46 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
 
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
 [[package]]
 name = "resvg"
 version = "0.41.0"
@@ -1535,6 +1978,20 @@ dependencies = [
  "bytemuck",
 ]
 
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "roxmltree"
 version = "0.19.0"
@@ -1585,6 +2042,39 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
+[[package]]
+name = "rustls"
+version = "0.23.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
 [[package]]
 name = "rustversion"
 version = "1.0.22"
@@ -1624,6 +2114,21 @@ dependencies = [
  "unicode-script",
 ]
 
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "scoped-tls"
 version = "1.0.1"
@@ -1636,6 +2141,29 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.11.1",
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "self_cell"
 version = "1.2.2"
@@ -1702,6 +2230,18 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -1817,6 +2357,12 @@ dependencies = [
  "bitflags 2.11.1",
 ]
 
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
 [[package]]
 name = "static_assertions"
 version = "1.1.0"
@@ -1854,6 +2400,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
 [[package]]
 name = "svg_fmt"
 version = "0.4.5"
@@ -1892,6 +2444,26 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "sys-locale"
 version = "0.3.2"
@@ -1901,6 +2473,27 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "system-configuration"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
+dependencies = [
+ "bitflags 2.11.1",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "tempfile"
 version = "3.27.0"
@@ -1908,7 +2501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
 dependencies = [
  "fastrand",
- "getrandom",
+ "getrandom 0.4.2",
  "once_cell",
  "rustix 1.1.4",
  "windows-sys 0.61.2",
@@ -1989,6 +2582,16 @@ dependencies = [
  "strict-num",
 ]
 
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
 [[package]]
 name = "tinyvec"
 version = "1.11.0"
@@ -2032,6 +2635,39 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
 [[package]]
 name = "toml_datetime"
 version = "1.1.1+spec-1.1.0"
@@ -2062,6 +2698,51 @@ dependencies = [
  "winnow",
 ]
 
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags 2.11.1",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
 [[package]]
 name = "tracing"
 version = "0.1.44"
@@ -2093,6 +2774,12 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
 [[package]]
 name = "ttf-parser"
 version = "0.20.0"
@@ -2182,6 +2869,24 @@ version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
 
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
 [[package]]
 name = "usvg"
 version = "0.41.0"
@@ -2209,6 +2914,12 @@ dependencies = [
  "xmlwriter",
 ]
 
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
 [[package]]
 name = "uuid"
 version = "1.23.1"
@@ -2220,12 +2931,27 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
 [[package]]
 name = "version_check"
 version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
 
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
 [[package]]
 name = "wasi"
 version = "0.11.1+wasi-snapshot-preview1"
@@ -2586,8 +3312,8 @@ checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
 dependencies = [
  "windows-implement",
  "windows-interface",
- "windows-result",
- "windows-strings",
+ "windows-result 0.2.0",
+ "windows-strings 0.1.0",
  "windows-targets",
 ]
 
@@ -2619,6 +3345,17 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
 
+[[package]]
+name = "windows-registry"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+dependencies = [
+ "windows-link",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
+]
+
 [[package]]
 name = "windows-result"
 version = "0.2.0"
@@ -2628,13 +3365,40 @@ dependencies = [
  "windows-targets",
 ]
 
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
 [[package]]
 name = "windows-strings"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
 dependencies = [
- "windows-result",
+ "windows-result 0.2.0",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
  "windows-targets",
 ]
 
@@ -2823,6 +3587,12 @@ dependencies = [
  "wasmparser",
 ]
 
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
 [[package]]
 name = "xcursor"
 version = "0.3.10"
@@ -2867,6 +3637,29 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1"
 
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
 [[package]]
 name = "zbus"
 version = "5.15.0"
@@ -2934,6 +3727,66 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
 
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "zmij"
 version = "1.0.21"
diff --git a/Cargo.toml b/Cargo.toml
index 4dbc9ef..751958e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ zbus = "5"
 futures = "0.3"
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
+reqwest = { version = "0.12", features = ["json"] }
 
 [lib]
 name = "clear_system_interface"
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4a2f426
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+.PHONY: build install run clean
+
+build:
+	cargo build --release
+
+install: build
+	mkdir -p ~/.local/bin
+	install -m 755 target/release/clear-system-interface ~/.local/bin/clear-system-interface
+
+run:
+	cargo run
+
+clean:
+	cargo clean
diff --git a/src/app.rs b/src/app.rs
index d17cdeb..b2f764d 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -15,6 +15,7 @@ use crate::pages::typeface;
 use crate::pages::services;
 use crate::pages::colors;
 use crate::pages::screensaver;
+use crate::pages::accounts;
 use crate::pages::Page;
 
 pub struct AppState {
@@ -34,6 +35,7 @@ pub struct AppState {
     pub services: services::ServicesState,
     pub colors: colors::ColorsState,
     pub screensaver: screensaver::ScreensaverState,
+    pub accounts: accounts::AccountsState,
 }
 
 impl Default for AppState {
@@ -55,6 +57,7 @@ impl Default for AppState {
             services: services::ServicesState::default(),
             colors: colors::ColorsState::default(),
             screensaver: screensaver::read_screensaver_config(),
+            accounts: accounts::AccountsState::default_mock(),
         }
     }
 }
@@ -76,12 +79,13 @@ pub enum AppAction {
     Services(services::ServicesMessage),
     Colors(colors::ColorsMessage),
     Screensaver(screensaver::ScreensaverMessage),
+    Accounts(accounts::AccountsMessage),
 }
 
 
 pub struct PageContent {
     pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
-    pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>)>,
+    pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
     pub buttons: Vec<ContentButton>,
 }
 
@@ -107,11 +111,11 @@ impl PageContent {
     }
 
     pub fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
-        self.texts.push((content.to_string(), size, x, y, color, None));
+        self.texts.push((content.to_string(), size, x, y, color, None, None));
     }
 
     pub fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
-        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string())));
+        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
     }
 
     pub fn button(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32,
@@ -143,10 +147,18 @@ impl RenderTarget for PageContent {
     }
 
     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
-        self.texts.push((content.to_string(), size, x, y, color, None));
+        self.texts.push((content.to_string(), size, x, y, color, None, None));
     }
 
     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
-        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string())));
+        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
+    }
+
+    fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
+        self.texts.push((content.to_string(), size, x, y, color, None, bounds));
+    }
+
+    fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: Option<[f32; 4]>) {
+        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), bounds));
     }
 }
diff --git a/src/main.rs b/src/main.rs
index c2dd1b7..f9d05aa 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -207,6 +207,7 @@ struct SystemInterface {
     rx_typeface: std::sync::mpsc::Receiver<pages::typeface::TypefaceState>,
     rx_services: std::sync::mpsc::Receiver<Vec<pages::services::ServiceInfo>>,
     rx_colors: std::sync::mpsc::Receiver<pages::colors::ColorsState>,
+    rx_accounts: std::sync::mpsc::Receiver<Vec<pages::accounts::AccountInfo>>,
     tx_backup: std::sync::mpsc::Sender<pages::backup::BackupMessage>,
     rx_backup: std::sync::mpsc::Receiver<pages::backup::BackupMessage>,
     tx_color_selector: std::sync::mpsc::Sender<ColorSelectorAction>,
@@ -363,6 +364,7 @@ impl clear_ui::engine::Application for SystemInterface {
         let rx_backup_state = spawn_bg(30, || pages::backup::fetch_backup_state());
         let rx_typeface = spawn_bg(30, || pages::typeface::fetch_typeface_state());
         let rx_services = spawn_bg(3, || pages::services::fetch_services());
+        let rx_accounts = spawn_bg(3, || pages::accounts::fetch_accounts());
         let rx_colors = {
             let (tx, rx) = std::sync::mpsc::channel::<pages::colors::ColorsState>();
             tokio::spawn(async move {
@@ -377,7 +379,7 @@ impl clear_ui::engine::Application for SystemInterface {
         let (tx_backup, rx_backup) = std::sync::mpsc::channel();
         let (tx_color_selector, rx_color_selector) = std::sync::mpsc::channel();
 
-        let (sans_family, serif_family, monospace_family, _, _, _, _) = pages::typeface::read_preferred_fonts();
+        let (sans_family, serif_family, monospace_family, _, _, _, _, _) = pages::typeface::read_preferred_fonts();
 
         let pages_names = Page::ALL.iter().map(|p| p.label().to_string()).collect::<Vec<_>>();
         let paginator = clear_ui::widget::Paginator::new(56.0, pages_names)
@@ -417,6 +419,7 @@ impl clear_ui::engine::Application for SystemInterface {
             rx_typeface,
             rx_services,
             rx_colors,
+            rx_accounts,
             tx_backup,
             rx_backup,
             tx_color_selector,
@@ -570,7 +573,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 hovering: false, kind: WidgetKind::Static,
             });
         }
-        for (t, size, x, y, tc, font_opt) in &paginator_pc.texts {
+        for (t, size, x, y, tc, font_opt, bounds) in &paginator_pc.texts {
             text_items.push(TextItem {
                 buffer: make_text_buffer_with_font(
                     &mut self.font_system,
@@ -585,6 +588,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 color: glyphon::Color::rgb(
                     (tc[0] * 255.0) as u8, (tc[1] * 255.0) as u8, (tc[2] * 255.0) as u8,
                 ),
+                bounds: *bounds,
             });
         }
 
@@ -609,10 +613,16 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
         self.app.typeface.fuzzel_box.clear_children(); self.app.typeface.fuzzel_box.set_parent(None);
         self.app.typeface.terminal_menu.clear_children(); self.app.typeface.terminal_menu.set_parent(None);
         self.app.typeface.terminal_box.clear_children(); self.app.typeface.terminal_box.set_parent(None);
+        self.app.typeface.paginator_menu.clear_children(); self.app.typeface.paginator_menu.set_parent(None);
+        self.app.typeface.paginator_box.clear_children(); self.app.typeface.paginator_box.set_parent(None);
         self.app.typeface.search_box.clear_children(); self.app.typeface.search_box.set_parent(None);
         self.app.typeface.list_box.scroll_box.clear_children(); self.app.typeface.list_box.scroll_box.set_parent(None);
 
         self.app.services.search_box.clear_children(); self.app.services.search_box.set_parent(None);
+        self.app.accounts.email_box.clear_children(); self.app.accounts.email_box.set_parent(None);
+        self.app.accounts.password_box.clear_children(); self.app.accounts.password_box.set_parent(None);
+        self.app.accounts.imap_box.clear_children(); self.app.accounts.imap_box.set_parent(None);
+        self.app.accounts.smtp_box.clear_children(); self.app.accounts.smtp_box.set_parent(None);
 
         self.app.services.list_box.scroll_box.clear_children(); self.app.services.list_box.scroll_box.set_parent(None);
 
@@ -674,6 +684,12 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
 
         use clear_ui::widget::focus::link_parent_child;
         match self.app.current_page {
+            Page::Accounts => {
+                link_parent_child(&mut self.page_root_container, &mut self.app.accounts.email_box);
+                link_parent_child(&mut self.page_root_container, &mut self.app.accounts.password_box);
+                link_parent_child(&mut self.page_root_container, &mut self.app.accounts.imap_box);
+                link_parent_child(&mut self.page_root_container, &mut self.app.accounts.smtp_box);
+            }
             Page::Typefaces => {
                 self.page_sec_containers.resize_with(3, clear_ui::widget::Container::new);
                 
@@ -693,6 +709,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.fuzzel_box);
                 link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.terminal_menu);
                 link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.terminal_box);
+                link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.paginator_menu);
+                link_parent_child(&mut self.page_sec_containers[1], &mut self.app.typeface.paginator_box);
                 
                 link_parent_child(&mut self.page_sec_containers[2], &mut self.app.typeface.search_box);
                 link_parent_child(&mut self.page_sec_containers[2], &mut self.app.typeface.list_box.scroll_box);
@@ -799,7 +817,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
         Self::collect_popover_rects(&self.page_root_container, &mut popovers);
 
         if !popovers.is_empty() {
-            pc.texts.retain(|(text, size, tx, ty, _, _)| {
+            pc.texts.retain(|(text, size, tx, ty, _, _, _)| {
                 let text_w = text.chars().count() as f32 * *size * 0.65;
                 for &(px, py, pw, ph) in &popovers {
                     let x_overlap = *tx <= px + pw && (*tx + text_w) >= px;
@@ -816,7 +834,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
         for (_, _, y, _, h) in &pc.rects {
             max_y = max_y.max(y + h);
         }
-        for (_, size, _, y, _, _) in &pc.texts {
+        for (_, size, _, y, _, _, _) in &pc.texts {
             max_y = max_y.max(y + size);
         }
         for btn in &pc.buttons {
@@ -834,7 +852,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 hovering: false, kind: WidgetKind::Static,
             });
         }
-        for (t, size, x, y, tc, font_opt) in &pc.texts {
+        for (t, size, x, y, tc, font_opt, bounds) in &pc.texts {
             text_items.push(TextItem {
                 buffer: make_text_buffer_with_font(
                     &mut self.font_system,
@@ -849,6 +867,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 color: glyphon::Color::rgb(
                     (tc[0] * 255.0) as u8, (tc[1] * 255.0) as u8, (tc[2] * 255.0) as u8,
                 ),
+                bounds: *bounds,
             });
         }
         for btn in &pc.buttons {
@@ -903,6 +922,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                     (btn.label_color[1] * 255.0) as u8,
                     (btn.label_color[2] * 255.0) as u8,
                 ),
+                bounds: None,
             });
             let mut cb = btn.clone();
             cb.x *= s; cb.y = (cb.y - scroll_offset_y) * s; cb.w *= s; cb.h *= s;
@@ -966,6 +986,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                     buffer: make_text_buffer(&mut self.font_system, opt, 12.0 * s),
                     x: (cx + 8.0) * s, y: iy * s,
                     color: text_color,
+                    bounds: None,
                 });
             }
         }
@@ -985,6 +1006,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
             .map(|c| clear_ui::widget::focus::is_focused(c))
             .collect();
         match self.app.current_page {
+            Page::Accounts => accounts::view(&mut self.app.accounts, cx, cy, cw, ch),
             Page::Audio => audio::view(&mut self.app.audio, cx, cy, cw, ch, &sec_focused),
             Page::Display => display::view(&mut self.app.display, cx, cy, cw, ch),
             Page::Radios => network::view(&mut self.app.network, cx, cy, cw, ch, root_focused),
@@ -1152,6 +1174,10 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
             colors::update(&mut self.app.colors, pages::colors::ColorsMessage::Refreshed(s));
             self.needs_rebuild = true;
         }
+        while let Ok(s) = self.rx_accounts.try_recv() {
+            accounts::update(&mut self.app.accounts, accounts::AccountsMessage::Refreshed(s));
+            self.needs_rebuild = true;
+        }
         while let Ok(m) = self.rx_backup.try_recv() {
             self.handle_action(&AppAction::Backup(m));
             self.needs_rebuild = true;
@@ -1202,6 +1228,16 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 }
                 _ => pages::backup::update(&mut self.app.backup, m.clone()),
             },
+            AppAction::Accounts(m) => match m {
+                pages::accounts::AccountsMessage::GoogleLoginInit => {
+                    pages::accounts::update(&mut self.app.accounts, m.clone());
+                    let sender = self.sender.clone();
+                    tokio::spawn(async move {
+                        pages::accounts::run_google_login(sender).await;
+                    });
+                }
+                _ => pages::accounts::update(&mut self.app.accounts, m.clone()),
+            },
         }
     }
 
@@ -1440,6 +1476,12 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
             if self.app.typeface.terminal_box.cursor_moved(lx, ly) {
                 changed = true;
             }
+            if self.app.typeface.paginator_menu.cursor_moved(lx, ly) {
+                changed = true;
+            }
+            if self.app.typeface.paginator_box.cursor_moved(lx, ly) {
+                changed = true;
+            }
             if self.app.typeface.search_box.cursor_moved(lx, ly) {
                 changed = true;
             }
@@ -1458,6 +1500,9 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
             if self.app.typeface.terminal_size_box.cursor_moved(lx, ly) {
                 changed = true;
             }
+            if self.app.typeface.paginator_size_box.cursor_moved(lx, ly) {
+                changed = true;
+            }
             let query = self.app.typeface.search_box.text.to_lowercase();
             let matching_count = self.app.typeface.all_fonts.iter()
                 .filter(|f| f.to_lowercase().contains(&query))
@@ -1479,6 +1524,12 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 changed = true;
             }
         }
+        if self.app.current_page == Page::Accounts {
+            if self.app.accounts.email_box.cursor_moved(lx, ly) { changed = true; }
+            if self.app.accounts.password_box.cursor_moved(lx, ly) { changed = true; }
+            if self.app.accounts.imap_box.cursor_moved(lx, ly) { changed = true; }
+            if self.app.accounts.smtp_box.cursor_moved(lx, ly) { changed = true; }
+        }
         if changed { self.needs_rebuild = true; }
         changed
     }
@@ -1526,6 +1577,13 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
         if state == clear_ui::widget::ElementState::Pressed {
             let mut clicked_any_focusable = false;
             match self.app.current_page {
+                Page::Accounts => {
+                    let accs = &mut self.app.accounts;
+                    if accs.email_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+                    if accs.password_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+                    if accs.imap_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+                    if accs.smtp_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+                }
                 Page::Layout => {
                     for sb in &mut self.app.layout.spinboxes {
                         if sb.hit_test(lx, ly) { clicked_any_focusable = true; }
@@ -1599,6 +1657,8 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                     if tf.fuzzel_box.hit_test(lx, ly) { clicked_any_focusable = true; }
                     if tf.terminal_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
                     if tf.terminal_box.hit_test(lx, ly) { clicked_any_focusable = true; }
+                    if tf.paginator_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
+                    if tf.paginator_box.hit_test(lx, ly) { clicked_any_focusable = true; }
                     if tf.search_box.hit_test(lx, ly) { clicked_any_focusable = true; }
                     if tf.list_box.hit_test(lx, ly) { clicked_any_focusable = true; }
                 }
@@ -1977,6 +2037,50 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 actions.push(AppAction::Status(pages::status::StatusMessage::ToggleUnderline));
             }
         }
+        if self.app.current_page == Page::Accounts {
+            let tb = &mut self.app.accounts.email_box;
+            if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
+            if tb.mouse_input(button, state, lx, ly) {
+                self.needs_rebuild = true;
+            }
+            if state == clear_ui::widget::ElementState::Pressed && tb.take_change() {
+                let email_val = tb.text.trim().to_lowercase();
+                if email_val.ends_with("@gmail.com") {
+                    self.app.accounts.imap_box.text = "imap.gmail.com:993".to_string();
+                    self.app.accounts.imap_box.edit_buffer = "imap.gmail.com:993".to_string();
+                    self.app.accounts.smtp_box.text = "smtp.gmail.com:465".to_string();
+                    self.app.accounts.smtp_box.edit_buffer = "smtp.gmail.com:465".to_string();
+                } else if email_val.ends_with("@icloud.com") {
+                    self.app.accounts.imap_box.text = "imap.mail.me.com:993".to_string();
+                    self.app.accounts.imap_box.edit_buffer = "imap.mail.me.com:993".to_string();
+                    self.app.accounts.smtp_box.text = "smtp.mail.me.com:587".to_string();
+                    self.app.accounts.smtp_box.edit_buffer = "smtp.mail.me.com:587".to_string();
+                } else if email_val.ends_with("@outlook.com") || email_val.ends_with("@hotmail.com") {
+                    self.app.accounts.imap_box.text = "outlook.office365.com:993".to_string();
+                    self.app.accounts.imap_box.edit_buffer = "outlook.office365.com:993".to_string();
+                    self.app.accounts.smtp_box.text = "smtp.office365.com:587".to_string();
+                    self.app.accounts.smtp_box.edit_buffer = "smtp.office365.com:587".to_string();
+                }
+            }
+
+            let tb = &mut self.app.accounts.password_box;
+            if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
+            if tb.mouse_input(button, state, lx, ly) {
+                self.needs_rebuild = true;
+            }
+
+            let tb = &mut self.app.accounts.imap_box;
+            if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
+            if tb.mouse_input(button, state, lx, ly) {
+                self.needs_rebuild = true;
+            }
+
+            let tb = &mut self.app.accounts.smtp_box;
+            if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
+            if tb.mouse_input(button, state, lx, ly) {
+                self.needs_rebuild = true;
+            }
+        }
         if self.app.current_page == Page::Typefaces {
             let tb = &mut self.app.typeface.sans_box;
             if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
@@ -2077,6 +2181,24 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetTerminal(tb.text.clone())));
             }
 
+            let menu = &mut self.app.typeface.paginator_menu;
+            if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
+            if menu.mouse_input(button, state, lx, ly) {
+                self.needs_rebuild = true;
+            }
+            if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
+                actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetPaginatorMenu(menu.selected)));
+            }
+
+            let tb = &mut self.app.typeface.paginator_box;
+            if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
+            if tb.mouse_input(button, state, lx, ly) {
+                self.needs_rebuild = true;
+            }
+            if state == clear_ui::widget::ElementState::Pressed && tb.take_change() {
+                actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetPaginator(tb.text.clone())));
+            }
+
             let tb = &mut self.app.typeface.search_box;
             if state == clear_ui::widget::ElementState::Pressed && !tb.hit_test(lx, ly) { tb.unfocus(); }
             if tb.mouse_input(button, state, lx, ly) {
@@ -2126,6 +2248,16 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetTerminalSize(sb.value)));
             }
 
+            let sb = &mut self.app.typeface.paginator_size_box;
+            if state == clear_ui::widget::ElementState::Pressed && !sb.hit_test(lx, ly) { sb.unfocus(); }
+            let old_val = sb.value;
+            if sb.mouse_input(button, state, lx, ly) {
+                self.needs_rebuild = true;
+            }
+            if sb.value != old_val {
+                actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetPaginatorSize(sb.value)));
+            }
+
             let tf = &mut self.app.typeface;
             let query = tf.search_box.text.to_lowercase();
             let matching_fonts: Vec<String> = tf.all_fonts.iter()
@@ -2500,6 +2632,41 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 return true;
             }
         }
+        if self.app.current_page == Page::Accounts {
+            let mut consumed = false;
+            let tb = &mut self.app.accounts.email_box;
+            if tb.keyboard_input(event) {
+                consumed = true;
+                let email_val = tb.edit_buffer.trim().to_lowercase();
+                if email_val.ends_with("@gmail.com") {
+                    self.app.accounts.imap_box.text = "imap.gmail.com:993".to_string();
+                    self.app.accounts.imap_box.edit_buffer = "imap.gmail.com:993".to_string();
+                    self.app.accounts.smtp_box.text = "smtp.gmail.com:465".to_string();
+                    self.app.accounts.smtp_box.edit_buffer = "smtp.gmail.com:465".to_string();
+                } else if email_val.ends_with("@icloud.com") {
+                    self.app.accounts.imap_box.text = "imap.mail.me.com:993".to_string();
+                    self.app.accounts.imap_box.edit_buffer = "imap.mail.me.com:993".to_string();
+                    self.app.accounts.smtp_box.text = "smtp.mail.me.com:587".to_string();
+                    self.app.accounts.smtp_box.edit_buffer = "smtp.mail.me.com:587".to_string();
+                } else if email_val.ends_with("@outlook.com") || email_val.ends_with("@hotmail.com") {
+                    self.app.accounts.imap_box.text = "outlook.office365.com:993".to_string();
+                    self.app.accounts.imap_box.edit_buffer = "outlook.office365.com:993".to_string();
+                    self.app.accounts.smtp_box.text = "smtp.office365.com:587".to_string();
+                    self.app.accounts.smtp_box.edit_buffer = "smtp.office365.com:587".to_string();
+                }
+            }
+            let tb = &mut self.app.accounts.password_box;
+            if tb.keyboard_input(event) { consumed = true; }
+            let tb = &mut self.app.accounts.imap_box;
+            if tb.keyboard_input(event) { consumed = true; }
+            let tb = &mut self.app.accounts.smtp_box;
+            if tb.keyboard_input(event) { consumed = true; }
+            
+            if consumed {
+                self.needs_rebuild = true;
+                return true;
+            }
+        }
         if self.app.current_page == Page::Audio {
             let mut actions = Vec::new();
             for (i, sb) in self.app.audio.sink_spinboxes.iter_mut().enumerate() {
@@ -2711,6 +2878,14 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 consumed = true;
             }
 
+            let tb = &mut self.app.typeface.paginator_box;
+            if tb.keyboard_input(event) {
+                if tb.take_change() {
+                    actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetPaginator(tb.text.clone())));
+                }
+                consumed = true;
+            }
+
             let tb = &mut self.app.typeface.search_box;
             if tb.keyboard_input(event) {
                 if tb.take_change() {
@@ -2742,6 +2917,12 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
                 actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetTerminalSize(sb.value)));
                 consumed = true;
             }
+
+            let sb = &mut self.app.typeface.paginator_size_box;
+            if sb.keyboard_input(event) {
+                actions.push(AppAction::Typeface(pages::typeface::TypefaceMessage::SetPaginatorSize(sb.value)));
+                consumed = true;
+            }
             
             for a in &actions {
                 self.handle_action(a);
diff --git a/src/pages/accounts.rs b/src/pages/accounts.rs
new file mode 100644
index 0000000..d8fdebe
--- /dev/null
+++ b/src/pages/accounts.rs
@@ -0,0 +1,635 @@
+use crate::app::{AppAction, PageContent};
+use clear_ui::layout::{render_widget, Section};
+use clear_ui::widget::{TextBox, Widget};
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
+pub struct AccountInfo {
+    pub email: String,
+    pub imap: String,
+    pub smtp: String,
+    pub is_default: bool,
+    pub password: String,
+    #[serde(default)]
+    pub is_oauth: bool,
+    #[serde(default)]
+    pub access_token: Option<String>,
+    #[serde(default)]
+    pub refresh_token: Option<String>,
+    #[serde(default)]
+    pub token_expiry: Option<u64>,
+    #[serde(default)]
+    pub client_id: Option<String>,
+    #[serde(default)]
+    pub client_secret: Option<String>,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct AccountsState {
+    pub loaded: bool,
+    pub accounts: Vec<AccountInfo>,
+    pub selected_idx: Option<usize>,
+    pub adding_new: bool,
+    pub email_box: TextBox,
+    pub password_box: TextBox,
+    pub imap_box: TextBox,
+    pub smtp_box: TextBox,
+    pub status_msg: Option<String>,
+    pub status_msg_timer: f32,
+    pub oauth_listener_running: bool,
+}
+
+impl AccountsState {
+    pub fn default_mock() -> Self {
+        let mut state = Self::default();
+        state.email_box = TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true).with_label("Email Address");
+        state.password_box = {
+            let mut tb = TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true).with_label("Password / App Password");
+            tb.is_password = true;
+            tb
+        };
+        state.imap_box = TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true).with_label("IMAP Server");
+        state.smtp_box = TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true).with_label("SMTP Server");
+        state
+    }
+}
+
+#[derive(Debug, Clone)]
+pub enum AccountsMessage {
+    Refreshed(Vec<AccountInfo>),
+    SelectAccount(usize),
+    AddAccountStart,
+    AddAccountCancel,
+    AddAccountSave,
+    DeleteAccount(usize),
+    MakeDefault(usize),
+    StatusMessage(String),
+    GoogleLoginInit,
+    GoogleLoginSuccess(AccountInfo),
+    ICloudLoginHelp,
+}
+
+pub fn get_accounts_path() -> std::path::PathBuf {
+    let p = std::path::PathBuf::from("/home/lsgalante/.config/ccec");
+    if !p.exists() {
+        let _ = std::fs::create_dir_all(&p);
+        #[cfg(unix)]
+        {
+            use std::os::unix::fs::PermissionsExt;
+            if let Ok(metadata) = std::fs::metadata(&p) {
+                let mut perms = metadata.permissions();
+                perms.set_mode(0o700);
+                let _ = std::fs::set_permissions(&p, perms);
+            }
+        }
+    }
+    p.join("accounts.json")
+}
+
+pub fn load_accounts() -> Vec<AccountInfo> {
+    let path = get_accounts_path();
+    if path.exists() {
+        if let Ok(content) = std::fs::read_to_string(&path) {
+            if let Ok(accounts) = serde_json::from_str(&content) {
+                return accounts;
+            }
+        }
+    }
+    vec![
+        AccountInfo {
+            email: "[email protected]".to_string(),
+            imap: "imap.clear-ui.org:993".to_string(),
+            smtp: "smtp.clear-ui.org:465".to_string(),
+            is_default: true,
+            password: "mock_password".to_string(),
+            is_oauth: false,
+            access_token: None,
+            refresh_token: None,
+            token_expiry: None,
+            client_id: None,
+            client_secret: None,
+        },
+    ]
+}
+
+pub fn save_accounts(accounts: &[AccountInfo]) {
+    let path = get_accounts_path();
+    if let Ok(content) = serde_json::to_string_pretty(accounts) {
+        let _ = std::fs::write(&path, content);
+        #[cfg(unix)]
+        {
+            use std::os::unix::fs::PermissionsExt;
+            if let Ok(metadata) = std::fs::metadata(&path) {
+                let mut perms = metadata.permissions();
+                perms.set_mode(0o600);
+                let _ = std::fs::set_permissions(&path, perms);
+            }
+        }
+    }
+}
+
+pub async fn fetch_accounts() -> Vec<AccountInfo> {
+    load_accounts()
+}
+
+const GOOGLE_CLIENT_ID: &str = "REDACTED.apps.googleusercontent.com";
+const GOOGLE_CLIENT_SECRET: &str = "GOCSPX-REDACTED";
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct GoogleClientConfig {
+    pub client_id: String,
+    pub client_secret: String,
+}
+
+pub fn load_google_client_config() -> GoogleClientConfig {
+    let p = std::path::PathBuf::from("/home/lsgalante/.config/ccec/google_client.json");
+    if p.exists() {
+        if let Ok(content) = std::fs::read_to_string(&p) {
+            if let Ok(config) = serde_json::from_str::<GoogleClientConfig>(&content) {
+                return config;
+            }
+        }
+    }
+    let default_config = GoogleClientConfig {
+        client_id: GOOGLE_CLIENT_ID.to_string(),
+        client_secret: GOOGLE_CLIENT_SECRET.to_string(),
+    };
+    if let Some(parent) = p.parent() {
+        let _ = std::fs::create_dir_all(parent);
+    }
+    if let Ok(content) = serde_json::to_string_pretty(&default_config) {
+        let _ = std::fs::write(&p, content);
+        #[cfg(unix)]
+        {
+            use std::os::unix::fs::PermissionsExt;
+            if let Ok(metadata) = std::fs::metadata(&p) {
+                let mut perms = metadata.permissions();
+                perms.set_mode(0o600);
+                let _ = std::fs::set_permissions(&p, perms);
+            }
+        }
+    }
+    default_config
+}
+
+pub async fn run_google_login(sender: calloop::channel::Sender<AppAction>) {
+    let client_config = load_google_client_config();
+    let listener = match tokio::net::TcpListener::bind("127.0.0.1:8080").await {
+        Ok(l) => l,
+        Err(e) => {
+            let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage(format!("Failed to bind port 8080: {}", e))));
+            return;
+        }
+    };
+    
+    let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage("Waiting for browser login...".to_string())));
+    
+    let auth_url = format!(
+        "https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F&access_type=offline&prompt=consent",
+        client_config.client_id
+    );
+    let _ = std::process::Command::new("xdg-open").arg(&auth_url).spawn();
+
+    if let Ok((mut stream, _)) = listener.accept().await {
+        use tokio::io::{AsyncReadExt, AsyncWriteExt};
+        let mut buffer = [0; 1024];
+        if let Ok(n) = stream.read(&mut buffer).await {
+            let req_str = String::from_utf8_lossy(&buffer[..n]);
+            if let Some(code_idx) = req_str.find("code=") {
+                let rest = &req_str[code_idx + 5..];
+                let end_idx = rest.find(|c: char| c == ' ' || c == '&' || c == '\r' || c == '\n').unwrap_or(rest.len());
+                let code = rest[..end_idx].to_string();
+                
+                let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage("Exchanging code for token...".to_string())));
+                
+                // Perform token exchange
+                exchange_code_for_tokens(code, sender.clone()).await;
+                
+                let response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n\
+                                <html><head><style>body { font-family: sans-serif; background-color: #08080c; color: #fff; text-align: center; padding-top: 50px; }</style></head><body><h2>Clear System Settings Authentication Successful!</h2><p>You can close this tab and return to the application.</p></body></html>";
+                let _ = stream.write_all(response.as_bytes()).await;
+                let _ = stream.flush().await;
+            } else {
+                let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage("OAuth Error: No code received".to_string())));
+                let response = "HTTP/1.1 400 Bad Request\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n\
+                                <html><head><style>body { font-family: sans-serif; background-color: #08080c; color: #ff6060; text-align: center; padding-top: 50px; }</style></head><body><h2>Clear System Settings Authentication Failed</h2><p>No authorization code was found.</p></body></html>";
+                let _ = stream.write_all(response.as_bytes()).await;
+                let _ = stream.flush().await;
+            }
+        }
+    }
+}
+
+pub async fn exchange_code_for_tokens(code: String, sender: calloop::channel::Sender<AppAction>) {
+    let client_config = load_google_client_config();
+    let client = reqwest::Client::new();
+    let params = [
+        ("code", code.as_str()),
+        ("client_id", client_config.client_id.as_str()),
+        ("client_secret", client_config.client_secret.as_str()),
+        ("redirect_uri", "http://127.0.0.1:8080"),
+        ("grant_type", "authorization_code"),
+    ];
+    
+    match client.post("https://oauth2.googleapis.com/token")
+        .form(&params)
+        .send()
+        .await 
+    {
+        Ok(resp) => {
+            if resp.status().is_success() {
+                if let Ok(json) = resp.json::<serde_json::Value>().await {
+                    let access_token = json.get("access_token").and_then(|v| v.as_str()).unwrap_or("").to_string();
+                    let refresh_token = json.get("refresh_token").and_then(|v| v.as_str()).unwrap_or("").to_string();
+                    let expires_in = json.get("expires_in").and_then(|v| v.as_u64()).unwrap_or(3600);
+                    
+                    let now = std::time::SystemTime::now()
+                        .duration_since(std::time::UNIX_EPOCH)
+                        .unwrap_or_default()
+                        .as_secs();
+                    let expiry = now + expires_in;
+ 
+                    // Request user profile info to get the email address
+                    if let Ok(email_resp) = client.get("https://www.googleapis.com/oauth2/v2/userinfo")
+                        .bearer_auth(&access_token)
+                        .send()
+                        .await 
+                    {
+                        if let Ok(email_json) = email_resp.json::<serde_json::Value>().await {
+                            if let Some(email) = email_json.get("email").and_then(|v| v.as_str()) {
+                                let new_acc = AccountInfo {
+                                    email: email.to_string(),
+                                    imap: "imap.gmail.com:993".to_string(),
+                                    smtp: "smtp.gmail.com:465".to_string(),
+                                    is_default: false,
+                                    password: String::new(),
+                                    is_oauth: true,
+                                    access_token: Some(access_token),
+                                    refresh_token: Some(refresh_token),
+                                    token_expiry: Some(expiry),
+                                    client_id: Some(client_config.client_id),
+                                    client_secret: Some(client_config.client_secret),
+                                };
+                                let _ = sender.send(AppAction::Accounts(AccountsMessage::GoogleLoginSuccess(new_acc)));
+                                return;
+                            }
+                        }
+                    }
+                }
+                let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage("Failed to parse Google profile".to_string())));
+            } else {
+                let err_text = resp.text().await.unwrap_or_default();
+                let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage(format!("Token exchange failed: {}", err_text))));
+            }
+        }
+        Err(e) => {
+            let _ = sender.send(AppAction::Accounts(AccountsMessage::StatusMessage(format!("Token request failed: {}", e))));
+        }
+    }
+}
+
+const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+
+pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+    let y = cy + 12.0;
+
+    let mut sec = Section::new(&mut pc, cx, y, cw, "Online Accounts");
+
+    if !state.loaded {
+        sec.text(&mut pc, "Loading online accounts...", 12.0, 0.0, 12.0, TEXT_DIM);
+        sec.spacing(18.0);
+    } else {
+        let usable_w = cw - 24.0;
+        let gap = 24.0;
+        let left_w = (usable_w - gap) * 0.40;
+        let right_w = (usable_w - gap) * 0.60;
+        let left_x = cx + 12.0;
+        let right_x = left_x + left_w + gap;
+
+        let mut left_y = sec.ay();
+        let row_h = 28.0;
+        let row_gap = 8.0;
+
+        if state.accounts.is_empty() {
+            pc.text("No accounts configured.", left_x + 8.0, left_y, 12.0, TEXT_DIM);
+            left_y += 20.0;
+        } else {
+            for (idx, acc) in state.accounts.iter().enumerate() {
+                let label = if acc.is_default {
+                    format!("{} [Default]", acc.email)
+                } else {
+                    acc.email.clone()
+                };
+                let is_selected = state.selected_idx == Some(idx) && !state.adding_new;
+                let bg_col = if is_selected { [0.20, 0.40, 0.65, 0.4] } else { [0.10, 0.10, 0.16, 0.3] };
+                pc.button(
+                    &label,
+                    left_x,
+                    left_y,
+                    left_w,
+                    row_h,
+                    bg_col,
+                    [0.20, 0.20, 0.25, 0.15],
+                    [0.90, 0.90, 0.95, 1.0],
+                    AppAction::Accounts(AccountsMessage::SelectAccount(idx)),
+                );
+                left_y += row_h + row_gap;
+            }
+        }
+
+        left_y += 12.0;
+        
+        let add_bg = if state.adding_new { [0.20, 0.40, 0.65, 0.4] } else { [0.13, 0.18, 0.14, 1.0] };
+        pc.button(
+            "Add Account",
+            left_x,
+            left_y,
+            left_w,
+            row_h,
+            add_bg,
+            [0.25, 0.30, 0.26, 1.0],
+            [1.0, 1.0, 1.0, 1.0],
+            AppAction::Accounts(AccountsMessage::AddAccountStart),
+        );
+        left_y += row_h + row_gap;
+
+        if let Some(selected_idx) = state.selected_idx {
+            if selected_idx < state.accounts.len() && !state.adding_new {
+                let acc = &state.accounts[selected_idx];
+                if !acc.is_default {
+                    pc.button(
+                        "Make Default",
+                        left_x,
+                        left_y,
+                        left_w,
+                        row_h,
+                        [0.15, 0.15, 0.25, 1.0],
+                        [0.25, 0.25, 0.35, 1.0],
+                        [1.0, 1.0, 1.0, 1.0],
+                        AppAction::Accounts(AccountsMessage::MakeDefault(selected_idx)),
+                    );
+                    left_y += row_h + row_gap;
+                }
+                pc.button(
+                    "Delete Account",
+                    left_x,
+                    left_y,
+                    left_w,
+                    row_h,
+                    [0.33, 0.20, 0.20, 1.0],
+                    [0.45, 0.25, 0.25, 1.0],
+                    [1.0, 0.33, 0.33, 1.0],
+                    AppAction::Accounts(AccountsMessage::DeleteAccount(selected_idx)),
+                );
+                left_y += row_h + row_gap;
+            }
+        }
+
+        let mut right_y = sec.ay();
+
+        if state.adding_new {
+            pc.text("Add New Account", right_x, right_y, 14.0, [0.35, 0.65, 0.90, 1.0]);
+            right_y += 24.0;
+
+            pc.text("Note: Gmail uses Google Login. iCloud requires App PW.", right_x, right_y, 11.0, TEXT_DIM);
+            right_y += 18.0;
+
+            let widget_h = 26.0;
+            let field_gap = 14.0;
+
+            // Email Address textbox
+            let email_top = state.email_box.top_room();
+            state.email_box.set_row_rect(right_x, right_w);
+            clear_ui::layout::render_widget(&mut pc, &mut state.email_box, right_x, right_y + email_top, right_w, widget_h);
+            right_y += widget_h + email_top + field_gap;
+
+            // Password textbox
+            let password_top = state.password_box.top_room();
+            state.password_box.set_row_rect(right_x, right_w);
+            clear_ui::layout::render_widget(&mut pc, &mut state.password_box, right_x, right_y + password_top, right_w, widget_h);
+            right_y += widget_h + password_top + field_gap;
+
+            // IMAP Server textbox
+            let imap_top = state.imap_box.top_room();
+            state.imap_box.set_row_rect(right_x, right_w);
+            clear_ui::layout::render_widget(&mut pc, &mut state.imap_box, right_x, right_y + imap_top, right_w, widget_h);
+            right_y += widget_h + imap_top + field_gap;
+
+            // SMTP Server textbox
+            let smtp_top = state.smtp_box.top_room();
+            state.smtp_box.set_row_rect(right_x, right_w);
+            clear_ui::layout::render_widget(&mut pc, &mut state.smtp_box, right_x, right_y + smtp_top, right_w, widget_h);
+            right_y += widget_h + smtp_top + field_gap;
+
+            let helper_w = (right_w - 8.0) / 2.0;
+            pc.button(
+                "Login (Google)",
+                right_x,
+                right_y,
+                helper_w,
+                row_h,
+                [0.15, 0.15, 0.25, 1.0],
+                [0.25, 0.25, 0.35, 1.0],
+                [1.0, 1.0, 1.0, 1.0],
+                AppAction::Accounts(AccountsMessage::GoogleLoginInit),
+            );
+            pc.button(
+                "Login (iCloud)",
+                right_x + helper_w + 8.0,
+                right_y,
+                helper_w,
+                row_h,
+                [0.15, 0.15, 0.25, 1.0],
+                [0.25, 0.25, 0.35, 1.0],
+                [1.0, 1.0, 1.0, 1.0],
+                AppAction::Accounts(AccountsMessage::ICloudLoginHelp),
+            );
+            right_y += row_h + 16.0;
+
+            pc.button(
+                "Save Account",
+                right_x,
+                right_y,
+                helper_w,
+                row_h,
+                [0.13, 0.18, 0.14, 1.0],
+                [0.25, 0.30, 0.26, 1.0],
+                [1.0, 1.0, 1.0, 1.0],
+                AppAction::Accounts(AccountsMessage::AddAccountSave),
+            );
+            pc.button(
+                "Cancel",
+                right_x + helper_w + 8.0,
+                right_y,
+                helper_w,
+                row_h,
+                [0.33, 0.20, 0.20, 1.0],
+                [0.45, 0.25, 0.25, 1.0],
+                [1.0, 1.0, 1.0, 1.0],
+                AppAction::Accounts(AccountsMessage::AddAccountCancel),
+            );
+            right_y += row_h + 12.0;
+        } else if let Some(selected_idx) = state.selected_idx {
+            if selected_idx < state.accounts.len() {
+                let acc = &state.accounts[selected_idx];
+
+                pc.text("Account Details", right_x, right_y, 14.0, [0.35, 0.65, 0.90, 1.0]);
+                right_y += 28.0;
+
+                pc.text(&format!("Email Address:   {}", acc.email), right_x + 8.0, right_y, 12.0, [0.90, 0.90, 0.95, 1.0]);
+                right_y += 18.0;
+
+                let auth_type = if acc.is_oauth { "OAuth2 (Google)" } else { "Password-based" };
+                pc.text(&format!("Authentication:  {}", auth_type), right_x + 8.0, right_y, 12.0, [0.83, 0.83, 0.83, 1.0]);
+                right_y += 18.0;
+
+                pc.text(&format!("IMAP Server:     {}", acc.imap), right_x + 8.0, right_y, 12.0, [0.83, 0.83, 0.83, 1.0]);
+                right_y += 18.0;
+
+                pc.text(&format!("SMTP Server:     {}", acc.smtp), right_x + 8.0, right_y, 12.0, [0.83, 0.83, 0.83, 1.0]);
+                right_y += 24.0;
+
+                if acc.is_oauth {
+                    pc.button(
+                        "Click to Login (Browser)",
+                        right_x + 8.0,
+                        right_y,
+                        200.0,
+                        row_h,
+                        [0.15, 0.15, 0.25, 1.0],
+                        [0.25, 0.25, 0.35, 1.0],
+                        [1.0, 1.0, 1.0, 1.0],
+                        AppAction::Accounts(AccountsMessage::GoogleLoginInit),
+                    );
+                    right_y += row_h + 12.0;
+                }
+            }
+        } else {
+            pc.text("Select an account to view details, or click Add Account.", right_x, right_y, 12.0, TEXT_DIM);
+            right_y += 20.0;
+        }
+
+        if let Some(ref msg) = state.status_msg {
+            pc.text(msg, right_x, right_y + 12.0, 12.0, [0.56, 0.83, 0.56, 1.0]);
+            right_y += 24.0;
+        }
+
+        sec.content_y = left_y.max(right_y) + 24.0;
+    }
+
+    sec.finish(&mut pc);
+    pc
+}
+
+pub fn update(state: &mut AccountsState, msg: AccountsMessage) {
+    match msg {
+        AccountsMessage::Refreshed(accs) => {
+            state.loaded = true;
+            state.accounts = accs;
+            if state.selected_idx.is_none() && !state.accounts.is_empty() {
+                state.selected_idx = Some(0);
+            } else if let Some(idx) = state.selected_idx {
+                if idx >= state.accounts.len() {
+                    state.selected_idx = if state.accounts.is_empty() { None } else { Some(0) };
+                }
+            }
+        }
+        AccountsMessage::SelectAccount(idx) => {
+            state.selected_idx = Some(idx);
+            state.adding_new = false;
+        }
+        AccountsMessage::AddAccountStart => {
+            state.adding_new = true;
+            state.email_box.text = String::new();
+            state.email_box.edit_buffer = String::new();
+            state.password_box.text = String::new();
+            state.password_box.edit_buffer = String::new();
+            state.imap_box.text = String::new();
+            state.imap_box.edit_buffer = String::new();
+            state.smtp_box.text = String::new();
+            state.smtp_box.edit_buffer = String::new();
+        }
+        AccountsMessage::AddAccountCancel => {
+            state.adding_new = false;
+            state.selected_idx = if state.accounts.is_empty() { None } else { Some(0) };
+        }
+        AccountsMessage::AddAccountSave => {
+            let email = state.email_box.text.trim().to_string();
+            let password = state.password_box.text.trim().to_string();
+            let imap = state.imap_box.text.trim().to_string();
+            let smtp = state.smtp_box.text.trim().to_string();
+
+            if email.is_empty() || password.is_empty() || imap.is_empty() || smtp.is_empty() {
+                state.status_msg = Some("All fields must be filled!".to_string());
+                return;
+            }
+
+            let new_acc = AccountInfo {
+                email: email.clone(),
+                imap,
+                smtp,
+                is_default: state.accounts.is_empty(),
+                password,
+                is_oauth: false,
+                access_token: None,
+                refresh_token: None,
+                token_expiry: None,
+                client_id: None,
+                client_secret: None,
+            };
+
+            if let Some(pos) = state.accounts.iter().position(|a| a.email == email) {
+                state.accounts[pos] = new_acc;
+            } else {
+                state.accounts.push(new_acc);
+            }
+
+            save_accounts(&state.accounts);
+            state.adding_new = false;
+            state.selected_idx = state.accounts.iter().position(|a| a.email == email);
+            state.status_msg = Some("Account saved successfully!".to_string());
+        }
+        AccountsMessage::DeleteAccount(idx) => {
+            if idx < state.accounts.len() {
+                let deleted = state.accounts.remove(idx);
+                if deleted.is_default && !state.accounts.is_empty() {
+                    state.accounts[0].is_default = true;
+                }
+                save_accounts(&state.accounts);
+                state.selected_idx = if state.accounts.is_empty() { None } else { Some(0) };
+                state.status_msg = Some("Account deleted successfully!".to_string());
+            }
+        }
+        AccountsMessage::MakeDefault(idx) => {
+            if idx < state.accounts.len() {
+                for (i, acc) in state.accounts.iter_mut().enumerate() {
+                    acc.is_default = i == idx;
+                }
+                save_accounts(&state.accounts);
+                state.status_msg = Some("Default account updated!".to_string());
+            }
+        }
+        AccountsMessage::StatusMessage(msg) => {
+            state.status_msg = Some(msg);
+        }
+        AccountsMessage::GoogleLoginInit => {
+            state.status_msg = Some("Starting Google Sign-In...".to_string());
+        }
+        AccountsMessage::GoogleLoginSuccess(new_acc) => {
+            let email = new_acc.email.clone();
+            if let Some(pos) = state.accounts.iter().position(|a| a.email == email) {
+                state.accounts[pos] = new_acc;
+            } else {
+                state.accounts.push(new_acc);
+            }
+            save_accounts(&state.accounts);
+            state.adding_new = false;
+            state.selected_idx = state.accounts.iter().position(|a| a.email == email);
+            state.status_msg = Some("Google account authenticated!".to_string());
+        }
+        AccountsMessage::ICloudLoginHelp => {
+            let _ = std::process::Command::new("xdg-open").arg("https://appleid.apple.com/").spawn();
+            state.status_msg = Some("Generate iCloud App Password...".to_string());
+        }
+    }
+}
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 5921d4c..de38e69 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -14,9 +14,11 @@ pub mod typeface;
 pub mod services;
 pub mod colors;
 pub mod screensaver;
+pub mod accounts;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum Page {
+    Accounts,
     Audio,
     Radios,
     Services,
@@ -35,7 +37,8 @@ pub enum Page {
 }
 
 impl Page {
-    pub const ALL: [Page; 15] = [
+    pub const ALL: [Page; 16] = [
+        Page::Accounts,
         Page::Audio,
         Page::Backup,
         Page::Colors,
@@ -55,6 +58,7 @@ impl Page {
 
     pub fn label(self) -> &'static str {
         match self {
+            Page::Accounts => "Accounts",
             Page::Audio => "Audio",
             Page::Radios => "Radios",
             Page::Services => "Services",
diff --git a/src/pages/typeface.rs b/src/pages/typeface.rs
index e53f283..5520975 100644
--- a/src/pages/typeface.rs
+++ b/src/pages/typeface.rs
@@ -18,6 +18,7 @@ pub struct TypefaceState {
     pub status_interface: String,
     pub fuzzel: String,
     pub terminal: String,
+    pub paginator: String,
     pub all_fonts: Vec<String>,
     pub mono_fonts: Vec<String>,
     pub sans_box: TextBox,
@@ -27,6 +28,7 @@ pub struct TypefaceState {
     pub status_box: TextBox,
     pub fuzzel_box: TextBox,
     pub terminal_box: TextBox,
+    pub paginator_box: TextBox,
     pub search_box: TextBox,
     pub selected_font: Option<String>,
     pub list_box: ScrollingList,
@@ -34,10 +36,12 @@ pub struct TypefaceState {
     pub status_menu: Dropdown,
     pub fuzzel_menu: Dropdown,
     pub terminal_menu: Dropdown,
+    pub paginator_menu: Dropdown,
     pub borders_size_box: Spinbox,
     pub status_size_box: Spinbox,
     pub fuzzel_size_box: Spinbox,
     pub terminal_size_box: Spinbox,
+    pub paginator_size_box: Spinbox,
     pub font_buttons: Vec<Button>,
     pub copy_buttons: Vec<Button>,
 }
@@ -53,6 +57,7 @@ impl Default for TypefaceState {
             status_interface: String::new(),
             fuzzel: String::new(),
             terminal: String::new(),
+            paginator: String::new(),
             all_fonts: Vec::new(),
             mono_fonts: Vec::new(),
             sans_box: TextBox::default(),
@@ -62,6 +67,7 @@ impl Default for TypefaceState {
             status_box: TextBox::default(),
             fuzzel_box: TextBox::default(),
             terminal_box: TextBox::default(),
+            paginator_box: TextBox::default(),
             search_box: TextBox::default(),
             selected_font: None,
             list_box: ScrollingList::new(24.0, 4.0),
@@ -69,10 +75,12 @@ impl Default for TypefaceState {
             status_menu: Dropdown::default(),
             fuzzel_menu: Dropdown::default(),
             terminal_menu: Dropdown::default(),
+            paginator_menu: Dropdown::default(),
             borders_size_box: Spinbox::new(11, 6, 72, 1),
             status_size_box: Spinbox::new(11, 6, 72, 1),
             fuzzel_size_box: Spinbox::new(14, 6, 72, 1),
             terminal_size_box: Spinbox::new(12, 6, 72, 1),
+            paginator_size_box: Spinbox::new(12, 6, 72, 1),
             font_buttons: Vec::new(),
             copy_buttons: Vec::new(),
         }
@@ -89,6 +97,7 @@ pub enum TypefaceMessage {
     SetStatus(String),
     SetFuzzel(String),
     SetTerminal(String),
+    SetPaginator(String),
     SetSearch(String),
     SelectFont(String),
     CopyFontName(String),
@@ -96,10 +105,12 @@ pub enum TypefaceMessage {
     SetStatusMenu(usize),
     SetFuzzelMenu(usize),
     SetTerminalMenu(usize),
+    SetPaginatorMenu(usize),
     SetBordersSize(i32),
     SetStatusSize(i32),
     SetFuzzelSize(i32),
     SetTerminalSize(i32),
+    SetPaginatorSize(i32),
 }
 
 fn parse_font_for_alias(content: &str, alias: &str) -> Option<String> {
@@ -128,7 +139,7 @@ fn parse_font_for_alias(content: &str, alias: &str) -> Option<String> {
     None
 }
 
-pub fn read_preferred_fonts() -> (String, String, String, String, String, String, String) {
+pub fn read_preferred_fonts() -> (String, String, String, String, String, String, String, String) {
     let content = fs::read_to_string(FONTS_CONF_PATH).unwrap_or_default();
     
     let sans = parse_font_for_alias(&content, "sans-serif").unwrap_or_else(|| "Noto Sans".to_string());
@@ -138,8 +149,9 @@ pub fn read_preferred_fonts() -> (String, String, String, String, String, String
     let status = parse_font_for_alias(&content, "status-interface").unwrap_or_else(|| "Noto Sans".to_string());
     let fuzzel_font = parse_font_for_alias(&content, "fuzzel").unwrap_or_else(|| "Noto Sans".to_string());
     let term = parse_font_for_alias(&content, "terminal").unwrap_or_else(|| "Noto Sans Mono".to_string());
+    let paginator = parse_font_for_alias(&content, "paginator-tab-labels").unwrap_or_else(|| "Noto Sans".to_string());
     
-    (sans, serif, mono, borders, status, fuzzel_font, term)
+    (sans, serif, mono, borders, status, fuzzel_font, term, paginator)
 }
 
 pub fn save_preferred_fonts(
@@ -150,6 +162,7 @@ pub fn save_preferred_fonts(
     status: &str,
     fuzzel: &str,
     terminal: &str,
+    paginator: &str,
 ) {
     let content = fs::read_to_string(FONTS_CONF_PATH).unwrap_or_default();
     
@@ -228,6 +241,14 @@ pub fn save_preferred_fonts(
     new_content.push_str(&format!("            <string>{}</string>\n", terminal));
     new_content.push_str("        </edit>\n");
     new_content.push_str("    </match>\n");
+
+    // Paginator
+    new_content.push_str("    <match target=\"pattern\">\n");
+    new_content.push_str("        <test qual=\"any\" name=\"family\"><string>paginator-tab-labels</string></test>\n");
+    new_content.push_str("        <edit name=\"family\" mode=\"assign\" binding=\"same\">\n");
+    new_content.push_str(&format!("            <string>{}</string>\n", paginator));
+    new_content.push_str("        </edit>\n");
+    new_content.push_str("    </match>\n");
     
     new_content.push_str("</fontconfig>\n");
     
@@ -391,6 +412,15 @@ fn write_terminal_size(size: u16) {
     let _ = fs::write(path, new_lines.join("\n"));
 }
 
+fn read_paginator_size() -> Option<u16> {
+    let content = fs::read_to_string("/home/lsgalante/.config/ccec/config.toml").ok()?;
+    Some(parse_u16_from(&content, "paginator_font_size", 12))
+}
+
+fn write_paginator_size(size: u16) {
+    write_config_value("paginator_font_size", &size.to_string());
+}
+
 fn parse_families(output: Option<std::process::Output>) -> Vec<String> {
     let mut families = Vec::new();
     if let Some(o) = output {
@@ -410,7 +440,7 @@ fn parse_families(output: Option<std::process::Output>) -> Vec<String> {
 }
 
 pub async fn fetch_typeface_state() -> TypefaceState {
-    let (sans, serif, mono, borders, status, fuzzel_font, term) = read_preferred_fonts();
+    let (sans, serif, mono, borders, status, fuzzel_font, term, paginator_font) = read_preferred_fonts();
     
     let all_output = tokio::process::Command::new("fc-list")
         .args([":", "family"])
@@ -440,6 +470,7 @@ pub async fn fetch_typeface_state() -> TypefaceState {
     let status_idx = determine_dropdown_index(&status, &sans, &serif, &mono);
     let fuzzel_idx = determine_dropdown_index(&fuzzel_font, &sans, &serif, &mono);
     let terminal_idx = determine_dropdown_index(&term, &sans, &serif, &mono);
+    let paginator_idx = determine_dropdown_index(&paginator_font, &sans, &serif, &mono);
 
     let menu_options = vec![
         "Sans-Serif".to_string(),
@@ -460,10 +491,14 @@ pub async fn fetch_typeface_state() -> TypefaceState {
     let mut terminal_box = TextBox::new(term.clone()).with_label("Terminal").with_width(300.0);
     terminal_box.disabled = terminal_idx != 3;
 
+    let mut paginator_box = TextBox::new(paginator_font.clone()).with_label("Paginator Tab Labels").with_width(300.0);
+    paginator_box.disabled = paginator_idx != 3;
+
     let borders_size = read_border_font_size().unwrap_or(11);
     let status_size = read_status_size().unwrap_or(11);
     let fuzzel_size = read_fuzzel_size().unwrap_or(14);
     let terminal_size = read_terminal_size().unwrap_or(12);
+    let paginator_size = read_paginator_size().unwrap_or(12);
 
     TypefaceState {
         loaded: true,
@@ -474,6 +509,7 @@ pub async fn fetch_typeface_state() -> TypefaceState {
         status_interface: status,
         fuzzel: fuzzel_font,
         terminal: term,
+        paginator: paginator_font,
         all_fonts,
         mono_fonts,
         sans_box: TextBox::new(sans).with_label("Sans-Serif"),
@@ -483,6 +519,7 @@ pub async fn fetch_typeface_state() -> TypefaceState {
         status_box,
         fuzzel_box,
         terminal_box,
+        paginator_box,
         search_box: TextBox::new(String::new()).with_label("Filter Fonts"),
         selected_font,
         list_box: ScrollingList::new(24.0, 4.0),
@@ -490,10 +527,12 @@ pub async fn fetch_typeface_state() -> TypefaceState {
         status_menu: Dropdown::new(menu_options.clone(), status_idx),
         fuzzel_menu: Dropdown::new(menu_options.clone(), fuzzel_idx),
         terminal_menu: Dropdown::new(menu_options.clone(), terminal_idx),
+        paginator_menu: Dropdown::new(menu_options.clone(), paginator_idx),
         borders_size_box: Spinbox::new(borders_size as i32, 6, 72, 1),
         status_size_box: Spinbox::new(status_size as i32, 6, 72, 1),
         fuzzel_size_box: Spinbox::new(fuzzel_size as i32, 6, 72, 1),
         terminal_size_box: Spinbox::new(terminal_size as i32, 6, 72, 1),
+        paginator_size_box: Spinbox::new(paginator_size as i32, 6, 72, 1),
         font_buttons: Vec::new(),
         copy_buttons: Vec::new(),
     }
@@ -593,6 +632,17 @@ pub fn view(state: &mut TypefaceState, cx: f32, cy: f32, cw: f32, _ch: f32, sec_
         clear_ui::layout::render_widget(&mut pc, &mut state.terminal_box, cx + 12.0 + dropdown_w + 12.0, start_y + top_room, textbox_w, widget_h);
         state.terminal_size_box.set_row_rect(spin_row_x, spinbox_row_w);
         clear_ui::layout::render_widget(&mut pc, &mut state.terminal_size_box, spin_x, start_y + top_room, spinbox_w, widget_h);
+        sec.spacing(widget_h + top_room + 12.0);
+
+        // Paginator Tab Labels
+        let start_y = sec.ay();
+        let top_room = state.paginator_box.top_room();
+        state.paginator_menu.set_row_rect(cx + 8.0, menu_row_w);
+        clear_ui::layout::render_widget(&mut pc, &mut state.paginator_menu, cx + 12.0, start_y + top_room, dropdown_w, widget_h);
+        state.paginator_box.set_row_rect(box_row_x, box_row_w);
+        clear_ui::layout::render_widget(&mut pc, &mut state.paginator_box, cx + 12.0 + dropdown_w + 12.0, start_y + top_room, textbox_w, widget_h);
+        state.paginator_size_box.set_row_rect(spin_row_x, spinbox_row_w);
+        clear_ui::layout::render_widget(&mut pc, &mut state.paginator_size_box, spin_x, start_y + top_room, spinbox_w, widget_h);
         sec.spacing(widget_h + top_room + 8.0);
     }
     let prog_focused = sec_focused.get(1).copied().unwrap_or(false);
@@ -840,6 +890,11 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 state.terminal_box = new.terminal_box;
                 state.terminal_menu = new.terminal_menu;
             }
+            if !state.paginator_box.editing {
+                state.paginator = new.paginator.clone();
+                state.paginator_box = new.paginator_box;
+                state.paginator_menu = new.paginator_menu;
+            }
             if !state.search_box.editing {
                 state.search_box = new.search_box;
             }
@@ -847,6 +902,7 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
             state.status_size_box = new.status_size_box;
             state.fuzzel_size_box = new.fuzzel_size_box;
             state.terminal_size_box = new.terminal_size_box;
+            state.paginator_size_box = new.paginator_size_box;
             state.font_buttons = new.font_buttons;
             state.copy_buttons = new.copy_buttons;
             let old_scroll = state.list_box.scroll_y();
@@ -872,6 +928,10 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 state.terminal = state.sans_serif.clone();
                 state.terminal_box.text = state.sans_serif.clone();
             }
+            if state.paginator_menu.selected == 0 {
+                state.paginator = state.sans_serif.clone();
+                state.paginator_box.text = state.sans_serif.clone();
+            }
             save_preferred_fonts(
                 &state.sans_serif,
                 &state.serif,
@@ -880,6 +940,7 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 &state.status_interface,
                 &state.fuzzel,
                 &state.terminal,
+                &state.paginator,
             );
         }
         TypefaceMessage::SetSerif(serif) => {
@@ -901,6 +962,10 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 state.terminal = state.serif.clone();
                 state.terminal_box.text = state.serif.clone();
             }
+            if state.paginator_menu.selected == 1 {
+                state.paginator = state.serif.clone();
+                state.paginator_box.text = state.serif.clone();
+            }
             save_preferred_fonts(
                 &state.sans_serif,
                 &state.serif,
@@ -909,6 +974,7 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 &state.status_interface,
                 &state.fuzzel,
                 &state.terminal,
+                &state.paginator,
             );
         }
         TypefaceMessage::SetMono(mono) => {
@@ -930,6 +996,10 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 state.terminal = state.monospace.clone();
                 state.terminal_box.text = state.monospace.clone();
             }
+            if state.paginator_menu.selected == 2 {
+                state.paginator = state.monospace.clone();
+                state.paginator_box.text = state.monospace.clone();
+            }
             save_preferred_fonts(
                 &state.sans_serif,
                 &state.serif,
@@ -938,6 +1008,7 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 &state.status_interface,
                 &state.fuzzel,
                 &state.terminal,
+                &state.paginator,
             );
         }
         TypefaceMessage::SetBorders(borders) => {
@@ -951,6 +1022,7 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 &state.status_interface,
                 &state.fuzzel,
                 &state.terminal,
+                &state.paginator,
             );
         }
         TypefaceMessage::SetStatus(status) => {
@@ -964,6 +1036,7 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 &state.status_interface,
                 &state.fuzzel,
                 &state.terminal,
+                &state.paginator,
             );
         }
         TypefaceMessage::SetFuzzel(fuzzel) => {
@@ -977,6 +1050,7 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 &state.status_interface,
                 &state.fuzzel,
                 &state.terminal,
+                &state.paginator,
             );
         }
         TypefaceMessage::SetTerminal(term) => {
@@ -990,6 +1064,21 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 &state.status_interface,
                 &state.fuzzel,
                 &state.terminal,
+                &state.paginator,
+            );
+        }
+        TypefaceMessage::SetPaginator(paginator) => {
+            state.paginator = paginator.clone();
+            state.paginator_box.text = paginator;
+            save_preferred_fonts(
+                &state.sans_serif,
+                &state.serif,
+                &state.monospace,
+                &state.window_borders,
+                &state.status_interface,
+                &state.fuzzel,
+                &state.terminal,
+                &state.paginator,
             );
         }
         TypefaceMessage::SetSearch(search) => {
@@ -1068,6 +1157,7 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 &state.status_interface,
                 &state.fuzzel,
                 &state.terminal,
+                &state.paginator,
             );
         }
         TypefaceMessage::SetStatusMenu(idx) => {
@@ -1091,6 +1181,7 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 &state.status_interface,
                 &state.fuzzel,
                 &state.terminal,
+                &state.paginator,
             );
         }
         TypefaceMessage::SetFuzzelMenu(idx) => {
@@ -1114,6 +1205,7 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 &state.status_interface,
                 &state.fuzzel,
                 &state.terminal,
+                &state.paginator,
             );
         }
         TypefaceMessage::SetTerminalMenu(idx) => {
@@ -1137,6 +1229,31 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
                 &state.status_interface,
                 &state.fuzzel,
                 &state.terminal,
+                &state.paginator,
+            );
+        }
+        TypefaceMessage::SetPaginatorMenu(idx) => {
+            state.paginator_menu.selected = idx;
+            state.paginator_box.disabled = idx != 3;
+            if idx == 0 {
+                state.paginator = state.sans_serif.clone();
+                state.paginator_box.text = state.sans_serif.clone();
+            } else if idx == 1 {
+                state.paginator = state.serif.clone();
+                state.paginator_box.text = state.serif.clone();
+            } else if idx == 2 {
+                state.paginator = state.monospace.clone();
+                state.paginator_box.text = state.monospace.clone();
+            }
+            save_preferred_fonts(
+                &state.sans_serif,
+                &state.serif,
+                &state.monospace,
+                &state.window_borders,
+                &state.status_interface,
+                &state.fuzzel,
+                &state.terminal,
+                &state.paginator,
             );
         }
         TypefaceMessage::SetBordersSize(val) => {
@@ -1156,6 +1273,10 @@ pub fn update(state: &mut TypefaceState, msg: TypefaceMessage) {
             state.terminal_size_box.value = val;
             write_terminal_size(val as u16);
         }
+        TypefaceMessage::SetPaginatorSize(val) => {
+            state.paginator_size_box.value = val;
+            write_paginator_size(val as u16);
+        }
     }
 }