From 6a1d9272bcd39eae853a1540f9a4778ca8101b82 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Sat, 15 Jul 2023 21:30:09 +0200 Subject: [PATCH 1/8] notifications and msi installer --- .github/workflows/release.yml | 7 +- CHANGELOG.md | 1 + Cargo.lock | 569 +++++++++++++++++++- Cargo.toml | 1 + assets/macos/Halloy.app/Contents/Info.plist | 2 +- config.yaml | 45 +- data/src/config.rs | 6 + data/src/config/notification.rs | 28 + scripts/build-windows-installer.sh | 14 + scripts/build-windows.sh | 3 + src/main.rs | 27 + src/notification.rs | 66 +++ wix/banner.png | Bin 0 -> 6140 bytes wix/dialog.png | Bin 0 -> 30576 bytes wix/license.rtf | 322 +++++++++++ wix/main.wxs | 152 ++++++ 16 files changed, 1225 insertions(+), 18 deletions(-) create mode 100644 data/src/config/notification.rs create mode 100755 scripts/build-windows-installer.sh mode change 100644 => 100755 scripts/build-windows.sh create mode 100644 src/notification.rs create mode 100644 wix/banner.png create mode 100644 wix/dialog.png create mode 100644 wix/license.rtf create mode 100644 wix/main.wxs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6220641ae..fad912157 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,16 +20,15 @@ jobs: echo "ARTIFACT_PATH=target/release/macos/halloy.dmg" >> "$GITHUB_ENV" - target: windows os: windows-latest - make: bash scripts/build-windows.sh + make: bash scripts/build-windows-installer.sh artifact_path: | - echo "ARTIFACT_PATH=target/release/halloy.exe" >> $env:GITHUB_ENV + echo "ARTIFACT_PATH=target/release/halloy-installer.msi" >> $env:GITHUB_ENV - target: linux os: ubuntu-latest make: bash scripts/package-linux.sh package artifact_path: | echo "ARTIFACT_PATH=$(bash scripts/package-linux.sh archive_path)" >> "$GITHUB_ENV" runs-on: ${{ matrix.target.os }} - steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 @@ -94,7 +93,7 @@ jobs: asset_type: application/octet-stream - artifact: windows artifact_name: | - echo "ARTIFACT_NAME=halloy.exe" >> "$GITHUB_ENV" + echo "ARTIFACT_NAME=halloy-installer.msi" >> "$GITHUB_ENV" asset_type: application/x-dosexec - artifact: linux artifact_name: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 618f44933..012944d82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Added: - IRCv3 capability `userhost-in-names` support added - IRCv3 capability `invite-notify` support added - Configuration option `dashboard.sidebar.width` to control sidebar width. +- Configuration option `notification` to control and enable notifications Changed: diff --git a/Cargo.lock b/Cargo.lock index 5d545ee3d..ccb478970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,134 @@ dependencies = [ "libloading 0.7.4", ] +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock", + "autocfg", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix", + "slab", + "socket2", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-process" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +dependencies = [ + "async-io", + "async-lock", + "autocfg", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", + "signal-hook", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-recursion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "async-task" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" + +[[package]] +name = "async-trait" +version = "0.1.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2d0f03b3640e3a630367e40c468cb7f309529c708ed1d88597047b0e7c6ef7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + [[package]] name = "autocfg" version = "1.1.0" @@ -211,6 +339,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-sys" version = "0.1.0-beta.1" @@ -230,6 +367,21 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "blocking" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "log", +] + [[package]] name = "bumpalo" version = "3.13.0" @@ -313,7 +465,7 @@ dependencies = [ "js-sys", "num-traits", "serde", - "time", + "time 0.1.45", "wasm-bindgen", "winapi", ] @@ -412,6 +564,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642" +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -473,6 +634,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -531,6 +701,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "d3d12" version = "0.6.0" @@ -567,6 +747,27 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -628,6 +829,27 @@ dependencies = [ "winreg", ] +[[package]] +name = "enumflags2" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "errno" version = "0.3.1" @@ -678,6 +900,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "exr" version = "1.6.4" @@ -827,6 +1055,21 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.28" @@ -868,6 +1111,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "0.2.3" @@ -1020,6 +1273,7 @@ dependencies = [ "iced", "image", "log", + "notify-rust", "once_cell", "open", "palette", @@ -1084,6 +1338,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hexf-parse" version = "0.2.1" @@ -1522,6 +1782,19 @@ dependencies = [ "hashbrown 0.14.0", ] +[[package]] +name = "mac-notification-sys" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e72d50edb17756489e79d52eb146927bec8eba9dd48faadf9ef08bca3791ad5" +dependencies = [ + "cc", + "dirs-next", + "objc-foundation", + "objc_id", + "time 0.3.23", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1777,6 +2050,19 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify-rust" +version = "4.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfa211d18e360f08e36c364308f394b5eb23a6629150690e109a916dc6f610e" +dependencies = [ + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1981,6 +2267,16 @@ dependencies = [ "redox_syscall 0.3.5", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "ouroboros" version = "0.17.0" @@ -2037,6 +2333,12 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" + [[package]] name = "parking_lot" version = "0.11.2" @@ -2190,6 +2492,22 @@ dependencies = [ "miniz_oxide 0.7.1", ] +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2552,6 +2870,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0a21fba416426ac927b1691996e82079f8b6156e920c85345f135b2e9ba2de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "serde_spanned" version = "0.6.2" @@ -2573,6 +2902,27 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2770,6 +3120,16 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5bff1d532fead7c43324a0fa33643b8621a47ce2944a633be4cb6c0240898f" +dependencies = [ + "quick-xml", + "windows 0.39.0", +] + [[package]] name = "tempfile" version = "3.5.0" @@ -2834,6 +3194,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +dependencies = [ + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + [[package]] name = "tiny-skia" version = "0.8.4" @@ -3008,9 +3384,21 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "tracing-core" version = "0.1.31" @@ -3037,6 +3425,22 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -3148,6 +3552,12 @@ dependencies = [ "libc", ] +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -3540,6 +3950,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "windows" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + [[package]] name = "windows" version = "0.44.0" @@ -3633,6 +4056,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -3645,6 +4074,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -3657,6 +4092,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -3669,6 +4110,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -3693,6 +4140,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -3815,6 +4268,16 @@ dependencies = [ "nom", ] +[[package]] +name = "xdg-home" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +dependencies = [ + "nix 0.26.2", + "winapi", +] + [[package]] name = "xml-rs" version = "0.8.13" @@ -3836,6 +4299,72 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" +[[package]] +name = "zbus" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "byteorder", + "derivative", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.26.2", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zeno" version = "0.2.2" @@ -3850,3 +4379,41 @@ checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" dependencies = [ "simd-adler32", ] + +[[package]] +name = "zvariant" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/Cargo.toml b/Cargo.toml index 410a3dcd7..bf18e2ca4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ dev = ["debug", "data/dev"] [dependencies] data = { version = "0.1.0", path = "data" } +notify-rust = "4" chrono = { version = "0.4", features = ['serde'] } fern = "0.6.1" iced = { version = "0.9", features = ["tokio", "lazy", "advanced", "image"] } diff --git a/assets/macos/Halloy.app/Contents/Info.plist b/assets/macos/Halloy.app/Contents/Info.plist index cfe69ed9f..8d08f5ae8 100644 --- a/assets/macos/Halloy.app/Contents/Info.plist +++ b/assets/macos/Halloy.app/Contents/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable halloy CFBundleIdentifier - irc.halloy + irc.squidowl.org CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/config.yaml b/config.yaml index 870d92a51..02cd9430d 100644 --- a/config.yaml +++ b/config.yaml @@ -5,6 +5,11 @@ # macOS: `$HOME`/Library/Application Support/halloy # Windows: `{FOLDERID_RoamingAppData}`\halloy +# Configuration wiki +# +# Visit our wiki for all configurations options +# https://github.com/squidowl/halloy/wiki/Configuration + # Theme # - Add theme files to the themes directory and fill this with the filename # without the .yaml extension to select the theme you want @@ -20,29 +25,29 @@ servers: liberachat: # Nickname to be used on the server nickname: halloy - + # Server address server: irc.libera.chat - + # Server port number port: 6697 - + # Whether to use TLS use_tls: true - + # Channels to join upon connecting to the server channels: - "#halloy" # Font settings -font: +font: # Specify the monospaced font family to use # - Default is Iosevka Term and provided by this application family: Iosevka Term # Specify the font size # - Default is 13 size: 13 - + # Buffer settings buffer: # Nickname settings @@ -51,20 +56,20 @@ buffer: # - Unique: Unique user colors [default] # - Solid: Solid user colors color: Unique - + # Nickname brackets: # - Default is empty "" brackets: left: "<" right: ">" - + # Timestamp settings - timestamp: + timestamp: # Timestamp format: # - Use `strftime` format (see documentation for details): # https://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html format: "%T" - + # Timestamp brackets: # - Default is empty "" brackets: @@ -80,7 +85,7 @@ buffer: # - Default: [] # - Supported values: ["join", "part", "quit"] hidden_server_messages: [] - + # Channel buffer settings channel: # User list settings @@ -91,7 +96,7 @@ buffer: # List position # - Left: Left side of pane # - Right: Right side of pane [default] - position: Right + position: Right # Dashboard settings dashboard: @@ -103,3 +108,19 @@ dashboard: # Maximum width of the sidebar # - Default: 120 width: 120 + + # Default action when selecting channels in the sidebar: + # - NewPane: Open a new pane for each unique channel [default] + # - ReplacePane: Replace the currently selected pane + sidebar_default_action: NewPane + +# Notification +# Display a OS level notification on certain events. +# +# For information about events and sound +# https://github.com/squidowl/halloy/wiki/Configuration#notification +notification: + Connected: + sound: glass + Disconnected: + sound: basso diff --git a/data/src/config.rs b/data/src/config.rs index 1213e5c02..946318285 100644 --- a/data/src/config.rs +++ b/data/src/config.rs @@ -18,6 +18,7 @@ mod buffer; pub mod channel; pub mod dashboard; mod keys; +pub mod notification; pub mod server; const CONFIG_TEMPLATE: &[u8] = include_bytes!("../../config.yaml"); @@ -31,6 +32,7 @@ pub struct Config { pub buffer: Buffer, pub dashboard: Dashboard, pub keys: Keys, + pub notification: notification::List, } #[derive(Debug, Clone, Default, Deserialize)] @@ -95,6 +97,8 @@ impl Config { pub dashboard: Dashboard, #[serde(default)] pub keys: Keys, + #[serde(default)] + pub notification: notification::List, } let path = Self::path(); @@ -107,6 +111,7 @@ impl Config { buffer, dashboard, keys, + notification, } = serde_yaml::from_reader(BufReader::new(file)) .map_err(|e| Error::Parse(e.to_string()))?; @@ -119,6 +124,7 @@ impl Config { buffer, dashboard, keys, + notification, }) } diff --git a/data/src/config/notification.rs b/data/src/config/notification.rs new file mode 100644 index 000000000..032892b7e --- /dev/null +++ b/data/src/config/notification.rs @@ -0,0 +1,28 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize, PartialEq, PartialOrd, Eq, Hash)] +pub enum Event { + Connected, + Reconnected, + Disconnected, + // TODO: Add more alert types. + // Highlighted + // .. +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + #[serde(default)] + pub sound: Option, +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct List(HashMap); + +impl List { + pub fn get(&self, event: Event) -> Option<&Config> { + self.0.get(&event) + } +} diff --git a/scripts/build-windows-installer.sh b/scripts/build-windows-installer.sh new file mode 100755 index 000000000..64d191c18 --- /dev/null +++ b/scripts/build-windows-installer.sh @@ -0,0 +1,14 @@ +#!/bin/bash +WXS_FILE="wix/main.wxs" +VERSION=$(cat VERSION) + +# update version and build +sed -i '' -e "s/{{ VERSION }}/$VERSION/g" "$WXS_FILE" + +# install wix tools, and ensure paths are set +choco install wixtoolset -y --force --version=3.11.2 +$env:Path += ';C:\Program Files (x86)\Wix Toolset v3.11\bin' + +# build msi installer +cargo install cargo-wix +cargo wix --nocapture --package halloy -o target/release/halloy-installer.msi diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh old mode 100644 new mode 100755 index a5b6e7ac2..84c86c20d --- a/scripts/build-windows.sh +++ b/scripts/build-windows.sh @@ -1,3 +1,6 @@ +# Deprecated for now. +# We should later use it for portable version of Halloy. + #!/bin/bash EXE_NAME="halloy.exe" TARGET="x86_64-pc-windows-msvc" diff --git a/src/main.rs b/src/main.rs index c2a45f12d..dd810368c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod event; mod font; mod icon; mod logger; +mod notification; mod screen; mod stream; mod theme; @@ -22,6 +23,7 @@ use iced::{executor, Application, Command, Length, Subscription}; use screen::{dashboard, help, welcome}; use self::event::{events, Event}; +pub use self::notification::Notification; pub use self::theme::Theme; use self::widget::Element; @@ -45,6 +47,9 @@ pub fn main() -> iced::Result { #[cfg(not(debug_assertions))] let is_debug = false; + // Prepare notifications. + notification::prepare(); + logger::setup(is_debug).expect("setup logging"); log::info!("halloy {} has started", environment::formatted_version()); log::info!("config dir: {:?}", environment::config_dir()); @@ -248,6 +253,8 @@ impl Application for Halloy { is_initial, error, } => { + use config::notification::Event; + self.clients.disconnected(server.clone()); let Screen::Dashboard(dashboard) = &mut self.screen else { @@ -258,6 +265,12 @@ impl Application for Halloy { // Intial is sent when first trying to connect dashboard.broadcast_connecting(&server); } else { + if let Some(config) = self.config.notification.get(Event::Connected) { + Notification::new(config) + .body(format!("Disconnected from {server}")) + .show(); + }; + dashboard.broadcast_disconnected(&server, error); } @@ -268,6 +281,8 @@ impl Application for Halloy { client: connection, is_initial, } => { + use config::notification::Event; + self.clients.ready(server.clone(), connection); let Screen::Dashboard(dashboard) = &mut self.screen else { @@ -275,8 +290,20 @@ impl Application for Halloy { }; if is_initial { + if let Some(config) = self.config.notification.get(Event::Connected) { + Notification::new(config) + .body(format!("Connected to {server}")) + .show(); + }; + dashboard.broadcast_connected(&server); } else { + if let Some(config) = self.config.notification.get(Event::Reconnected) { + Notification::new(config) + .body(format!("Reconnected to {server}")) + .show(); + }; + dashboard.broadcast_reconnected(&server); } diff --git a/src/notification.rs b/src/notification.rs new file mode 100644 index 000000000..32079de8c --- /dev/null +++ b/src/notification.rs @@ -0,0 +1,66 @@ +use data::config; + +#[allow(dead_code)] +const APP_ID: &str = "irc.squidowl.org"; + +#[cfg(target_os = "macos")] +pub fn prepare() { + match notify_rust::set_application(APP_ID) { + Ok(_) => {} + Err(error) => { + log::error!("{}", error.to_string()); + } + } +} + +#[cfg(not(any(target_os = "macos")))] +pub fn prepare() {} + +#[derive(Default)] +pub struct Notification { + body: Option, + title: Option, + sound: Option, +} + +impl Notification { + pub fn new(config: &config::notification::Config) -> Notification { + Notification { + sound: config.sound.clone(), + ..Default::default() + } + } + + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + pub fn body(mut self, body: impl Into) -> Self { + self.body = Some(body.into()); + self + } + + pub fn show(self) { + let mut notification = notify_rust::Notification::new(); + + if let Some(body) = self.body { + notification.body(&body); + } + + if let Some(title) = self.title { + notification.summary(&title); + } + + if let Some(sound) = self.sound { + notification.sound_name(&sound); + } + + #[cfg(windows)] + { + notification.app_id(APP_ID); + } + + let _ = notification.show(); + } +} diff --git a/wix/banner.png b/wix/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..dd1ead4b9434803459e78334b9733c4f39a804e3 GIT binary patch literal 6140 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 N<0(j`RO!XL{{bP>4Rini literal 0 HcmV?d00001 diff --git a/wix/dialog.png b/wix/dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..09fb042de8383155ab71253d3fe38f5c4d991521 GIT binary patch literal 30576 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 po$>$\line Copyright (C) \line \line This program is free software: you can redistribute it and/or modify\line it under the terms of the GNU General Public License as published by\line the Free Software Foundation, either version 3 of the License, or\line (at your option) any later version.\line \line This program is distributed in the hope that it will be useful,\line but WITHOUT ANY WARRANTY; without even the implied warranty of\line MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\line GNU General Public License for more details.\line \line You should have received a copy of the GNU General Public License\line along with this program. If not, see .} +{\rtlch\af0\afs20\ltrch\b0\i0\fs20\loch\af0\dbch\af0\hich\f0\cs10\strike0\par}\pard\plain\itap0\s0\sa160\aspalpha\aspnum\adjustright\ltrpar\li0\lin0\ri0\rin0\ql\faauto\rtlch\af1\alang1025\afs22\ltrch\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1 +\dbch\af1\hich\f1{\rtlch\af1\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1\dbch\af1\hich\f1\cs10\strike0 Also add information on how to contact you by electronic and paper mail.}{\rtlch\afs22\ltrch\b0\i0\fs22\cs10\strike0 +\par}\pard\plain\itap0\s0\sa160\aspalpha\aspnum\adjustright\ltrpar\li0\lin0\ri0\rin0\ql\faauto\rtlch\af1\alang1025\afs22\ltrch\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1\dbch\af1\hich\f1{\rtlch\af1\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033 +\loch\af1\dbch\af1\hich\f1\cs10\strike0 If the program does terminal interaction, make it output a short\line notice like this when it starts in an interactive mode:}{\rtlch\afs22\ltrch\b0\i0\fs22\cs10\strike0\par}\pard\plain\itap0\s15\sa160\aspalpha\aspnum +\adjustright\shading10000\cfpat1\ltrpar\li0\lin0\ri0\rin0\ql\faauto\rtlch\af0\alang1025\afs20\ltrch\fs20\lang1033\langnp1033\langfe1033\langfenp1033\loch\af0\dbch\af0\hich\f0{\rtlch\af0\alang1025\afs20\ltrch\b0\i0\fs20\lang1033\langnp1033\langfe1033\langfenp1033 +\loch\af0\dbch\af0\hich\f0\cs10\strike0 Copyright (C) \line This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\line This is free software, and you are welcome to redistribute it\line under certain conditions; type `show c' for details.} +{\rtlch\af0\afs20\ltrch\b0\i0\fs20\loch\af0\dbch\af0\hich\f0\cs10\strike0\par}\pard\plain\itap0\s0\sa160\aspalpha\aspnum\adjustright\ltrpar\li0\lin0\ri0\rin0\ql\faauto\rtlch\af1\alang1025\afs22\ltrch\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1 +\dbch\af1\hich\f1{\rtlch\af1\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1\dbch\af1\hich\f1\cs10\strike0 The hypothetical commands }{\rtlch\af0\alang1025\afs20\ltrch\b0\i0\fs20\lang1033\langnp1033\langfe1033\langfenp1033 +\loch\af0\dbch\af0\hich\f0\highlight3\cs17\strike0\cf4 show w' and }{\rtlch\af1\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1\dbch\af1\hich\f1\cs10\strike0 show c' should show the appropriate\line parts of the General Public License. Of course, your program's commands\line might be different; for a GUI interface, you would use an "about box".} +{\rtlch\afs22\ltrch\b0\i0\fs22\cs10\strike0\par}\pard\plain\itap0\s0\sa160\aspalpha\aspnum\adjustright\ltrpar\li0\lin0\ri0\rin0\ql\faauto\rtlch\af1\alang1025\afs22\ltrch\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1\dbch\af1\hich\f1{\rtlch\af1\alang1025\afs22 +\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1\dbch\af1\hich\f1\cs10\strike0 You should also get your employer (if you work as a programmer) or school,\line if any, to sign a "copyright disclaimer" for the program, if necessary.\line For more information on this, and how to apply and follow the GNU GPL, see\line } +{\field{\*\fldinst{\rtlch\af1\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1\dbch\af1\hich\f1\cs16\strike0\ul\cf2 HYPERLINK "https://www.gnu.org/licenses/" }{\*\datafield 08d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b3c000000680074007400700073003a002f002f007700770077002e0067006e0075002e006f00720067002f006c006900630065006e007300650073002f000000}} +{\fldrslt{\rtlch\af1\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1\dbch\af1\hich\f1\cs16\strike0\ul\cf2 https://www.gnu.org/licenses/}}}{\rtlch\af1\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033 +\loch\af1\dbch\af1\hich\f1\cs10\strike0 .}{\rtlch\afs22\ltrch\b0\i0\fs22\cs10\strike0\par}\pard\plain\itap0\s0\sa160\aspalpha\aspnum\adjustright\ltrpar\li0\lin0\ri0\rin0\ql\faauto\rtlch\af1\alang1025\afs22\ltrch\fs22\lang1033\langnp1033\langfe1033\langfenp1033 +\loch\af1\dbch\af1\hich\f1{\rtlch\af1\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1\dbch\af1\hich\f1\cs10\strike0 The GNU General Public License does not permit incorporating your program\line into proprietary programs. If your program is a subroutine library, you\line may consider it more useful to permit linking proprietary applications with\line the library. If this is what you want to do, use the GNU Lesser General\line Public License instead of this License. But first, please read\line } +{\field{\*\fldinst{\rtlch\af1\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1\dbch\af1\hich\f1\cs16\strike0\ul\cf2 HYPERLINK "https://www.gnu.org/licenses/why-not-lgpl.html" }{\*\datafield 08d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b5e000000680074007400700073003a002f002f007700770077002e0067006e0075002e006f00720067002f006c006900630065006e007300650073002f007700680079002d006e006f0074002d006c00670070006c002e00680074006d006c000000}} +{\fldrslt{\rtlch\af1\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af1\dbch\af1\hich\f1\cs16\strike0\ul\cf2 https://www.gnu.org/licenses/why-not-lgpl.html}}}{\rtlch\af1\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033 +\loch\af1\dbch\af1\hich\f1\cs10\strike0 .}{\rtlch\afs22\ltrch\b0\i0\fs22\cs10\strike0\par}{\*\latentstyles\lsdstimax267\lsdlockeddef0\lsdsemihiddendef1\lsdunhideuseddef1\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept}}} \ No newline at end of file diff --git a/wix/main.wxs b/wix/main.wxs new file mode 100644 index 000000000..5d1bb6bfa --- /dev/null +++ b/wix/main.wxs @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NOT REMOVE + + + + + + + From 1a2c604c1a587992c1beb1f756374535d7c3c01b Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Wed, 2 Aug 2023 11:41:36 -0700 Subject: [PATCH 2/8] Refactor notifications --- data/src/config.rs | 9 +++-- data/src/config/notification.rs | 48 ++++++++++++++---------- src/main.rs | 35 ++++++++---------- src/notification.rs | 65 +++++++++------------------------ 4 files changed, 66 insertions(+), 91 deletions(-) diff --git a/data/src/config.rs b/data/src/config.rs index 946318285..1286e18e3 100644 --- a/data/src/config.rs +++ b/data/src/config.rs @@ -9,6 +9,7 @@ pub use self::buffer::Buffer; pub use self::channel::Channel; pub use self::dashboard::Dashboard; pub use self::keys::Keys; +pub use self::notification::{Notification, Notifications}; pub use self::server::Server; use crate::server::Map as ServerMap; use crate::theme::Palette; @@ -32,7 +33,7 @@ pub struct Config { pub buffer: Buffer, pub dashboard: Dashboard, pub keys: Keys, - pub notification: notification::List, + pub notifications: Notifications, } #[derive(Debug, Clone, Default, Deserialize)] @@ -98,7 +99,7 @@ impl Config { #[serde(default)] pub keys: Keys, #[serde(default)] - pub notification: notification::List, + pub notifications: Notifications, } let path = Self::path(); @@ -111,7 +112,7 @@ impl Config { buffer, dashboard, keys, - notification, + notifications, } = serde_yaml::from_reader(BufReader::new(file)) .map_err(|e| Error::Parse(e.to_string()))?; @@ -124,7 +125,7 @@ impl Config { buffer, dashboard, keys, - notification, + notifications, }) } diff --git a/data/src/config/notification.rs b/data/src/config/notification.rs index 032892b7e..8150b738a 100644 --- a/data/src/config/notification.rs +++ b/data/src/config/notification.rs @@ -1,28 +1,38 @@ -use std::collections::HashMap; - use serde::Deserialize; -#[derive(Debug, Clone, Deserialize, PartialEq, PartialOrd, Eq, Hash)] -pub enum Event { - Connected, - Reconnected, - Disconnected, - // TODO: Add more alert types. - // Highlighted - // .. -} +#[cfg(target_os = "macos")] +const DEFAULT_SOUND: &str = "Submarine"; +#[cfg(all(unix, not(target_os = "macos")))] +const DEFAULT_SOUND: &str = "message-new-instant"; +#[cfg(target_os = "windows")] +const DEFAULT_SOUND: &str = "Mail"; -#[derive(Debug, Clone, Deserialize)] -pub struct Config { +#[derive(Debug, Clone, Default, Deserialize)] +pub struct Notification { #[serde(default)] - pub sound: Option, + pub enabled: bool, + #[serde(default = "default_sound")] + sound: String, + #[serde(default)] + mute: bool, +} + +impl Notification { + pub fn sound(&self) -> Option<&str> { + (!self.mute).then_some(&self.sound) + } } #[derive(Debug, Clone, Default, Deserialize)] -pub struct List(HashMap); +pub struct Notifications { + #[serde(default)] + pub connected: Notification, + #[serde(default)] + pub disconnected: Notification, + #[serde(default)] + pub reconnected: Notification, +} -impl List { - pub fn get(&self, event: Event) -> Option<&Config> { - self.0.get(&event) - } +fn default_sound() -> String { + DEFAULT_SOUND.to_string() } diff --git a/src/main.rs b/src/main.rs index dd810368c..240bc45e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,8 +23,7 @@ use iced::{executor, Application, Command, Length, Subscription}; use screen::{dashboard, help, welcome}; use self::event::{events, Event}; -pub use self::notification::Notification; -pub use self::theme::Theme; +use self::theme::Theme; use self::widget::Element; pub fn main() -> iced::Result { @@ -253,8 +252,6 @@ impl Application for Halloy { is_initial, error, } => { - use config::notification::Event; - self.clients.disconnected(server.clone()); let Screen::Dashboard(dashboard) = &mut self.screen else { @@ -265,10 +262,10 @@ impl Application for Halloy { // Intial is sent when first trying to connect dashboard.broadcast_connecting(&server); } else { - if let Some(config) = self.config.notification.get(Event::Connected) { - Notification::new(config) - .body(format!("Disconnected from {server}")) - .show(); + let notification = &self.config.notifications.disconnected; + + if notification.enabled { + notification::show("Disconnected", &server, notification.sound()); }; dashboard.broadcast_disconnected(&server, error); @@ -281,8 +278,6 @@ impl Application for Halloy { client: connection, is_initial, } => { - use config::notification::Event; - self.clients.ready(server.clone(), connection); let Screen::Dashboard(dashboard) = &mut self.screen else { @@ -290,19 +285,19 @@ impl Application for Halloy { }; if is_initial { - if let Some(config) = self.config.notification.get(Event::Connected) { - Notification::new(config) - .body(format!("Connected to {server}")) - .show(); - }; + let notification = &self.config.notifications.connected; + + if notification.enabled { + notification::show("Connected", &server, notification.sound()); + } dashboard.broadcast_connected(&server); } else { - if let Some(config) = self.config.notification.get(Event::Reconnected) { - Notification::new(config) - .body(format!("Reconnected to {server}")) - .show(); - }; + let notification = &self.config.notifications.reconnected; + + if notification.enabled { + notification::show("Reconnected", &server, notification.sound()); + } dashboard.broadcast_reconnected(&server); } diff --git a/src/notification.rs b/src/notification.rs index 32079de8c..89e50a89d 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -1,11 +1,6 @@ -use data::config; - -#[allow(dead_code)] -const APP_ID: &str = "irc.squidowl.org"; - #[cfg(target_os = "macos")] pub fn prepare() { - match notify_rust::set_application(APP_ID) { + match notify_rust::set_application(data::environment::APPLICATION_ID) { Ok(_) => {} Err(error) => { log::error!("{}", error.to_string()); @@ -13,54 +8,28 @@ pub fn prepare() { } } -#[cfg(not(any(target_os = "macos")))] +#[cfg(not(target_os = "macos"))] pub fn prepare() {} -#[derive(Default)] -pub struct Notification { - body: Option, - title: Option, - sound: Option, -} +pub fn show(title: &str, body: impl ToString, sound: Option<&str>) { + let mut notification = notify_rust::Notification::new(); -impl Notification { - pub fn new(config: &config::notification::Config) -> Notification { - Notification { - sound: config.sound.clone(), - ..Default::default() - } - } + notification.summary(title); + notification.body(&body.to_string()); - pub fn title(mut self, title: impl Into) -> Self { - self.title = Some(title.into()); - self + if let Some(sound) = sound { + notification.sound_name(sound); } - pub fn body(mut self, body: impl Into) -> Self { - self.body = Some(body.into()); - self + #[cfg(target_os = "linux")] + { + notification.appname("Halloy"); + notification.icon(data::environment::APPLICATION_ID); } - - pub fn show(self) { - let mut notification = notify_rust::Notification::new(); - - if let Some(body) = self.body { - notification.body(&body); - } - - if let Some(title) = self.title { - notification.summary(&title); - } - - if let Some(sound) = self.sound { - notification.sound_name(&sound); - } - - #[cfg(windows)] - { - notification.app_id(APP_ID); - } - - let _ = notification.show(); + #[cfg(target_os = "windows")] + { + notification.app_id(data::environment::APPLICATION_ID); } + + let _ = notification.show(); } From 3372de5d21c22dbb21806c414d9d8bc4bdc6d62f Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Wed, 2 Aug 2023 11:59:45 -0700 Subject: [PATCH 3/8] Update app ids --- assets/macos/Halloy.app/Contents/Info.plist | 2 +- wix/main.wxs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/macos/Halloy.app/Contents/Info.plist b/assets/macos/Halloy.app/Contents/Info.plist index 8d08f5ae8..66bd2532d 100644 --- a/assets/macos/Halloy.app/Contents/Info.plist +++ b/assets/macos/Halloy.app/Contents/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable halloy CFBundleIdentifier - irc.squidowl.org + org.squidowl.halloy CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/wix/main.wxs b/wix/main.wxs index 5d1bb6bfa..677fcbd34 100644 --- a/wix/main.wxs +++ b/wix/main.wxs @@ -64,7 +64,7 @@ Description="IRC application written in Rust" Advertise="yes" WorkingDirectory="APPLICATIONFOLDER"> - + Date: Wed, 13 Sep 2023 19:55:45 +0200 Subject: [PATCH 4/8] clippy --- src/widget/combo_box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/combo_box.rs b/src/widget/combo_box.rs index 651611fc5..b505f8c26 100644 --- a/src/widget/combo_box.rs +++ b/src/widget/combo_box.rs @@ -665,7 +665,7 @@ where options .into_iter() - .zip(option_matchers.into_iter()) + .zip(option_matchers) // Make sure each part of the query is found in the option .filter_map(move |(option, matcher)| { if query.iter().all(|part| matcher.as_ref().contains(part)) { From 713f5be103fed3acb68aeb81d5c07717df6f254d Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Sun, 17 Sep 2023 19:38:11 +0200 Subject: [PATCH 5/8] check if referenced nick --- data/src/client.rs | 39 ++++++++++++++++++++++++--------- data/src/config/notification.rs | 2 ++ data/src/message.rs | 4 ++++ src/buffer/channel.rs | 7 ++++-- src/main.rs | 2 +- 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/data/src/client.rs b/data/src/client.rs index 63a17fbe6..179aed330 100644 --- a/data/src/client.rs +++ b/data/src/client.rs @@ -1,10 +1,9 @@ -use std::collections::{BTreeMap, HashMap, HashSet}; -use std::fmt; -use std::time::{Duration, Instant}; - use futures::channel::mpsc; use irc::proto::{self, command, Command}; use itertools::Itertools; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::fmt; +use std::time::{Duration, Instant}; use crate::time::Posix; use crate::user::{Nick, NickRef}; @@ -12,6 +11,7 @@ use crate::{config, message, mode, Buffer, Server, User}; const WHO_POLL_INTERVAL: Duration = Duration::from_secs(60); const WHO_RETRY_INTERVAL: Duration = Duration::from_secs(10); +const HIGHLIGHT_BLACKOUT: Duration = Duration::from_secs(5); #[derive(Debug, Clone, Copy)] pub enum Status { @@ -157,12 +157,12 @@ impl Client { } } - fn receive(&mut self, message: message::Encoded) -> Vec { + fn receive(&mut self, message: message::Encoded, config: &config::Config) -> Vec { log::trace!("Message received => {:?}", *message); let stop_reroute = stop_reroute(&message.command); - let events = self.handle(message, None).unwrap_or_default(); + let events = self.handle(message, None, config).unwrap_or_default(); if stop_reroute { self.reroute_responses_to = None; @@ -175,6 +175,7 @@ impl Client { &mut self, mut message: message::Encoded, parent_context: Option, + config: &config::Config, ) -> Option> { use irc::proto::command::Numeric::*; @@ -225,7 +226,7 @@ impl Client { return None; } _ if batch_tag.is_some() => { - let events = self.handle(message, context)?; + let events = self.handle(message, context, config)?; if let Some(batch) = self.batches.get_mut(&batch_tag.unwrap()) { batch.events.extend(events); @@ -364,7 +365,20 @@ impl Client { Command::Numeric(RPL_LOGGEDIN, _) => { log::info!("[{}] logged in", self.server); } - Command::PRIVMSG(_, _) | Command::NOTICE(_, _) => { + Command::PRIVMSG(_, text) | Command::NOTICE(_, text) => { + // Highlight notification + if let Some(user) = message.user() { + if message::reference_user(user.nickname(), self.nickname(), text) { + let notification = &config.notifications.highlight; + if notification.enabled { + // notification::show("Highlight", &server, notification.sound()); + // TODO: + // - Notify highlight. + // - Initial blackout period. + }; + } + } + if let Some(user) = message.user() { // If we sent (echo) & context exists (we sent from this client), ignore if user.nickname() == self.nickname() && context.is_some() { @@ -752,9 +766,14 @@ impl Map { self.client(server).map(Client::nickname) } - pub fn receive(&mut self, server: &Server, message: message::Encoded) -> Vec { + pub fn receive( + &mut self, + server: &Server, + message: message::Encoded, + config: &config::Config, + ) -> Vec { self.client_mut(server) - .map(|client| client.receive(message)) + .map(|client| client.receive(message, config)) .unwrap_or_default() } diff --git a/data/src/config/notification.rs b/data/src/config/notification.rs index 8150b738a..4a12c1d56 100644 --- a/data/src/config/notification.rs +++ b/data/src/config/notification.rs @@ -31,6 +31,8 @@ pub struct Notifications { pub disconnected: Notification, #[serde(default)] pub reconnected: Notification, + #[serde(default)] + pub highlight: Notification, } fn default_sound() -> String { diff --git a/data/src/message.rs b/data/src/message.rs index c8e0adada..92137c829 100644 --- a/data/src/message.rs +++ b/data/src/message.rs @@ -392,3 +392,7 @@ pub fn parse_action(nick: NickRef, text: &str) -> Option { pub fn action_text(nick: NickRef, action: &str) -> String { format!(" ∙ {nick} {action}") } + +pub fn reference_user(sender: NickRef, own_nick: NickRef, text: &str) -> bool { + sender != own_nick && text.contains(own_nick.as_ref()) +} diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index 629909bd7..8755d5096 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -57,8 +57,11 @@ pub fn view<'a>( .map(scroll_view::Message::UserContext); let row_style = match our_nick { Some(nick) - if user.nickname() != nick - && message.text.contains(nick.as_ref()) => + if message::reference_user( + user.nickname(), + nick, + &message.text, + ) => { theme::Container::Highlight } diff --git a/src/main.rs b/src/main.rs index 240bc45e7..d11a00fa0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -319,7 +319,7 @@ impl Application for Halloy { }; messages.into_iter().for_each(|message| { - for event in self.clients.receive(&server, message) { + for event in self.clients.receive(&server, message, &self.config) { match event { data::client::Event::Single(encoded, our_nick) => { if let Some(message) = From 26b481e4f0405d5fd2917d9a99d8c212d23e7456 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Mon, 18 Sep 2023 20:05:24 +0200 Subject: [PATCH 6/8] add highlight and timeout period --- data/src/client.rs | 80 ++++++++++++++++++++++++++++++---------------- src/main.rs | 20 +++++++++++- 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/data/src/client.rs b/data/src/client.rs index 179aed330..561c75cef 100644 --- a/data/src/client.rs +++ b/data/src/client.rs @@ -32,6 +32,11 @@ pub enum State { Ready(Client), } +#[derive(Debug)] +pub enum Notification { + Highlight(User, String), +} + #[derive(Debug)] pub enum Brodcast { Quit { @@ -57,6 +62,7 @@ pub enum Event { Single(message::Encoded, Nick), WithTarget(message::Encoded, Nick, message::Target), Brodcast(Brodcast), + Notification(Notification), } pub struct Client { @@ -75,6 +81,22 @@ pub struct Client { listed_caps: Vec, supports_labels: bool, supports_away_notify: bool, + highlight_blackout: HighlightBlackout, +} + +#[derive(Debug)] +enum HighlightBlackout { + Blackout(Instant), + Receiving, +} + +impl HighlightBlackout { + fn allow_highlights(&self) -> bool { + match self { + HighlightBlackout::Blackout(_) => false, + HighlightBlackout::Receiving => true, + } + } } impl fmt::Debug for Client { @@ -122,6 +144,7 @@ impl Client { listed_caps: vec![], supports_labels: false, supports_away_notify: false, + highlight_blackout: HighlightBlackout::Blackout(Instant::now()), } } @@ -157,12 +180,12 @@ impl Client { } } - fn receive(&mut self, message: message::Encoded, config: &config::Config) -> Vec { + fn receive(&mut self, message: message::Encoded) -> Vec { log::trace!("Message received => {:?}", *message); let stop_reroute = stop_reroute(&message.command); - let events = self.handle(message, None, config).unwrap_or_default(); + let events = self.handle(message, None).unwrap_or_default(); if stop_reroute { self.reroute_responses_to = None; @@ -175,10 +198,11 @@ impl Client { &mut self, mut message: message::Encoded, parent_context: Option, - config: &config::Config, ) -> Option> { use irc::proto::command::Numeric::*; + let mut events = vec![]; + let label_tag = remove_tag("label", message.tags.as_mut()); let batch_tag = remove_tag("batch", message.tags.as_mut()); @@ -226,7 +250,7 @@ impl Client { return None; } _ if batch_tag.is_some() => { - let events = self.handle(message, context, config)?; + let events = self.handle(message, context)?; if let Some(batch) = self.batches.get_mut(&batch_tag.unwrap()) { batch.events.extend(events); @@ -365,23 +389,18 @@ impl Client { Command::Numeric(RPL_LOGGEDIN, _) => { log::info!("[{}] logged in", self.server); } - Command::PRIVMSG(_, text) | Command::NOTICE(_, text) => { - // Highlight notification - if let Some(user) = message.user() { - if message::reference_user(user.nickname(), self.nickname(), text) { - let notification = &config.notifications.highlight; - if notification.enabled { - // notification::show("Highlight", &server, notification.sound()); - // TODO: - // - Notify highlight. - // - Initial blackout period. - }; - } - } - + Command::PRIVMSG(channel, text) | Command::NOTICE(channel, text) => { if let Some(user) = message.user() { - // If we sent (echo) & context exists (we sent from this client), ignore - if user.nickname() == self.nickname() && context.is_some() { + // Highlight notification + if message::reference_user(user.nickname(), self.nickname(), text) + && self.highlight_blackout.allow_highlights() + { + events.push(Event::Notification(Notification::Highlight( + user, + channel.clone(), + ))); + } else if user.nickname() == self.nickname() && context.is_some() { + // If we sent (echo) & context exists (we sent from this client), ignore return None; } } @@ -641,7 +660,8 @@ impl Client { _ => {} } - Some(vec![Event::Single(message, self.nickname().to_owned())]) + events.push(Event::Single(message, self.nickname().to_owned())); + Some(events) } fn sync(&mut self) { @@ -697,6 +717,15 @@ impl Client { Retry, } + match self.highlight_blackout { + HighlightBlackout::Blackout(instant) => { + if now.duration_since(instant) >= HIGHLIGHT_BLACKOUT { + self.highlight_blackout = HighlightBlackout::Receiving; + } + } + HighlightBlackout::Receiving => {} + } + let request = match state.last_who { Some(WhoStatus::Done(last)) if !self.supports_away_notify => { (now.duration_since(last) >= WHO_POLL_INTERVAL).then_some(Request::Poll) @@ -766,14 +795,9 @@ impl Map { self.client(server).map(Client::nickname) } - pub fn receive( - &mut self, - server: &Server, - message: message::Encoded, - config: &config::Config, - ) -> Vec { + pub fn receive(&mut self, server: &Server, message: message::Encoded) -> Vec { self.client_mut(server) - .map(|client| client.receive(message, config)) + .map(|client| client.receive(message)) .unwrap_or_default() } diff --git a/src/main.rs b/src/main.rs index d11a00fa0..a1a7839c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -319,7 +319,7 @@ impl Application for Halloy { }; messages.into_iter().for_each(|message| { - for event in self.clients.receive(&server, message, &self.config) { + for event in self.clients.receive(&server, message) { match event { data::client::Event::Single(encoded, our_nick) => { if let Some(message) = @@ -375,6 +375,24 @@ impl Application for Halloy { ); } }, + data::client::Event::Notification(notification) => { + match notification { + data::client::Notification::Highlight(user, channel) => { + let notification = &self.config.notifications.highlight; + if notification.enabled { + notification::show( + "Highlight", + format!( + "{} highlighted you in {}", + user.nickname(), + channel + ), + notification.sound(), + ); + } + } + } + } } } }); From f70ce6842ac271b3a4508b6af2eecda6c7e5da1f Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Tue, 19 Sep 2023 10:42:53 +0200 Subject: [PATCH 7/8] feedback --- data/src/client.rs | 61 +++++++++++++++++++++++----------------------- src/main.rs | 12 ++++++++- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/data/src/client.rs b/data/src/client.rs index 561c75cef..87c6eb0da 100644 --- a/data/src/client.rs +++ b/data/src/client.rs @@ -11,7 +11,7 @@ use crate::{config, message, mode, Buffer, Server, User}; const WHO_POLL_INTERVAL: Duration = Duration::from_secs(60); const WHO_RETRY_INTERVAL: Duration = Duration::from_secs(10); -const HIGHLIGHT_BLACKOUT: Duration = Duration::from_secs(5); +const HIGHLIGHT_BLACKOUT_INTERVAL: Duration = Duration::from_secs(5); #[derive(Debug, Clone, Copy)] pub enum Status { @@ -62,7 +62,7 @@ pub enum Event { Single(message::Encoded, Nick), WithTarget(message::Encoded, Nick, message::Target), Brodcast(Brodcast), - Notification(Notification), + Notification(message::Encoded, Nick, Notification), } pub struct Client { @@ -84,21 +84,6 @@ pub struct Client { highlight_blackout: HighlightBlackout, } -#[derive(Debug)] -enum HighlightBlackout { - Blackout(Instant), - Receiving, -} - -impl HighlightBlackout { - fn allow_highlights(&self) -> bool { - match self { - HighlightBlackout::Blackout(_) => false, - HighlightBlackout::Receiving => true, - } - } -} - impl fmt::Debug for Client { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Client").finish() @@ -395,10 +380,11 @@ impl Client { if message::reference_user(user.nickname(), self.nickname(), text) && self.highlight_blackout.allow_highlights() { - events.push(Event::Notification(Notification::Highlight( - user, - channel.clone(), - ))); + events.push(Event::Notification( + message.clone(), + self.nickname().to_owned(), + Notification::Highlight(user, channel.clone()), + )); } else if user.nickname() == self.nickname() && context.is_some() { // If we sent (echo) & context exists (we sent from this client), ignore return None; @@ -711,21 +697,21 @@ impl Client { } pub fn tick(&mut self, now: Instant) { + match self.highlight_blackout { + HighlightBlackout::Blackout(instant) => { + if now.duration_since(instant) >= HIGHLIGHT_BLACKOUT_INTERVAL { + self.highlight_blackout = HighlightBlackout::Receiving; + } + } + HighlightBlackout::Receiving => {} + } + for (channel, state) in self.chanmap.iter_mut() { enum Request { Poll, Retry, } - match self.highlight_blackout { - HighlightBlackout::Blackout(instant) => { - if now.duration_since(instant) >= HIGHLIGHT_BLACKOUT { - self.highlight_blackout = HighlightBlackout::Receiving; - } - } - HighlightBlackout::Receiving => {} - } - let request = match state.last_who { Some(WhoStatus::Done(last)) if !self.supports_away_notify => { (now.duration_since(last) >= WHO_POLL_INTERVAL).then_some(Request::Poll) @@ -752,6 +738,21 @@ impl Client { } } +#[derive(Debug)] +enum HighlightBlackout { + Blackout(Instant), + Receiving, +} + +impl HighlightBlackout { + fn allow_highlights(&self) -> bool { + match self { + HighlightBlackout::Blackout(_) => false, + HighlightBlackout::Receiving => true, + } + } +} + #[derive(Debug, Default)] pub struct Map(BTreeMap); diff --git a/src/main.rs b/src/main.rs index a1a7839c9..05aa05bfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -375,7 +375,17 @@ impl Application for Halloy { ); } }, - data::client::Event::Notification(notification) => { + data::client::Event::Notification( + encoded, + our_nick, + notification, + ) => { + if let Some(message) = + data::Message::received(encoded, our_nick) + { + dashboard.record_message(&server, message); + } + match notification { data::client::Notification::Highlight(user, channel) => { let notification = &self.config.notifications.highlight; From 2f855babba40ce0b361236b813de91cbe7b97374 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Tue, 19 Sep 2023 19:16:55 +0200 Subject: [PATCH 8/8] return instead of use vec --- data/src/client.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/data/src/client.rs b/data/src/client.rs index 87c6eb0da..4f39c580d 100644 --- a/data/src/client.rs +++ b/data/src/client.rs @@ -186,8 +186,6 @@ impl Client { ) -> Option> { use irc::proto::command::Numeric::*; - let mut events = vec![]; - let label_tag = remove_tag("label", message.tags.as_mut()); let batch_tag = remove_tag("batch", message.tags.as_mut()); @@ -380,11 +378,11 @@ impl Client { if message::reference_user(user.nickname(), self.nickname(), text) && self.highlight_blackout.allow_highlights() { - events.push(Event::Notification( + return Some(vec![Event::Notification( message.clone(), self.nickname().to_owned(), Notification::Highlight(user, channel.clone()), - )); + )]); } else if user.nickname() == self.nickname() && context.is_some() { // If we sent (echo) & context exists (we sent from this client), ignore return None; @@ -646,8 +644,7 @@ impl Client { _ => {} } - events.push(Event::Single(message, self.nickname().to_owned())); - Some(events) + Some(vec![Event::Single(message, self.nickname().to_owned())]) } fn sync(&mut self) {