system settings
git clone https://git.lucas.co/cce-system-interface.git
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(¶ms)
+ .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);
+ }
}
}