diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc000ec..cd4002b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build CLI - working-directory: cli - run: cargo build + run: cargo build -p fuso linux: runs-on: ubuntu-latest @@ -22,5 +21,4 @@ jobs: - name: Install GTK4 dev dependencies run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev - name: Build Linux app - working-directory: linux - run: cargo build + run: cargo build -p fuso-linux diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 525f33f..3227c3b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,8 +12,12 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Publish - working-directory: cli + - name: Publish fuso-core env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: cargo publish + run: cargo publish -p fuso-core + + - name: Publish fuso + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: sleep 30 && cargo publish -p fuso diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bcd20c5..340283d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,15 +20,17 @@ jobs: - uses: actions/checkout@v4 - name: Build Swift app + working-directory: macos run: swift build -c release - name: Bundle app + working-directory: macos run: | mkdir -p Fuso.app/Contents/MacOS mkdir -p Fuso.app/Contents/Resources cp .build/release/Fuso Fuso.app/Contents/MacOS/Fuso cp Sources/Info.plist Fuso.app/Contents/Info.plist - zip -r Fuso-macos-app.zip Fuso.app + zip -r ../Fuso-macos-app.zip Fuso.app - uses: actions/upload-artifact@v4 with: @@ -67,14 +69,13 @@ jobs: echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml - name: Build CLI - working-directory: cli - run: cargo build --release --target ${{ matrix.target }} + run: cargo build --release -p fuso --target ${{ matrix.target }} - name: Package run: | - cd cli/target/${{ matrix.target }}/release + cd target/${{ matrix.target }}/release tar czf ${{ matrix.artifact }}.tar.gz fuso - mv ${{ matrix.artifact }}.tar.gz ../../../../ + mv ${{ matrix.artifact }}.tar.gz ../../../ - uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index 59bf5d4..fd692b1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,4 @@ DerivedData/ .DS_Store Fuso.app/ thoughts/ -cli/target/ -linux/target/ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..04ada88 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1141 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cairo-rs" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fuso" +version = "0.1.0" +dependencies = [ + "chrono", + "chrono-tz", + "fuso-core", +] + +[[package]] +name = "fuso-core" +version = "0.1.0" +dependencies = [ + "chrono", + "chrono-tz", + "dirs", + "iana-time-zone", + "serde", + "serde_json", +] + +[[package]] +name = "fuso-linux" +version = "0.1.0" +dependencies = [ + "chrono", + "chrono-tz", + "fuso-core", + "glib", + "gtk4", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[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 = "gio" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.59.0", +] + +[[package]] +name = "glib" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" +dependencies = [ + "gio", + "glib", + "libc", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "7.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +dependencies = [ + "indexmap", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..08fc9ad --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +members = [ + "crates/fuso-core", + "cli", + "linux", +] +resolver = "2" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f9f1b2d..e4cc85f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -11,9 +11,6 @@ keywords = ["timezone", "cli", "worldclock", "team"] categories = ["command-line-utilities"] [dependencies] +fuso-core = { path = "../crates/fuso-core" } chrono = "0.4" chrono-tz = "0.10" -dirs = "6" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -iana-time-zone = "0.1" diff --git a/cli/src/main.rs b/cli/src/main.rs index 42efb49..e133e5e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,254 +1,18 @@ -use chrono::{Datelike, Timelike}; use chrono_tz::Tz; -use serde::Deserialize; -use std::collections::HashMap; -use std::fs; -use std::path::PathBuf; - -#[derive(Deserialize)] -struct Config { - clocks: Vec, -} - -#[derive(Deserialize)] -struct ClockEntry { - name: String, - city: String, - timezone: String, - flag: Option, - status: Option, -} - -#[derive(Deserialize)] -struct StatusSchedule { - blocks: HashMap, - months: HashMap, -} - -#[derive(Deserialize)] -struct StatusBlock { - label: String, - start: String, - end: String, -} - -enum Availability { - Busy(String), - Available, - DayOff, -} - -fn config_path() -> PathBuf { - let home = dirs::home_dir().expect("could not find home directory"); - home.join(".config/fuso/clocks.json") -} - -fn default_config() -> Config { - Config { - clocks: vec![ClockEntry { - name: "Me".into(), - city: "New York".into(), - timezone: "America/New_York".into(), - flag: None, - status: None, - }], - } -} - -fn load_config() -> Config { - let path = config_path(); - - if !path.exists() { - let config = default_config(); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).ok(); - } - let json = serde_json::to_string_pretty(&serde_json::json!({ - "clocks": [{ - "name": "Me", - "city": "New York", - "timezone": "America/New_York" - }] - })) - .unwrap(); - fs::write(&path, json).ok(); - return config; - } - - let data = fs::read_to_string(&path).expect("could not read config file"); - serde_json::from_str(&data).expect("invalid config format") -} - -fn timezone_to_flag(tz: &str) -> &'static str { - match tz { - s if s.starts_with("America/New_York") - | s.starts_with("America/Chicago") - | s.starts_with("America/Denver") - | s.starts_with("America/Los_Angeles") - | s.starts_with("America/Phoenix") - | s.starts_with("America/Anchorage") - | s.starts_with("Pacific/Honolulu") => - { - "🇺🇸" - } - s if s.starts_with("America/Sao_Paulo") - | s.starts_with("America/Fortaleza") - | s.starts_with("America/Manaus") - | s.starts_with("America/Bahia") - | s.starts_with("America/Belem") - | s.starts_with("America/Recife") - | s.starts_with("America/Cuiaba") - | s.starts_with("America/Campo_Grande") - | s.starts_with("America/Rio_Branco") - | s.starts_with("America/Porto_Velho") - | s.starts_with("America/Maceio") - | s.starts_with("America/Araguaina") => - { - "🇧🇷" - } - "Asia/Tokyo" => "🇯🇵", - "Europe/London" | "Europe/Dublin" => "🇬🇧", - "Europe/Paris" => "🇫🇷", - "Europe/Berlin" => "🇩🇪", - "Europe/Rome" => "🇮🇹", - "Europe/Madrid" => "🇪🇸", - "Europe/Lisbon" => "🇵🇹", - "Europe/Amsterdam" => "🇳🇱", - "Europe/Zurich" => "🇨🇭", - "Europe/Vienna" => "🇦🇹", - "Europe/Prague" => "🇨🇿", - "Europe/Warsaw" => "🇵🇱", - "Europe/Stockholm" => "🇸🇪", - "Europe/Oslo" => "🇳🇴", - "Europe/Copenhagen" => "🇩🇰", - "Europe/Helsinki" => "🇫🇮", - "Europe/Moscow" => "🇷🇺", - "Europe/Istanbul" => "🇹🇷", - "Asia/Shanghai" => "🇨🇳", - "Asia/Hong_Kong" => "🇭🇰", - "Asia/Seoul" => "🇰🇷", - "Asia/Singapore" => "🇸🇬", - "Asia/Kolkata" => "🇮🇳", - "Asia/Dubai" => "🇦🇪", - "Asia/Bangkok" => "🇹🇭", - "Asia/Jakarta" => "🇮🇩", - "Asia/Taipei" => "🇹🇼", - "Asia/Riyadh" => "🇸🇦", - "Asia/Jerusalem" => "🇮🇱", - "Australia/Sydney" | "Australia/Melbourne" | "Australia/Perth" | "Australia/Brisbane" => { - "🇦🇺" - } - "Pacific/Auckland" => "🇳🇿", - "America/Toronto" | "America/Vancouver" | "America/Edmonton" => "🇨🇦", - "America/Mexico_City" => "🇲🇽", - "America/Argentina/Buenos_Aires" => "🇦🇷", - "America/Santiago" => "🇨🇱", - "America/Bogota" => "🇨🇴", - "America/Lima" => "🇵🇪", - "Africa/Johannesburg" => "🇿🇦", - "Africa/Lagos" => "🇳🇬", - "Africa/Cairo" => "🇪🇬", - "Africa/Nairobi" => "🇰🇪", - _ => "🌍", - } -} - -fn parse_time(t: &str) -> u32 { - let parts: Vec = t.split(':').filter_map(|p| p.parse().ok()).collect(); - if parts.len() == 2 { - parts[0] * 60 + parts[1] - } else { - 0 - } -} - -fn current_availability(entry: &ClockEntry, now: chrono::DateTime) -> Option { - let schedule = entry.status.as_ref()?; - - let month_key = format!("{}-{:02}", now.year(), now.month()); - let month_str = schedule.months.get(&month_key)?; - let day = now.day() as usize; - - if day < 1 || day > month_str.len() { - return None; - } - - let block_id = &month_str[day - 1..day]; - let now_minutes = now.hour() * 60 + now.minute(); - - if block_id != "0" { - if let Some(block) = schedule.blocks.get(block_id) { - let start = parse_time(&block.start); - let end = parse_time(&block.end); - - if end > start { - if now_minutes >= start && now_minutes < end { - return Some(Availability::Busy(block.label.clone())); - } - } else if end < start && now_minutes >= start { - return Some(Availability::Busy(block.label.clone())); - } - } - } - - // Check yesterday's cross-midnight block - let yesterday = now - chrono::Duration::days(1); - let y_month_key = format!("{}-{:02}", yesterday.year(), yesterday.month()); - if let Some(y_month_str) = schedule.months.get(&y_month_key) { - let y_day = yesterday.day() as usize; - if y_day >= 1 && y_day <= y_month_str.len() { - let y_block_id = &y_month_str[y_day - 1..y_day]; - if y_block_id != "0" { - if let Some(y_block) = schedule.blocks.get(y_block_id) { - let y_start = parse_time(&y_block.start); - let y_end = parse_time(&y_block.end); - if y_end < y_start && now_minutes < y_end { - return Some(Availability::Busy(y_block.label.clone())); - } - } - } - } - } - - if block_id == "0" { - Some(Availability::DayOff) - } else { - Some(Availability::Available) - } -} - -fn relative_offset(local_tz: Tz, remote_tz: Tz, now: chrono::DateTime) -> String { - use chrono::Offset; - let local_offset = now.with_timezone(&local_tz).offset().fix().local_minus_utc(); - let remote_offset = now.with_timezone(&remote_tz).offset().fix().local_minus_utc(); - let diff = remote_offset - local_offset; - let hours = diff / 3600; - let minutes = (diff.abs() % 3600) / 60; - - if hours == 0 && minutes == 0 { - return "local".into(); - } - if minutes > 0 { - format!("{:+}:{:02}h", hours, minutes) - } else { - format!("{:+}h", hours) - } -} +use fuso_core::{ + current_availability, load_config, local_tz, relative_offset, timezone_to_flag, Availability, +}; fn main() { let config = load_config(); let now_utc = chrono::Utc::now(); - let local_tz: Tz = iana_time_zone::get_timezone() - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(chrono_tz::UTC); + let ltz = local_tz(); if config.clocks.is_empty() { println!("No clocks configured. Edit ~/.config/fuso/clocks.json"); return; } - // Calculate column widths let max_name = config.clocks.iter().map(|c| c.name.len()).max().unwrap_or(0); let max_city = config.clocks.iter().map(|c| c.city.len()).max().unwrap_or(0); @@ -266,7 +30,7 @@ fn main() { let flag = entry.flag.as_deref().unwrap_or_else(|| timezone_to_flag(&entry.timezone)); let time = now_tz.format("%H:%M").to_string(); let day = now_tz.format("%a").to_string(); - let offset = relative_offset(local_tz, tz, now_utc); + let offset = relative_offset(ltz, tz, now_utc); let status = current_availability(entry, now_tz) .map(|a| match a { diff --git a/crates/fuso-core/Cargo.toml b/crates/fuso-core/Cargo.toml new file mode 100644 index 0000000..57f6e65 --- /dev/null +++ b/crates/fuso-core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fuso-core" +version = "0.1.0" +edition = "2021" +description = "Shared config, timezone, and availability logic for Fuso" +license = "MIT" + +[dependencies] +chrono = "0.4" +chrono-tz = "0.10" +dirs = "6" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +iana-time-zone = "0.1" diff --git a/crates/fuso-core/src/config.rs b/crates/fuso-core/src/config.rs new file mode 100644 index 0000000..3642087 --- /dev/null +++ b/crates/fuso-core/src/config.rs @@ -0,0 +1,140 @@ +use serde::Deserialize; +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; + +#[derive(Deserialize, Clone)] +pub struct Config { + pub clocks: Vec, +} + +#[derive(Deserialize, Clone)] +pub struct ClockEntry { + pub name: String, + pub city: String, + pub timezone: String, + pub flag: Option, + pub status: Option, +} + +#[derive(Deserialize, Clone)] +pub struct StatusSchedule { + pub blocks: HashMap, + pub months: HashMap, +} + +#[derive(Deserialize, Clone)] +pub struct StatusBlock { + pub label: String, + pub start: String, + pub end: String, +} + +pub enum Availability { + Busy(String), + Available, + DayOff, +} + +pub fn config_path() -> PathBuf { + let home = dirs::home_dir().expect("could not find home directory"); + home.join(".config/fuso/clocks.json") +} + +pub fn load_config() -> Config { + let path = config_path(); + + if !path.exists() { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).ok(); + } + let json = serde_json::to_string_pretty(&serde_json::json!({ + "clocks": [{ + "name": "Me", + "city": "New York", + "timezone": "America/New_York" + }] + })) + .unwrap(); + fs::write(&path, json).ok(); + return Config { + clocks: vec![ClockEntry { + name: "Me".into(), + city: "New York".into(), + timezone: "America/New_York".into(), + flag: None, + status: None, + }], + }; + } + + let data = fs::read_to_string(&path).expect("could not read config file"); + serde_json::from_str(&data).expect("invalid config format") +} + +fn parse_time(t: &str) -> u32 { + let parts: Vec = t.split(':').filter_map(|p| p.parse().ok()).collect(); + if parts.len() == 2 { + parts[0] * 60 + parts[1] + } else { + 0 + } +} + +pub fn current_availability( + entry: &ClockEntry, + now: chrono::DateTime, +) -> Option { + use chrono::{Datelike, Timelike}; + + let schedule = entry.status.as_ref()?; + let month_key = format!("{}-{:02}", now.year(), now.month()); + let month_str = schedule.months.get(&month_key)?; + let day = now.day() as usize; + + if day < 1 || day > month_str.len() { + return None; + } + + let block_id = &month_str[day - 1..day]; + let now_minutes = now.hour() * 60 + now.minute(); + + if block_id != "0" { + if let Some(block) = schedule.blocks.get(block_id) { + let start = parse_time(&block.start); + let end = parse_time(&block.end); + + if end > start { + if now_minutes >= start && now_minutes < end { + return Some(Availability::Busy(block.label.clone())); + } + } else if end < start && now_minutes >= start { + return Some(Availability::Busy(block.label.clone())); + } + } + } + + let yesterday = now - chrono::Duration::days(1); + let y_month_key = format!("{}-{:02}", yesterday.year(), yesterday.month()); + if let Some(y_month_str) = schedule.months.get(&y_month_key) { + let y_day = yesterday.day() as usize; + if y_day >= 1 && y_day <= y_month_str.len() { + let y_block_id = &y_month_str[y_day - 1..y_day]; + if y_block_id != "0" { + if let Some(y_block) = schedule.blocks.get(y_block_id) { + let y_start = parse_time(&y_block.start); + let y_end = parse_time(&y_block.end); + if y_end < y_start && now_minutes < y_end { + return Some(Availability::Busy(y_block.label.clone())); + } + } + } + } + } + + if block_id == "0" { + Some(Availability::DayOff) + } else { + Some(Availability::Available) + } +} diff --git a/crates/fuso-core/src/flags.rs b/crates/fuso-core/src/flags.rs new file mode 100644 index 0000000..bda0ac8 --- /dev/null +++ b/crates/fuso-core/src/flags.rs @@ -0,0 +1,73 @@ +pub fn timezone_to_flag(tz: &str) -> &'static str { + match tz { + s if s.starts_with("America/New_York") + | s.starts_with("America/Chicago") + | s.starts_with("America/Denver") + | s.starts_with("America/Los_Angeles") + | s.starts_with("America/Phoenix") + | s.starts_with("America/Anchorage") + | s.starts_with("Pacific/Honolulu") => + { + "\u{1f1fa}\u{1f1f8}" + } + s if s.starts_with("America/Sao_Paulo") + | s.starts_with("America/Fortaleza") + | s.starts_with("America/Manaus") + | s.starts_with("America/Bahia") + | s.starts_with("America/Belem") + | s.starts_with("America/Recife") + | s.starts_with("America/Cuiaba") + | s.starts_with("America/Campo_Grande") + | s.starts_with("America/Rio_Branco") + | s.starts_with("America/Porto_Velho") + | s.starts_with("America/Maceio") + | s.starts_with("America/Araguaina") => + { + "\u{1f1e7}\u{1f1f7}" + } + "Asia/Tokyo" => "\u{1f1ef}\u{1f1f5}", + "Europe/London" | "Europe/Dublin" => "\u{1f1ec}\u{1f1e7}", + "Europe/Paris" => "\u{1f1eb}\u{1f1f7}", + "Europe/Berlin" => "\u{1f1e9}\u{1f1ea}", + "Europe/Rome" => "\u{1f1ee}\u{1f1f9}", + "Europe/Madrid" => "\u{1f1ea}\u{1f1f8}", + "Europe/Lisbon" => "\u{1f1f5}\u{1f1f9}", + "Europe/Amsterdam" => "\u{1f1f3}\u{1f1f1}", + "Europe/Zurich" => "\u{1f1e8}\u{1f1ed}", + "Europe/Vienna" => "\u{1f1e6}\u{1f1f9}", + "Europe/Prague" => "\u{1f1e8}\u{1f1ff}", + "Europe/Warsaw" => "\u{1f1f5}\u{1f1f1}", + "Europe/Stockholm" => "\u{1f1f8}\u{1f1ea}", + "Europe/Oslo" => "\u{1f1f3}\u{1f1f4}", + "Europe/Copenhagen" => "\u{1f1e9}\u{1f1f0}", + "Europe/Helsinki" => "\u{1f1eb}\u{1f1ee}", + "Europe/Moscow" => "\u{1f1f7}\u{1f1fa}", + "Europe/Istanbul" => "\u{1f1f9}\u{1f1f7}", + "Asia/Shanghai" => "\u{1f1e8}\u{1f1f3}", + "Asia/Hong_Kong" => "\u{1f1ed}\u{1f1f0}", + "Asia/Seoul" => "\u{1f1f0}\u{1f1f7}", + "Asia/Singapore" => "\u{1f1f8}\u{1f1ec}", + "Asia/Kolkata" => "\u{1f1ee}\u{1f1f3}", + "Asia/Dubai" => "\u{1f1e6}\u{1f1ea}", + "Asia/Bangkok" => "\u{1f1f9}\u{1f1ed}", + "Asia/Jakarta" => "\u{1f1ee}\u{1f1e9}", + "Asia/Taipei" => "\u{1f1f9}\u{1f1fc}", + "Asia/Riyadh" => "\u{1f1f8}\u{1f1e6}", + "Asia/Jerusalem" => "\u{1f1ee}\u{1f1f1}", + "Australia/Sydney" | "Australia/Melbourne" | "Australia/Perth" | "Australia/Brisbane" => { + "\u{1f1e6}\u{1f1fa}" + } + "Pacific/Auckland" => "\u{1f1f3}\u{1f1ff}", + "America/Toronto" | "America/Vancouver" | "America/Edmonton" => "\u{1f1e8}\u{1f1e6}", + "America/Mexico_City" => "\u{1f1f2}\u{1f1fd}", + "America/Argentina/Buenos_Aires" => "\u{1f1e6}\u{1f1f7}", + "America/Santiago" => "\u{1f1e8}\u{1f1f1}", + "America/Bogota" => "\u{1f1e8}\u{1f1f4}", + "America/Lima" => "\u{1f1f5}\u{1f1ea}", + "Africa/Johannesburg" => "\u{1f1ff}\u{1f1e6}", + "Africa/Lagos" => "\u{1f1f3}\u{1f1ec}", + "Africa/Cairo" => "\u{1f1ea}\u{1f1ec}", + "Africa/Nairobi" => "\u{1f1f0}\u{1f1ea}", + _ => "\u{1f30d}", + } +} diff --git a/crates/fuso-core/src/lib.rs b/crates/fuso-core/src/lib.rs new file mode 100644 index 0000000..8478970 --- /dev/null +++ b/crates/fuso-core/src/lib.rs @@ -0,0 +1,7 @@ +mod config; +mod flags; +mod timezone; + +pub use config::*; +pub use flags::timezone_to_flag; +pub use timezone::*; diff --git a/crates/fuso-core/src/timezone.rs b/crates/fuso-core/src/timezone.rs new file mode 100644 index 0000000..4e235d6 --- /dev/null +++ b/crates/fuso-core/src/timezone.rs @@ -0,0 +1,26 @@ +use chrono::Offset; +use chrono_tz::Tz; + +pub fn relative_offset(local_tz: Tz, remote_tz: Tz, now: chrono::DateTime) -> String { + let local_offset = now.with_timezone(&local_tz).offset().fix().local_minus_utc(); + let remote_offset = now.with_timezone(&remote_tz).offset().fix().local_minus_utc(); + let diff = remote_offset - local_offset; + let hours = diff / 3600; + let minutes = (diff.abs() % 3600) / 60; + + if hours == 0 && minutes == 0 { + return "local".into(); + } + if minutes > 0 { + format!("{:+}:{:02}h", hours, minutes) + } else { + format!("{:+}h", hours) + } +} + +pub fn local_tz() -> Tz { + iana_time_zone::get_timezone() + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(chrono_tz::UTC) +} diff --git a/linux/Cargo.toml b/linux/Cargo.toml index 651a37c..c782013 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -6,11 +6,8 @@ description = "Fuso — track your team's timezones (Linux desktop app)" license = "MIT" [dependencies] +fuso-core = { path = "../crates/fuso-core" } gtk4 = "0.9" chrono = "0.4" chrono-tz = "0.10" -dirs = "6" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -iana-time-zone = "0.1" glib = "0.20" diff --git a/linux/src/config.rs b/linux/src/config.rs deleted file mode 100644 index 1750307..0000000 --- a/linux/src/config.rs +++ /dev/null @@ -1,237 +0,0 @@ -use chrono::{Datelike, Timelike}; -use chrono_tz::Tz; -use serde::Deserialize; -use std::collections::HashMap; -use std::fs; -use std::path::PathBuf; - -#[derive(Deserialize, Clone)] -pub struct Config { - pub clocks: Vec, -} - -#[derive(Deserialize, Clone)] -pub struct ClockEntry { - pub name: String, - pub city: String, - pub timezone: String, - pub flag: Option, - pub status: Option, -} - -#[derive(Deserialize, Clone)] -pub struct StatusSchedule { - pub blocks: HashMap, - pub months: HashMap, -} - -#[derive(Deserialize, Clone)] -pub struct StatusBlock { - pub label: String, - pub start: String, - pub end: String, -} - -pub enum Availability { - Busy(String), - Available, - DayOff, -} - -pub fn config_path() -> PathBuf { - let home = dirs::home_dir().expect("could not find home directory"); - home.join(".config/fuso/clocks.json") -} - -pub fn load_config() -> Config { - let path = config_path(); - - if !path.exists() { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).ok(); - } - let json = serde_json::to_string_pretty(&serde_json::json!({ - "clocks": [{ - "name": "Me", - "city": "New York", - "timezone": "America/New_York" - }] - })) - .unwrap(); - fs::write(&path, json).ok(); - return Config { - clocks: vec![ClockEntry { - name: "Me".into(), - city: "New York".into(), - timezone: "America/New_York".into(), - flag: None, - status: None, - }], - }; - } - - let data = fs::read_to_string(&path).expect("could not read config file"); - serde_json::from_str(&data).expect("invalid config format") -} - -pub fn timezone_to_flag(tz: &str) -> &'static str { - match tz { - s if s.starts_with("America/New_York") - | s.starts_with("America/Chicago") - | s.starts_with("America/Denver") - | s.starts_with("America/Los_Angeles") - | s.starts_with("America/Phoenix") - | s.starts_with("America/Anchorage") - | s.starts_with("Pacific/Honolulu") => - { - "\u{1f1fa}\u{1f1f8}" - } - s if s.starts_with("America/Sao_Paulo") - | s.starts_with("America/Fortaleza") - | s.starts_with("America/Manaus") - | s.starts_with("America/Bahia") - | s.starts_with("America/Belem") - | s.starts_with("America/Recife") - | s.starts_with("America/Cuiaba") - | s.starts_with("America/Campo_Grande") - | s.starts_with("America/Rio_Branco") - | s.starts_with("America/Porto_Velho") - | s.starts_with("America/Maceio") - | s.starts_with("America/Araguaina") => - { - "\u{1f1e7}\u{1f1f7}" - } - "Asia/Tokyo" => "\u{1f1ef}\u{1f1f5}", - "Europe/London" | "Europe/Dublin" => "\u{1f1ec}\u{1f1e7}", - "Europe/Paris" => "\u{1f1eb}\u{1f1f7}", - "Europe/Berlin" => "\u{1f1e9}\u{1f1ea}", - "Europe/Rome" => "\u{1f1ee}\u{1f1f9}", - "Europe/Madrid" => "\u{1f1ea}\u{1f1f8}", - "Europe/Lisbon" => "\u{1f1f5}\u{1f1f9}", - "Europe/Amsterdam" => "\u{1f1f3}\u{1f1f1}", - "Europe/Zurich" => "\u{1f1e8}\u{1f1ed}", - "Europe/Vienna" => "\u{1f1e6}\u{1f1f9}", - "Europe/Prague" => "\u{1f1e8}\u{1f1ff}", - "Europe/Warsaw" => "\u{1f1f5}\u{1f1f1}", - "Europe/Stockholm" => "\u{1f1f8}\u{1f1ea}", - "Europe/Oslo" => "\u{1f1f3}\u{1f1f4}", - "Europe/Copenhagen" => "\u{1f1e9}\u{1f1f0}", - "Europe/Helsinki" => "\u{1f1eb}\u{1f1ee}", - "Europe/Moscow" => "\u{1f1f7}\u{1f1fa}", - "Europe/Istanbul" => "\u{1f1f9}\u{1f1f7}", - "Asia/Shanghai" => "\u{1f1e8}\u{1f1f3}", - "Asia/Hong_Kong" => "\u{1f1ed}\u{1f1f0}", - "Asia/Seoul" => "\u{1f1f0}\u{1f1f7}", - "Asia/Singapore" => "\u{1f1f8}\u{1f1ec}", - "Asia/Kolkata" => "\u{1f1ee}\u{1f1f3}", - "Asia/Dubai" => "\u{1f1e6}\u{1f1ea}", - "Asia/Bangkok" => "\u{1f1f9}\u{1f1ed}", - "Asia/Jakarta" => "\u{1f1ee}\u{1f1e9}", - "Asia/Taipei" => "\u{1f1f9}\u{1f1fc}", - "Asia/Riyadh" => "\u{1f1f8}\u{1f1e6}", - "Asia/Jerusalem" => "\u{1f1ee}\u{1f1f1}", - "Australia/Sydney" | "Australia/Melbourne" | "Australia/Perth" | "Australia/Brisbane" => { - "\u{1f1e6}\u{1f1fa}" - } - "Pacific/Auckland" => "\u{1f1f3}\u{1f1ff}", - "America/Toronto" | "America/Vancouver" | "America/Edmonton" => "\u{1f1e8}\u{1f1e6}", - "America/Mexico_City" => "\u{1f1f2}\u{1f1fd}", - "America/Argentina/Buenos_Aires" => "\u{1f1e6}\u{1f1f7}", - "America/Santiago" => "\u{1f1e8}\u{1f1f1}", - "America/Bogota" => "\u{1f1e8}\u{1f1f4}", - "America/Lima" => "\u{1f1f5}\u{1f1ea}", - "Africa/Johannesburg" => "\u{1f1ff}\u{1f1e6}", - "Africa/Lagos" => "\u{1f1f3}\u{1f1ec}", - "Africa/Cairo" => "\u{1f1ea}\u{1f1ec}", - "Africa/Nairobi" => "\u{1f1f0}\u{1f1ea}", - _ => "\u{1f30d}", - } -} - -fn parse_time(t: &str) -> u32 { - let parts: Vec = t.split(':').filter_map(|p| p.parse().ok()).collect(); - if parts.len() == 2 { - parts[0] * 60 + parts[1] - } else { - 0 - } -} - -pub fn current_availability(entry: &ClockEntry, now: chrono::DateTime) -> Option { - let schedule = entry.status.as_ref()?; - - let month_key = format!("{}-{:02}", now.year(), now.month()); - let month_str = schedule.months.get(&month_key)?; - let day = now.day() as usize; - - if day < 1 || day > month_str.len() { - return None; - } - - let block_id = &month_str[day - 1..day]; - let now_minutes = now.hour() * 60 + now.minute(); - - if block_id != "0" { - if let Some(block) = schedule.blocks.get(block_id) { - let start = parse_time(&block.start); - let end = parse_time(&block.end); - - if end > start { - if now_minutes >= start && now_minutes < end { - return Some(Availability::Busy(block.label.clone())); - } - } else if end < start && now_minutes >= start { - return Some(Availability::Busy(block.label.clone())); - } - } - } - - let yesterday = now - chrono::Duration::days(1); - let y_month_key = format!("{}-{:02}", yesterday.year(), yesterday.month()); - if let Some(y_month_str) = schedule.months.get(&y_month_key) { - let y_day = yesterday.day() as usize; - if y_day >= 1 && y_day <= y_month_str.len() { - let y_block_id = &y_month_str[y_day - 1..y_day]; - if y_block_id != "0" { - if let Some(y_block) = schedule.blocks.get(y_block_id) { - let y_start = parse_time(&y_block.start); - let y_end = parse_time(&y_block.end); - if y_end < y_start && now_minutes < y_end { - return Some(Availability::Busy(y_block.label.clone())); - } - } - } - } - } - - if block_id == "0" { - Some(Availability::DayOff) - } else { - Some(Availability::Available) - } -} - -pub fn relative_offset(local_tz: Tz, remote_tz: Tz, now: chrono::DateTime) -> String { - use chrono::Offset; - let local_offset = now.with_timezone(&local_tz).offset().fix().local_minus_utc(); - let remote_offset = now.with_timezone(&remote_tz).offset().fix().local_minus_utc(); - let diff = remote_offset - local_offset; - let hours = diff / 3600; - let minutes = (diff.abs() % 3600) / 60; - - if hours == 0 && minutes == 0 { - return "local".into(); - } - if minutes > 0 { - format!("{:+}:{:02}h", hours, minutes) - } else { - format!("{:+}h", hours) - } -} - -pub fn local_tz() -> Tz { - iana_time_zone::get_timezone() - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(chrono_tz::UTC) -} diff --git a/linux/src/main.rs b/linux/src/main.rs index ef81ea4..93df42b 100644 --- a/linux/src/main.rs +++ b/linux/src/main.rs @@ -1,6 +1,7 @@ -mod config; - -use config::{current_availability, load_config, local_tz, relative_offset, timezone_to_flag, Availability}; +use fuso_core::{ + config_path, current_availability, load_config, local_tz, relative_offset, timezone_to_flag, + Availability, ClockEntry, +}; use chrono_tz::Tz; use glib::ControlFlow; use gtk4::prelude::*; @@ -78,14 +79,13 @@ const CSS: &str = r#" } "#; -fn build_clock_card(entry: &config::ClockEntry, now_utc: chrono::DateTime, local_tz: Tz) -> GtkBox { +fn build_clock_card(entry: &ClockEntry, now_utc: chrono::DateTime, local_tz: Tz) -> GtkBox { let card = GtkBox::new(Orientation::Horizontal, 0); card.add_css_class("clock-card"); let tz: Tz = entry.timezone.parse().unwrap_or(chrono_tz::UTC); let now_tz = now_utc.with_timezone(&tz); - // Left side: flag + name/city/status let left = GtkBox::new(Orientation::Horizontal, 10); left.set_hexpand(true); @@ -122,7 +122,6 @@ fn build_clock_card(entry: &config::ClockEntry, now_utc: chrono::DateTime