diff --git a/.github/workflows/lints.yaml b/.github/workflows/lints.yaml index 8ac5229b..3187612d 100644 --- a/.github/workflows/lints.yaml +++ b/.github/workflows/lints.yaml @@ -46,6 +46,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: + sh_checker_shfmt_disable: true sh_checker_comment: true sh_checker_checkbashisms_enable: true diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f64959f3..dfe96389 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,6 +2,8 @@ name: Release on: push: + branches: + - release/* tags: - "v*" @@ -9,6 +11,11 @@ defaults: run: shell: bash +env: + MAINTAINER: "xrelkd <46590321+xrelkd@users.noreply.github.com>" + PACKAGE_NAME: "clipcat" + PACKAGE_DESCRIPTION: "Clipboard manager written in Rust Programming Language" + jobs: all: name: Release @@ -16,30 +23,15 @@ jobs: strategy: matrix: target: - - aarch64-unknown-linux-musl - - armv7-unknown-linux-musleabihf - x86_64-unknown-linux-musl - # TODO: support macOS in the future? - # - x86_64-apple-darwin include: - - target: aarch64-unknown-linux-musl - os: ubuntu-latest - target_rustflags: "--codegen linker=aarch64-linux-gnu-gcc" - - - target: armv7-unknown-linux-musleabihf - os: ubuntu-latest - target_rustflags: "--codegen linker=arm-linux-gnueabihf-gcc" - - target: x86_64-unknown-linux-musl + arch_deb: amd64 + arch_rpm: x86_64 os: ubuntu-latest target_rustflags: "" - # TODO: support macOS in the future? - # - target: x86_64-apple-darwin - # os: macos-latest - # target_rustflags: "" - runs-on: ${{matrix.os}} steps: @@ -48,7 +40,7 @@ jobs: - name: Install Dependencies if: ${{ matrix.os == 'ubuntu-latest' }} run: | - sudo apt install -y protobuf-compiler + sudo apt install -y --no-install-recommends musl-tools protobuf-compiler libprotobuf-dev - name: Install Rust Toolchain Components uses: actions-rs/toolchain@v1 @@ -57,18 +49,6 @@ jobs: target: ${{ matrix.target }} toolchain: stable - - name: Install AArch64 Toolchain - if: ${{ matrix.target == 'aarch64-unknown-linux-musl' }} - run: | - sudo apt update - sudo apt install -y gcc-aarch64-linux-gnu - - - name: Install ARM7 Toolchain - if: ${{ matrix.target == 'armv7-unknown-linux-musleabihf' }} - run: | - sudo apt update - sudo apt install -y gcc-arm-linux-gnueabihf - - name: Create Package id: package env: @@ -79,7 +59,54 @@ jobs: run: ./dev-support/bin/create-package shell: bash - - name: Publish Archive + - name: Prepare DEB Package + env: + TARGET: ${{ matrix.target }} + REF: ${{ github.ref }} + OS: ${{ matrix.os }} + ARCH: ${{ matrix.arch_deb }} + TARGET_RUSTFLAGS: ${{ matrix.target_rustflags }} + run: ./dev-support/bin/prepare-deb-package + if: ${{ matrix.arch_deb == 'amd64' }} + shell: bash + + - name: Create DEB Package + id: deb-package + uses: jiro4989/build-deb-action@v3 + if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.arch_deb == 'amd64' }} + with: + package: ${{ env.PACKAGE_NAME }} + package_root: .debpkg + maintainer: ${{ env.MAINTAINER }} + version: ${{ github.ref }} + arch: ${{ matrix.arch_deb }} + desc: ${{ env.PACKAGE_DESCRIPTION }} + + - name: Prepare RPM Package + env: + TARGET: ${{ matrix.target }} + REF: ${{ github.ref }} + OS: ${{ matrix.os }} + ARCH: ${{ matrix.arch_rpm }} + TARGET_RUSTFLAGS: ${{ matrix.target_rustflags }} + run: ./dev-support/bin/prepare-rpm-package + if: ${{ matrix.arch_rpm == 'x86_64' }} + shell: bash + + - name: Create RPM Package + id: rpm-package + uses: jiro4989/build-rpm-action@v2 + if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.arch_rpm == 'x86_64' }} + with: + summary: ${{ env.PACKAGE_DESCRIPTION }} + package: ${{ env.PACKAGE_NAME }} + package_root: .rpmpkg + maintainer: ${{ env.MAINTAINER }} + version: ${{ github.ref }} + arch: ${{ matrix.arch_rpm }} + desc: ${{ env.PACKAGE_DESCRIPTION }} + + - name: Publish Package Archive uses: softprops/action-gh-release@v1 if: ${{ startsWith(github.ref, 'refs/tags/') }} with: @@ -88,3 +115,23 @@ jobs: prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish DEB Package + uses: softprops/action-gh-release@v1 + if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.arch_deb == 'amd64' }} + with: + draft: false + files: ${{ steps.deb-package.outputs.file_name }} + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish RPM Package + uses: softprops/action-gh-release@v1 + if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.arch_rpm == 'x86_64' }} + with: + draft: false + files: ${{ steps.rpm-package.outputs.file_name }} + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 85b37161..84fe857a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,9 +311,9 @@ checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "531b97fb4cd3dfdce92c35dedbfdc1f0b9d8091c8ca943d6dae340ef5012d514" dependencies = [ "proc-macro2", "quote", @@ -354,8 +354,9 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", + "headers", "http 0.2.11", - "http-body", + "http-body 0.4.6", "hyper", "itoa", "matchit", @@ -365,7 +366,11 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper", + "tokio", "tower", "tower-layer", "tower-service", @@ -381,7 +386,7 @@ dependencies = [ "bytes", "futures-util", "http 0.2.11", - "http-body", + "http-body 0.4.6", "mime", "rustversion", "tower-layer", @@ -509,6 +514,7 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ + "jobserver", "libc", ] @@ -551,9 +557,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.11" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" dependencies = [ "clap_builder", "clap_derive", @@ -561,9 +567,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" dependencies = [ "anstream", "anstyle", @@ -573,11 +579,11 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.4.4" +version = "4.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffe91f06a11b4b9420f62103854e90867812cd5d01557f853c5ee8e791b12ae" +checksum = "a51919c5608a32e34ea1d6be321ad070065e17613e168c5b6977024290f2630b" dependencies = [ - "clap 4.4.11", + "clap 4.4.12", ] [[package]] @@ -620,7 +626,7 @@ dependencies = [ [[package]] name = "clipcat-base" -version = "0.15.0" +version = "0.16.0" dependencies = [ "bytes", "directories", @@ -633,13 +639,14 @@ dependencies = [ "semver", "serde", "sha2", - "snafu", + "snafu 0.8.0", "time", + "tokio", ] [[package]] name = "clipcat-cli" -version = "0.15.0" +version = "0.16.0" dependencies = [ "serde", "serde_with", @@ -650,7 +657,7 @@ dependencies = [ [[package]] name = "clipcat-client" -version = "0.15.0" +version = "0.16.0" dependencies = [ "async-trait", "clipcat-base", @@ -659,7 +666,7 @@ dependencies = [ "mime", "prost-types", "semver", - "snafu", + "snafu 0.8.0", "tokio", "tonic", "tower", @@ -668,7 +675,7 @@ dependencies = [ [[package]] name = "clipcat-clipboard" -version = "0.15.0" +version = "0.16.0" dependencies = [ "arboard", "bytes", @@ -677,7 +684,7 @@ dependencies = [ "mio", "parking_lot", "sigfinn", - "snafu", + "snafu 0.8.0", "tokio", "tracing", "tracing-subscriber", @@ -685,20 +692,31 @@ dependencies = [ "x11rb 0.13.0", ] +[[package]] +name = "clipcat-dbus-variant" +version = "0.16.0" +dependencies = [ + "clipcat-base", + "mime", + "serde", + "time", + "zvariant", +] + [[package]] name = "clipcat-external-editor" -version = "0.15.0" +version = "0.16.0" dependencies = [ "clipcat-base", - "snafu", + "snafu 0.8.0", "tokio", ] [[package]] name = "clipcat-menu" -version = "0.15.0" +version = "0.16.0" dependencies = [ - "clap 4.4.11", + "clap 4.4.12", "clap_complete", "clipcat-base", "clipcat-cli", @@ -707,8 +725,9 @@ dependencies = [ "http 1.0.0", "http-serde", "serde", + "shadow-rs", "skim", - "snafu", + "snafu 0.8.0", "tokio", "toml", "tracing", @@ -716,25 +735,41 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "clipcat-metrics" +version = "0.16.0" +dependencies = [ + "async-trait", + "axum", + "bytes", + "lazy_static", + "mime", + "prometheus", + "snafu 0.8.0", + "tower", + "tower-http", +] + [[package]] name = "clipcat-notify" -version = "0.15.0" +version = "0.16.0" dependencies = [ - "clap 4.4.11", + "clap 4.4.12", "clap_complete", "clipcat-base", "clipcat-server", "mime", "serde", "serde_json", - "snafu", + "shadow-rs", + "snafu 0.8.0", "time", "tokio", ] [[package]] name = "clipcat-proto" -version = "0.15.0" +version = "0.16.0" dependencies = [ "clipcat-base", "mime", @@ -748,39 +783,46 @@ dependencies = [ [[package]] name = "clipcat-server" -version = "0.15.0" +version = "0.16.0" dependencies = [ "async-trait", "bincode", "clipcat-base", "clipcat-clipboard", + "clipcat-dbus-variant", + "clipcat-metrics", "clipcat-proto", "futures", "hex", "humansize", "lazy_static", "mime", + "notify", "notify-rust", "parking_lot", + "prometheus", "regex", "semver", "serde", "serde_json", "sigfinn", - "snafu", + "simdutf8", + "snafu 0.8.0", "time", "tokio", "tokio-stream", "tonic", "tracing", + "zbus", + "zvariant", ] [[package]] name = "clipcatctl" -version = "0.15.0" +version = "0.16.0" dependencies = [ "bytes", - "clap 4.4.11", + "clap 4.4.12", "clap_complete", "clipcat-base", "clipcat-cli", @@ -791,8 +833,9 @@ dependencies = [ "http-serde", "mime", "serde", + "shadow-rs", "simdutf8", - "snafu", + "snafu 0.8.0", "tokio", "toml", "tracing", @@ -802,9 +845,9 @@ dependencies = [ [[package]] name = "clipcatd" -version = "0.15.0" +version = "0.16.0" dependencies = [ - "clap 4.4.11", + "clap 4.4.12", "clap_complete", "clipcat-base", "clipcat-cli", @@ -816,8 +859,9 @@ dependencies = [ "linicon", "mime", "serde", + "shadow-rs", "simdutf8", - "snafu", + "snafu 0.8.0", "time", "tokio", "toml", @@ -847,6 +891,32 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const_fn" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" + +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1380,6 +1450,18 @@ dependencies = [ "nix 0.25.1", ] +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1426,6 +1508,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "freedesktop_entry_parser" version = "1.3.0" @@ -1436,11 +1527,20 @@ dependencies = [ "thiserror", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1453,9 +1553,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1463,15 +1563,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1480,9 +1580,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -1514,9 +1614,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", @@ -1525,21 +1625,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1619,6 +1719,19 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "git2" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf97ba92db08df386e10c8ede66a2a0369bd277090afd8710e19e38de9ec0cd" +dependencies = [ + "bitflags 2.4.1", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "h2" version = "0.3.22" @@ -1668,6 +1781,30 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http 0.2.11", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.11", +] + [[package]] name = "heck" version = "0.4.1" @@ -1737,6 +1874,29 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "pin-project-lite", +] + [[package]] name = "http-serde" version = "2.0.0" @@ -1786,7 +1946,7 @@ dependencies = [ "futures-util", "h2", "http 0.2.11", - "http-body", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -1839,6 +1999,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "image" version = "0.24.7" @@ -1880,6 +2050,26 @@ dependencies = [ "serde", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.12" @@ -1900,6 +2090,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is_debug" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d198e9919d9822d5f7083ba8530e04de87841eaf21ead9af8f2304efd57c89" + [[package]] name = "itertools" version = "0.11.0" @@ -1915,6 +2111,15 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.3.0" @@ -1933,6 +2138,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1951,6 +2176,18 @@ version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +[[package]] +name = "libgit2-sys" +version = "0.16.1+1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + [[package]] name = "libloading" version = "0.8.1" @@ -1978,6 +2215,18 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libz-sys" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linicon" version = "2.3.0" @@ -2187,6 +2436,25 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.4.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "notify-rust" version = "4.10.0" @@ -2529,6 +2797,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prometheus" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror", +] + [[package]] name = "prost" version = "0.12.3" @@ -2583,6 +2866,12 @@ dependencies = [ "prost", ] +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + [[package]] name = "qoi" version = "0.4.1" @@ -2764,6 +3053,15 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2813,6 +3111,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.17" @@ -2833,6 +3141,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.4.0" @@ -2884,6 +3204,19 @@ dependencies = [ "digest", ] +[[package]] +name = "shadow-rs" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "878cb1e3162d98ee1016b832efbb683ad6302b462a2894c54f488dc0bd96f11c" +dependencies = [ + "const_format", + "git2", + "is_debug", + "time", + "tzdb", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2901,12 +3234,12 @@ checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" [[package]] name = "sigfinn" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ccc3278dafd66473c654456b52f3d356df5c5d0ce149b39268ff59654afbcc" +checksum = "4b56d8d2e5afd3a3ca29e98bbcb4de595db4101b988a2990c0f014e42599b464" dependencies = [ "futures", - "snafu", + "snafu 0.7.5", "tokio", "tracing", ] @@ -2985,7 +3318,16 @@ dependencies = [ "doc-comment", "futures-core", "pin-project", - "snafu-derive", + "snafu-derive 0.7.5", +] + +[[package]] +name = "snafu" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d342c51730e54029130d7dc9fd735d28c4cd360f1368c01981d4f03ff207f096" +dependencies = [ + "snafu-derive 0.8.0", ] [[package]] @@ -3000,6 +3342,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "snafu-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080c44971436b1af15d6f61ddd8b543995cf63ab8e677d46b00cc06f4ef267a0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.41", +] + [[package]] name = "socket2" version = "0.4.10" @@ -3167,9 +3521,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", "itoa", @@ -3189,9 +3543,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" dependencies = [ "time-core", ] @@ -3205,11 +3559,26 @@ dependencies = [ "chrono", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.35.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes", @@ -3220,6 +3589,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.5", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] @@ -3328,7 +3698,7 @@ dependencies = [ "flate2", "h2", "http 0.2.11", - "http-body", + "http-body 0.4.6", "hyper", "hyper-timeout", "percent-encoding", @@ -3375,6 +3745,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e12e6351354851911bdf8c2b8f2ab15050c567d70a8b9a37ae7b8301a4080d" +dependencies = [ + "bitflags 2.4.1", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -3496,6 +3884,35 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "tz-rs" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33851b15c848fad2cf4b105c6bb66eb9512b6f6c44a4b13f57c53c73c707e2b4" +dependencies = [ + "const_fn", +] + +[[package]] +name = "tzdb" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b580f6b365fa89f5767cdb619a55d534d04a4e14c2d7e5b9a31e94598687fb1" +dependencies = [ + "iana-time-zone", + "tz-rs", + "tzdb_data", +] + +[[package]] +name = "tzdb_data" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63691b1ba8e003011b427b0a2c369e2795d8f179b7ad6df3e3d7692338ff4b3" +dependencies = [ + "tz-rs", +] + [[package]] name = "uds_windows" version = "1.1.0" @@ -3507,18 +3924,50 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -3531,6 +3980,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -3564,6 +4019,16 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -4025,6 +4490,7 @@ dependencies = [ "serde_repr", "sha1", "static_assertions", + "tokio", "tracing", "uds_windows", "winapi", diff --git a/Cargo.toml b/Cargo.toml index 5af4a8ef..7e948521 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.15.0" +version = "0.16.0" authors = ["xrelkd <46590321+xrelkd@users.noreply.github.com>"] homepage = "https://github.com/xrelkd/clipcat" repository = "https://github.com/xrelkd/clipcat" @@ -21,7 +21,9 @@ members = [ "crates/cli", "crates/client", "crates/clipboard", + "crates/dbus-variant", "crates/external-editor", + "crates/metrics", "crates/proto", "crates/server", ] diff --git a/README.md b/README.md index cec2ed82..37b4b20f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@

+ GitHub License @@ -39,6 +40,7 @@ - [x] Support `gRPC` - [x] gRPC over `HTTP` - [x] gRPC over `Unix domain socket` +- [x] Support `D-Bus` ## Screenshots @@ -59,11 +61,12 @@

Install with package manager -| Linux Distribution | Package Manager | Package | Command | -| ----------------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| Various | [Nix](https://github.com/NixOS/nix) | [clipcat](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/clipcat/default.nix) | `nix profile install 'github:xrelkd/clipcat/main'` or
`nix-env -iA nixpkgs.clipcat` | -| [NixOS](https://nixos.org) | [Nix](https://github.com/NixOS/nix) | [clipcat](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/clipcat/default.nix) | `nix profile install 'github:xrelkd/clipcat/main'` or
`nix-env -iA nixos.clipcat` | -| [Arch Linux](https://archlinux.org) | [Yay](https://github.com/Jguer/yay) | [clipcat](https://aur.archlinux.org/packages/clipcat/) | `yay -S clipcat` | +| Linux Distribution | Package Manager | Package | Command | +| ------------------------------------------------------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Various | [Nix](https://github.com/NixOS/nix) | [clipcat](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/clipcat/default.nix) | `nix profile install 'github:xrelkd/clipcat/main'` or
`nix-env -iA nixpkgs.clipcat` | +| [NixOS](https://nixos.org) | [Nix](https://github.com/NixOS/nix) | [clipcat](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/clipcat/default.nix) | `nix profile install 'github:xrelkd/clipcat/main'` or
`nix-env -iA nixos.clipcat` | +| [Arch Linux](https://archlinux.org) | [Yay](https://github.com/Jguer/yay) | [clipcat](https://aur.archlinux.org/packages/clipcat/) | `yay -S clipcat` | +| [Debian](https://debian.org) and [Ubuntu](https://ubuntu.com) derivatives | APT | [clipcat](https://github.com/xrelkd/clipcat/releases/latest) | `dpkg -i clipcat_*.deb` |
@@ -83,10 +86,10 @@ cd ~/bin # download and extract clipcat to ~/bin/ # NOTE: replace the version with the version you want to install -export CLIPCAT_VERSION=v0.15.0 +export CLIPCAT_VERSION=v0.16.0 # NOTE: the architecture of your machine, -# available values are `x86_64-unknown-linux-musl`, `armv7-unknown-linux-musleabihf`, `aarch64-unknown-linux-musl` +# available value is `x86_64-unknown-linux-musl` export ARCH=x86_64-unknown-linux-musl curl -s -L "https://github.com/xrelkd/clipcat/releases/download/${CLIPCAT_VERSION}/clipcat-${CLIPCAT_VERSION}-${ARCH}.tar.gz" | tar xzf - @@ -179,7 +182,7 @@ clipcat-menu default-config > $XDG_CONFIG_HOME/clipcat/clipcat-menu.toml 1. Start `clipcatd` for watching clipboard events. ```bash -# show the usage, please read the usage before doing any other operations +# Show the usage, please read the usage before doing any other operations. clipcatd help # Start and daemonize clipcatd, clipcatd will run in the background. @@ -228,94 +231,108 @@ clipcatd --no-daemon Configuration for clipcatd ```toml -# run as a traditional UNIX daemon +# Run as a traditional UNIX daemon. daemonize = true -# maximum number of clip history +# Maximum number of clip history. max_history = 50 -# file path of clip history, -# if you omit this value, clipcatd will persist history in `$XDG_CACHE_HOME/clipcat/clipcatd-history` +# File path of clip history, +# if you omit this value, clipcatd will persist history in `$XDG_CACHE_HOME/clipcat/clipcatd-history`. history_file_path = "/home//.cache/clipcat/clipcatd-history" -# file path of PID file, -# if you omit this value, clipcatd will place the PID file on `$XDG_RUNTIME_DIR/clipcatd.pid` +# File path of PID file, +# if you omit this value, clipcatd will place the PID file on `$XDG_RUNTIME_DIR/clipcatd.pid`. pid_file = "/run/user//clipcatd.pid" [log] -# emit log message to a log file. -# if you omit this value, clipcatd will disable emitting to a log file +# Emit log message to a log file. +# If you omit this value, clipcatd will disable emitting to a log file. file_path = "/path/to/log/file" -# emit log message to journald +# Emit log message to systemd-journald. emit_journald = true -# emit log message to stdout +# Emit log message to stdout. emit_stdout = false -# emit log message to stderr +# Emit log message to stderr. emit_stderr = false -# log level +# Log level level = "INFO" [watcher] -# load current clipboard content at startup -load_current = true -# enable watching X11/Wayland clipboard selection +# Enable watching X11/Wayland clipboard selection. enable_clipboard = true -# enable watching X11/Wayland primary selection +# Enable watching X11/Wayland primary selection. enable_primary = true -# ignore clips which match with one of the X11 `TARGETS` +# Ignore clips which match with one of the X11 `TARGETS`. sensitive_x11_atoms = ["x-kde-passwordManagerHint"] -# ignore text clips which match with one of the regular expressions -# the regular expression engine is powered by https://github.com/rust-lang/regex +# Ignore text clips which match with one of the regular expressions. +# The regular expression engine is powered by https://github.com/rust-lang/regex . denied_text_regex_patterns = [] -# ignore text clips with a length <= `filter_text_min_length`, in characters (Unicode scalar value), not in byte +# Ignore text clips with a length <= `filter_text_min_length`, in characters (Unicode scalar value), not in byte. filter_text_min_length = 1 -# ignore text clips with a length > `filter_text_max_length`, in characters (Unicode scalar value), not in byte +# Ignore text clips with a length > `filter_text_max_length`, in characters (Unicode scalar value), not in byte. filter_text_max_length = 20000000 -# enable capturing image or not +# Enable capturing image or not. capture_image = true -# ignore image clips with a size > `filter_image_max_size`, in byte +# Ignore image clips with a size > `filter_image_max_size`, in byte. filter_image_max_size = 5242880 [grpc] -# enable gRPC over http +# Enable gRPC over http. enable_http = true -# enable gRPC over unix domain socket +# Enable gRPC over unix domain socket. enable_local_socket = true -# host address for gRPC +# Host address for gRPC. host = "127.0.0.1" -# port number for gRPC +# Port number for gRPC. port = 45045 -# path of unix domain socket -# if you omit this value, clipcatd will place the socket on `$XDG_RUNTIME_DIR/clipcat/grpc.sock` +# Path of unix domain socket. +# If you omit this value, clipcatd will place the socket on `$XDG_RUNTIME_DIR/clipcat/grpc.sock`. local_socket = "/run/user//clipcat/grpc.sock" +[dbus] +# Enable D-Bus. +enable = true + +# Specify the identifier for current clipcat instance. +# The D-Bus service name shows as "org.clipcat.clipcat.instance-0". +# If identifier is not provided, D-Bus service name shows as "org.clipcat.clipcat". +identifier = "instance-0" + [desktop_notification] -# enable desktop notification +# Enable desktop notification. enable = true -# path of a icon, the given icon will be displayed on desktop notification, +# Path of a icon, the given icon will be displayed on desktop notification, # if your desktop notification server supports showing a icon -# if not provided, the value `accessories-clipboard` will be applied +# If not provided, the value `accessories-clipboard` will be applied. icon = "/path/to/the/icon" -# timeout duration in milliseconds -# this sets the time from the time the notification is displayed until it is -# closed again by the notification server +# Timeout duration in milliseconds. +# This sets the time from the time the notification is displayed until it is +# closed again by the notification server. timeout_ms = 2000 -# define the length of a long plaintext, +# Define the length of a long plaintext, # if the length of a plaintext is >= `long_plaintext_length`, -# desktop notification will be emitted -# if this value is 0, no desktop desktop notification will be emitted when fetched plaintext +# desktop notification will be emitted. +# If this value is 0, no desktop desktop notification will be emitted when fetched a long plaintext. long_plaintext_length = 2000 -# snippets, only UTF-8 text is supported. +# Snippets, only UTF-8 text is supported. [[snippets]] -# name of snippet -name = "os-release" +[snippets.Directory] +# Name of snippet +name = "my-snippets" +# File path to the directory containing snippets. +path = "/home/user/snippets" -# file path to the snippet, if both `content` and `file_path` are provided, `file_path` is preferred -file_path = "/etc/os-release" +[[snippets]] +[snippets.File] +# Name of snippet. +name = "os-release" +# File path to the snippet. +path = "/etc/os-release" [[snippets]] -# name of snippet +[snippets.Text] +# Name of snippet. name = "cxx-io-speed-up" - -# content of the snippet, if both `content` and `file_path` are provided, `file_path` is preferred +# Content of the snippet. content = ''' int io_speed_up = [] { std::ios::sync_with_stdio(false); @@ -326,6 +343,7 @@ int io_speed_up = [] { ''' [[snippets]] +[snippets.Text] name = "rust-sieve-primes" content = ''' fn sieve_primes(n: usize) -> Vec { @@ -358,22 +376,23 @@ fn sieve_primes(n: usize) -> Vec { Configuration for clipcatctl ```toml -# server endpoint +# Server endpoint. # clipcatctl connects to server via unix domain socket if `server_endpoint` is a file path like: # "/run/user//clipcat/grpc.sock". # clipcatctl connects to server via http if `server_endpoint` is a URL like: "http://127.0.0.1:45045" server_endpoint = "/run/user//clipcat/grpc.sock" [log] -# emit log message to a log file. Delete this line to disable emitting to a log file +# Emit log message to a log file. +# Delete this line to disable emitting to a log file. file_path = "/path/to/log/file" -# emit log message to journald +# Emit log message to systemd-journald emit_journald = true -# emit log message to stdout +# Emit log message to stdout. emit_stdout = false -# emit log message to stderr +# Emit log message to stderr. emit_stderr = false -# log level +# Log level level = "INFO" ``` @@ -383,47 +402,47 @@ level = "INFO" Configuration for clipcat-menu ```toml -# server endpoint +# Server endpoint # clipcat-menu connects to server via unix domain socket if `server_endpoint` is a file path like: # "/run/user//clipcat/grpc.sock". -# clipcat-menu connects to server via http if `server_endpoint` is a URL like: "http://127.0.0.1:45045" +# clipcat-menu connects to server via http if `server_endpoint` is a URL like: "http://127.0.0.1:45045". server_endpoint = "/run/user//clipcat/grpc.sock" -# the default finder to invoke when no "--finder=" option provided +# The default finder to invoke when no "--finder=" option provided. finder = "rofi" [log] -# emit log message to a log file. Delete this line to disable emitting to a log file +# Emit log message to a log file. Delete this line to disable emitting to a log file. file_path = "/path/to/log/file" -# emit log message to journald +# Emit log message to systemd-journald. emit_journald = true -# emit log message to stdout +# Emit log message to stdout. emit_stdout = false -# emit log message to stderr +# Emit log message to stderr. emit_stderr = false -# log level +# Log level. level = "INFO" -# options for "rofi" +# Options for "rofi". [rofi] -# length of line +# Length of line. line_length = 100 -# length of menu +# Length of menu. menu_length = 30 -# prompt of menu +# Prompt of menu. menu_prompt = "Clipcat" -# extra arguments to pass to `rofi` +# Extra arguments to pass to `rofi`. extra_arguments = ["-mesg", "Please select a clip"] -# options for "dmenu" +# Options for "dmenu". [dmenu] -# length of line +# Length of line. line_length = 100 -# length of menu +# Length of menu. menu_length = 30 -# prompt of menu +# Prompt of menu. menu_prompt = "Clipcat" -# extra arguments to pass to `dmenu` +# Extra arguments to pass to `dmenu`. extra_arguments = [ "-fn", "SauceCodePro Nerd Font Mono-12", @@ -437,11 +456,11 @@ extra_arguments = [ "#282828", ] -# customize your finder +# Customize your finder. [custom_finder] -# external program name +# External program name. program = "fzf" -# arguments for calling external program +# Arguments for calling external program. args = [] ``` @@ -516,7 +535,7 @@ Add the following command in your `$XDG_CONFIG_HOME/leftwm/themes/current/up`: ```bash # other configurations -# start clipcatd +# Start clipcatd clipcatd # other configurations @@ -527,7 +546,7 @@ Add the following command in your `$XDG_CONFIG_HOME/leftwm/themes/current/down`: ```bash # other configurations -# terminate clipcatd +# Terminate clipcatd pkill clipcatd # other configurations diff --git a/clipcat-menu/Cargo.toml b/clipcat-menu/Cargo.toml index f405e0d5..292368e8 100644 --- a/clipcat-menu/Cargo.toml +++ b/clipcat-menu/Cargo.toml @@ -25,13 +25,17 @@ tokio = { version = "1", features = ["rt-multi-thread", "sync"] } clap = { version = "4", features = ["derive", "env"] } clap_complete = "4" http = "1" +shadow-rs = "0.26" skim = "0.10" -snafu = "0.7" +snafu = "0.8" clipcat-base = { path = "../crates/base" } clipcat-cli = { path = "../crates/cli" } clipcat-client = { path = "../crates/client" } clipcat-external-editor = { path = "../crates/external-editor" } +[build-dependencies] +shadow-rs = "0.26" + [lints] workspace = true diff --git a/clipcat-menu/build.rs b/clipcat-menu/build.rs new file mode 100644 index 00000000..e0f6c422 --- /dev/null +++ b/clipcat-menu/build.rs @@ -0,0 +1 @@ +fn main() -> shadow_rs::SdResult<()> { shadow_rs::new() } diff --git a/clipcat-menu/src/cli/mod.rs b/clipcat-menu/src/cli/mod.rs index 9d06b6e5..4fbd065d 100644 --- a/clipcat-menu/src/cli/mod.rs +++ b/clipcat-menu/src/cli/mod.rs @@ -13,12 +13,20 @@ use crate::{ config::Config, error::{self, Error}, finder::{FinderRunner, FinderType}, + shadow, }; const PREVIEW_LENGTH: usize = 80; #[derive(Parser)] -#[command(name = clipcat_base::MENU_PROGRAM_NAME, author, version, about, long_about = None)] +#[command( + name = clipcat_base::MENU_PROGRAM_NAME, + author, + version, + long_version = shadow::CLAP_LONG_VERSION, + about, + long_about = None +)] pub struct Cli { #[command(subcommand)] commands: Option, @@ -144,7 +152,10 @@ impl Cli { let finder = build_finder(finder, rofi_config, dmenu_config, custom_finder_config, &mut config); let fut = async move { - let client = Client::new(config.server_endpoint).await?; + let client = { + let access_token = config.access_token(); + Client::new(config.server_endpoint, access_token).await? + }; let clips = client.list(PREVIEW_LENGTH).await?; match commands { @@ -310,9 +321,12 @@ mod tests { #[test] fn test_command_simple() { - match Cli::parse_from(["program_name", "version"]).commands { - Some(Commands::Version { .. }) => (), - _ => panic!(), + if let Some(Commands::Version { .. }) = + Cli::parse_from(["program_name", "version"]).commands + { + // everything is good. + } else { + panic!(); } } } diff --git a/clipcat-menu/src/config.rs b/clipcat-menu/src/config.rs index e39df478..05f71301 100644 --- a/clipcat-menu/src/config.rs +++ b/clipcat-menu/src/config.rs @@ -10,6 +10,10 @@ pub struct Config { #[serde(default = "clipcat_base::config::default_server_endpoint", with = "http_serde::uri")] pub server_endpoint: http::Uri, + pub access_token: Option, + + pub access_token_file_path: Option, + #[serde(default)] pub finder: FinderType, @@ -42,7 +46,16 @@ impl Config { let data = std::fs::read_to_string(&path) .context(OpenConfigSnafu { filename: path.as_ref().to_path_buf() })?; - toml::from_str(&data).context(ParseConfigSnafu { filename: path.as_ref().to_path_buf() }) + let mut config: Self = toml::from_str(&data) + .context(ParseConfigSnafu { filename: path.as_ref().to_path_buf() })?; + + if let Some(ref file_path) = config.access_token_file_path { + if let Ok(token) = std::fs::read_to_string(file_path) { + config.access_token = Some(token.trim_end().to_string()); + } + } + + Ok(config) } #[inline] @@ -59,12 +72,16 @@ impl Config { } } } + + pub fn access_token(&self) -> Option { self.access_token.clone() } } impl Default for Config { fn default() -> Self { Self { server_endpoint: clipcat_base::config::default_server_endpoint(), + access_token: None, + access_token_file_path: None, finder: FinderType::Rofi, rofi: Some(Rofi::default()), dmenu: Some(Dmenu::default()), diff --git a/clipcat-menu/src/main.rs b/clipcat-menu/src/main.rs index 35eeb5da..8fc9ec41 100644 --- a/clipcat-menu/src/main.rs +++ b/clipcat-menu/src/main.rs @@ -2,6 +2,13 @@ mod cli; mod config; mod error; mod finder; +mod shadow { + #![allow(clippy::needless_raw_string_hashes)] + use shadow_rs::shadow; + shadow!(build); + + pub use self::build::*; +} use self::cli::Cli; diff --git a/clipcat-notify/Cargo.toml b/clipcat-notify/Cargo.toml index 0d8d5200..25a16087 100644 --- a/clipcat-notify/Cargo.toml +++ b/clipcat-notify/Cargo.toml @@ -20,11 +20,15 @@ tokio = { version = "1", features = ["rt-multi-thread", "sync"] } clap = { version = "4", features = ["derive", "env"] } clap_complete = "4" mime = "0.3" -snafu = "0.7" +shadow-rs = "0.26" +snafu = "0.8" time = { version = "0.3", features = ["local-offset", "serde"] } clipcat-base = { path = "../crates/base" } clipcat-server = { path = "../crates/server" } +[build-dependencies] +shadow-rs = "0.26" + [lints] workspace = true diff --git a/clipcat-notify/build.rs b/clipcat-notify/build.rs new file mode 100644 index 00000000..e0f6c422 --- /dev/null +++ b/clipcat-notify/build.rs @@ -0,0 +1 @@ +fn main() -> shadow_rs::SdResult<()> { shadow_rs::new() } diff --git a/clipcat-notify/src/main.rs b/clipcat-notify/src/main.rs index d955c7b5..18fe4bca 100644 --- a/clipcat-notify/src/main.rs +++ b/clipcat-notify/src/main.rs @@ -1,4 +1,11 @@ mod error; +mod shadow { + #![allow(clippy::needless_raw_string_hashes)] + use shadow_rs::shadow; + shadow!(build); + + pub use self::build::*; +} use std::{io::Write, sync::Arc}; @@ -12,7 +19,14 @@ use tokio::runtime::Runtime; use self::error::Error; #[derive(Parser)] -#[clap(name = clipcat_base::NOTIFY_PROGRAM_NAME, author, version, about, long_about = None)] +#[command( + name = clipcat_base::NOTIFY_PROGRAM_NAME, + author, + version, + long_version = shadow::CLAP_LONG_VERSION, + about, + long_about = None +)] struct Cli { #[clap(subcommand)] commands: Option, diff --git a/clipcatctl/Cargo.toml b/clipcatctl/Cargo.toml index 145c9813..5916ee14 100644 --- a/clipcatctl/Cargo.toml +++ b/clipcatctl/Cargo.toml @@ -28,13 +28,17 @@ clap_complete = "4" directories = "5" http = "1" mime = "0.3" +shadow-rs = "0.26" simdutf8 = "0.1" -snafu = "0.7" +snafu = "0.8" clipcat-base = { path = "../crates/base" } clipcat-cli = { path = "../crates/cli" } clipcat-client = { path = "../crates/client" } clipcat-external-editor = { path = "../crates/external-editor" } +[build-dependencies] +shadow-rs = "0.26" + [lints] workspace = true diff --git a/clipcatctl/build.rs b/clipcatctl/build.rs new file mode 100644 index 00000000..e0f6c422 --- /dev/null +++ b/clipcatctl/build.rs @@ -0,0 +1 @@ +fn main() -> shadow_rs::SdResult<()> { shadow_rs::new() } diff --git a/clipcatctl/src/cli.rs b/clipcatctl/src/cli.rs index ae8cd4bc..e39a58b9 100644 --- a/clipcatctl/src/cli.rs +++ b/clipcatctl/src/cli.rs @@ -13,12 +13,20 @@ use tokio::{ use crate::{ config::Config, error::{self, Error}, + shadow, }; const PREVIEW_LENGTH: usize = 100; #[derive(Parser)] -#[clap(name = clipcat_base::CTL_PROGRAM_NAME, author, version, about, long_about = None)] +#[command( + name = clipcat_base::CTL_PROGRAM_NAME, + author, + version, + long_version = shadow::CLAP_LONG_VERSION, + about, + long_about = None +)] pub struct Cli { #[clap(subcommand)] commands: Option, @@ -59,12 +67,12 @@ pub enum Commands { #[clap(about = "Insert new clip into clipboard")] Insert { #[clap( - long = "kind", + long = "kinds", short = 'k', default_value = "clipboard", help = "Specify which clipboard to insert (\"clipboard\", \"primary\", \"secondary\")" )] - kind: ClipboardKind, + kinds: Vec, data: String, }, @@ -72,12 +80,12 @@ pub enum Commands { #[clap(aliases = &["cut"], about = "Loads file into clipboard")] Load { #[clap( - long = "kind", + long = "kinds", short = 'k', default_value = "clipboard", help = "Specify which clipboard to insert (\"clipboard\", \"primary\", \"secondary\")" )] - kind: ClipboardKind, + kinds: Vec, #[clap( long = "mime", @@ -231,11 +239,14 @@ impl Cli { _ => {} } - let Config { server_endpoint, log } = self.load_config(); - log.registry(); + let config = self.load_config(); + config.log.registry(); let fut = async move { - let client = Client::new(server_endpoint).await?; + let client = { + let access_token = config.access_token(); + Client::new(config.server_endpoint, access_token).await? + }; let server_version = client .get_version() .await @@ -276,15 +287,20 @@ impl Cli { println!("{data}"); } - Some(Commands::Insert { kind, data }) => { - let _id = client.insert(data.as_bytes(), mime::TEXT_PLAIN_UTF_8, kind).await?; + Some(Commands::Insert { kinds, data }) => { + for kind in kinds { + let _id = + client.insert(data.as_bytes(), mime::TEXT_PLAIN_UTF_8, kind).await?; + } } Some(Commands::Length) => { println!("{len}", len = client.length().await?); } - Some(Commands::Load { kind, file_path, mime }) => { + Some(Commands::Load { kinds, file_path, mime }) => { let (data, mime) = load_file_or_read_stdin(file_path, mime).await?; - let _id = client.insert(&data, mime, kind).await?; + for kind in kinds { + let _id = client.insert(&data, mime.clone(), kind).await?; + } } Some(Commands::Save { file_path, kind }) => { let data = client.get_current_clip(kind).await?.encoded()?; diff --git a/clipcatctl/src/config.rs b/clipcatctl/src/config.rs index be209bf8..7119a137 100644 --- a/clipcatctl/src/config.rs +++ b/clipcatctl/src/config.rs @@ -8,6 +8,10 @@ pub struct Config { #[serde(default = "clipcat_base::config::default_server_endpoint", with = "http_serde::uri")] pub server_endpoint: http::Uri, + pub access_token: Option, + + pub access_token_file_path: Option, + #[serde(default)] pub log: clipcat_cli::config::LogConfig, } @@ -16,6 +20,8 @@ impl Default for Config { fn default() -> Self { Self { server_endpoint: clipcat_base::config::default_server_endpoint(), + access_token: None, + access_token_file_path: None, log: clipcat_cli::config::LogConfig::default(), } } @@ -37,11 +43,22 @@ impl Config { let data = std::fs::read_to_string(&path) .context(OpenConfigSnafu { filename: path.as_ref().to_path_buf() })?; - toml::from_str(&data).context(ParseConfigSnafu { filename: path.as_ref().to_path_buf() }) + let mut config: Self = toml::from_str(&data) + .context(ParseConfigSnafu { filename: path.as_ref().to_path_buf() })?; + + if let Some(ref file_path) = config.access_token_file_path { + if let Ok(token) = std::fs::read_to_string(file_path) { + config.access_token = Some(token.trim_end().to_string()); + } + } + + Ok(config) } #[inline] pub fn load_or_default>(path: P) -> Self { Self::load(path).unwrap_or_default() } + + pub fn access_token(&self) -> Option { self.access_token.clone() } } #[derive(Debug, Snafu)] diff --git a/clipcatctl/src/error.rs b/clipcatctl/src/error.rs index c6410209..d0070531 100644 --- a/clipcatctl/src/error.rs +++ b/clipcatctl/src/error.rs @@ -25,7 +25,7 @@ pub enum Error { Client { source: clipcat_client::Error }, #[snafu(display("Error occurs while interacting with server, error: {error}"))] - OperationError { error: String }, + Operation { error: String }, #[snafu(display("{error}"))] EncodeData { error: clipcat_base::ClipEntryError }, @@ -44,85 +44,85 @@ impl From for Error { impl From for Error { fn from(err: clipcat_client::error::InsertClipError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::GetClipError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::GetCurrentClipError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::GetLengthError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::ClearClipError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::RemoveClipError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::BatchRemoveClipError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::MarkClipError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::UpdateClipError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::ListClipError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::EnableWatcherError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::DisableWatcherError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::ToggleWatcherError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } impl From for Error { fn from(err: clipcat_client::error::GetWatcherStateError) -> Self { - Self::OperationError { error: err.to_string() } + Self::Operation { error: err.to_string() } } } diff --git a/clipcatctl/src/main.rs b/clipcatctl/src/main.rs index 98b39b5b..0de2daad 100644 --- a/clipcatctl/src/main.rs +++ b/clipcatctl/src/main.rs @@ -1,6 +1,13 @@ mod cli; mod config; mod error; +mod shadow { + #![allow(clippy::needless_raw_string_hashes)] + use shadow_rs::shadow; + shadow!(build); + + pub use self::build::*; +} use self::cli::Cli; diff --git a/clipcatd/Cargo.toml b/clipcatd/Cargo.toml index 2e50faf7..835ceaf4 100644 --- a/clipcatd/Cargo.toml +++ b/clipcatd/Cargo.toml @@ -29,13 +29,17 @@ exitcode = "1" libc = "0.2" linicon = "2" mime = "0.3" +shadow-rs = "0.26" simdutf8 = "0.1" -snafu = "0.7" +snafu = "0.8" time = { version = "0.3", features = ["formatting", "macros"] } clipcat-base = { path = "../crates/base" } clipcat-cli = { path = "../crates/cli" } clipcat-server = { path = "../crates/server" } +[build-dependencies] +shadow-rs = "0.26" + [lints] workspace = true diff --git a/clipcatd/build.rs b/clipcatd/build.rs new file mode 100644 index 00000000..e0f6c422 --- /dev/null +++ b/clipcatd/build.rs @@ -0,0 +1 @@ +fn main() -> shadow_rs::SdResult<()> { shadow_rs::new() } diff --git a/clipcatd/src/command.rs b/clipcatd/src/command.rs index 67c93ef6..a3412223 100644 --- a/clipcatd/src/command.rs +++ b/clipcatd/src/command.rs @@ -8,10 +8,18 @@ use crate::{ config::Config, error::{self, Error}, pid_file::PidFile, + shadow, }; #[derive(Parser)] -#[command(name = clipcat_base::DAEMON_PROGRAM_NAME, author, version, about, long_about = None)] +#[command( + name = clipcat_base::DAEMON_PROGRAM_NAME, + author, + version, + long_version = shadow::CLAP_LONG_VERSION, + about, + long_about = None +)] pub struct Cli { #[clap(subcommand)] subcommand: Option, @@ -174,7 +182,6 @@ fn run_clipcatd(config: Config, replace: bool) -> Result<(), Error> { pid_file.create()?; } - let snippets = config.load_snippets(); let config = clipcat_server::Config::from(config); tracing::info!( @@ -186,9 +193,9 @@ fn run_clipcatd(config: Config, replace: bool) -> Result<(), Error> { tracing::info!("Initializing Tokio runtime"); let exit_status = match Runtime::new().context(error::InitializeTokioRuntimeSnafu) { - Ok(runtime) => runtime - .block_on(clipcat_server::serve_with_shutdown(config, &snippets)) - .map_err(Error::from), + Ok(runtime) => { + runtime.block_on(clipcat_server::serve_with_shutdown(config)).map_err(Error::from) + } Err(err) => Err(err), }; diff --git a/clipcatd/src/config.rs b/clipcatd/src/config.rs index e8df1ce9..066bd4c3 100644 --- a/clipcatd/src/config.rs +++ b/clipcatd/src/config.rs @@ -8,7 +8,6 @@ use std::{ use directories::BaseDirs; use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Snafu}; -use time::OffsetDateTime; const DEFAULT_ICON_NAME: &str = "accessories-clipboard"; @@ -22,6 +21,9 @@ pub struct Config { #[serde(default = "Config::default_max_history")] pub max_history: usize, + #[serde(default = "Config::default_synchronize_selection_with_clipboard")] + pub synchronize_selection_with_clipboard: bool, + #[serde(default = "Config::default_history_file_path")] pub history_file_path: PathBuf, @@ -34,6 +36,12 @@ pub struct Config { #[serde(default)] pub grpc: GrpcConfig, + #[serde(default)] + pub dbus: DBusConfig, + + #[serde(default)] + pub metrics: MetricsConfig, + #[serde(default)] pub desktop_notification: DesktopNotificationConfig, @@ -45,9 +53,6 @@ pub struct Config { #[allow(clippy::struct_excessive_bools)] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct WatcherConfig { - #[serde(default)] - pub load_current: bool, - #[serde(default)] pub enable_clipboard: bool, @@ -79,7 +84,6 @@ pub struct WatcherConfig { impl From for clipcat_server::ClipboardWatcherOptions { fn from( WatcherConfig { - load_current, enable_clipboard, enable_primary, enable_secondary, @@ -92,7 +96,6 @@ impl From for clipcat_server::ClipboardWatcherOptions { }: WatcherConfig, ) -> Self { Self { - load_current, enable_clipboard, enable_primary, enable_secondary, @@ -139,6 +142,12 @@ pub struct GrpcConfig { #[serde(default = "clipcat_base::config::default_unix_domain_socket")] pub local_socket: PathBuf, + + #[serde(default = "GrpcConfig::default_access_token")] + pub access_token: Option, + + #[serde(default = "GrpcConfig::default_access_token_file_path")] + pub access_token_file_path: Option, } impl GrpcConfig { @@ -156,73 +165,88 @@ impl GrpcConfig { #[inline] pub const fn default_port() -> u16 { clipcat_base::DEFAULT_GRPC_PORT } + + #[inline] + pub const fn default_access_token() -> Option { None } + + #[inline] + pub const fn default_access_token_file_path() -> Option { None } } -#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -pub struct SnippetConfig { - name: String, +impl Default for GrpcConfig { + fn default() -> Self { + Self { + enable_http: Self::default_enable_http(), + enable_local_socket: Self::default_enable_local_socket(), + host: Self::default_host(), + port: Self::default_port(), + local_socket: clipcat_base::config::default_unix_domain_socket(), + access_token: Self::default_access_token(), + access_token_file_path: Self::default_access_token_file_path(), + } + } +} - file_path: Option, +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct DBusConfig { + #[serde(default = "DBusConfig::default_enable")] + pub enable: bool, - content: Option, + pub identifier: Option, } -impl SnippetConfig { - #[allow(clippy::cognitive_complexity)] - fn load(&self) -> Option { - let Self { name, file_path, content } = self; - tracing::trace!("Load snippet `{name}`"); - let data = match (file_path, content) { - (Some(file_path), Some(_content)) => { - tracing::warn!( - "Loading snippet, both `file_path` and `content` are provided, prefer \ - `file_path`" - ); - std::fs::read(file_path) - .map_err(|err| { - tracing::warn!( - "Failed to load snippet from `{}`, error: {err}", - file_path.display() - ); - }) - .ok() - } - (Some(file_path), None) => std::fs::read(file_path) - .map_err(|err| { - tracing::warn!( - "Failed to load snippet from `{}`, error: {err}", - file_path.display() - ); - }) - .ok(), - (None, Some(content)) => Some(content.as_bytes().to_vec()), - (None, None) => None, - }; +impl DBusConfig { + #[inline] + pub const fn default_enable() -> bool { true } +} - if let Some(data) = data { - if data.is_empty() { - tracing::warn!("Snippet `{name}` is empty, ignored it"); - return None; - } +impl Default for DBusConfig { + fn default() -> Self { Self { enable: Self::default_enable(), identifier: None } } +} - if let Err(err) = simdutf8::basic::from_utf8(&data) { - tracing::warn!("Snippet `{name}` is not valid UTF-8 string, error: {err}"); - return None; - } +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct MetricsConfig { + #[serde(default = "MetricsConfig::default_enable")] + pub enable: bool, - clipcat_base::ClipEntry::new( - &data, - &mime::TEXT_PLAIN_UTF_8, - clipcat_base::ClipboardKind::Clipboard, - Some(OffsetDateTime::UNIX_EPOCH), - ) - .ok() - } else { - None + #[serde(default = "MetricsConfig::default_host")] + pub host: IpAddr, + + #[serde(default = "MetricsConfig::default_port")] + pub port: u16, +} + +impl MetricsConfig { + #[inline] + pub const fn socket_address(&self) -> SocketAddr { SocketAddr::new(self.host, self.port) } + + #[inline] + pub const fn default_enable() -> bool { true } + + #[inline] + pub const fn default_host() -> IpAddr { clipcat_base::DEFAULT_METRICS_HOST } + + #[inline] + pub const fn default_port() -> u16 { clipcat_base::DEFAULT_METRICS_PORT } +} + +impl Default for MetricsConfig { + fn default() -> Self { + Self { + enable: Self::default_enable(), + host: Self::default_host(), + port: Self::default_port(), } } } +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum SnippetConfig { + Text { name: String, content: String }, + File { name: String, path: PathBuf }, + Directory { name: String, path: PathBuf }, +} + impl Default for Config { fn default() -> Self { Self { @@ -230,10 +254,14 @@ impl Default for Config { pid_file: Self::default_pid_file_path(), max_history: Self::default_max_history(), history_file_path: Self::default_history_file_path(), + synchronize_selection_with_clipboard: + Self::default_synchronize_selection_with_clipboard(), log: clipcat_cli::config::LogConfig::default(), watcher: WatcherConfig::default(), grpc: GrpcConfig::default(), desktop_notification: DesktopNotificationConfig::default(), + dbus: DBusConfig::default(), + metrics: MetricsConfig::default(), snippets: Vec::new(), } } @@ -242,7 +270,6 @@ impl Default for Config { impl Default for WatcherConfig { fn default() -> Self { Self { - load_current: true, enable_clipboard: true, enable_primary: true, enable_secondary: Self::default_enable_secondary(), @@ -256,18 +283,6 @@ impl Default for WatcherConfig { } } -impl Default for GrpcConfig { - fn default() -> Self { - Self { - enable_http: true, - enable_local_socket: true, - host: clipcat_base::DEFAULT_GRPC_HOST, - port: clipcat_base::DEFAULT_GRPC_PORT, - local_socket: clipcat_base::config::default_unix_domain_socket(), - } - } -} - impl Config { #[inline] pub fn default_path() -> PathBuf { @@ -291,6 +306,9 @@ impl Config { .collect() } + #[inline] + pub const fn default_synchronize_selection_with_clipboard() -> bool { true } + #[inline] pub const fn default_max_history() -> usize { 50 } @@ -321,10 +339,6 @@ impl Config { Ok(config) } - - pub fn load_snippets(&self) -> Vec { - self.snippets.iter().filter_map(SnippetConfig::load).collect() - } } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -401,22 +415,72 @@ impl From for clipcat_server::config::DesktopNotifica } } +impl From for clipcat_server::config::DBusConfig { + fn from(DBusConfig { enable, identifier }: DBusConfig) -> Self { Self { enable, identifier } } +} + +impl From for clipcat_server::config::MetricsConfig { + fn from(config: MetricsConfig) -> Self { + Self { enable: config.enable, listen_address: config.socket_address() } + } +} + +impl From for clipcat_server::config::SnippetConfig { + fn from(config: SnippetConfig) -> Self { + match config { + SnippetConfig::Text { name, content } => Self::Inline { name, content }, + SnippetConfig::File { name, path } => Self::File { name, path }, + SnippetConfig::Directory { name, path } => Self::Directory { name, path }, + } + } +} + impl From for clipcat_server::Config { fn from( - Config { grpc, max_history, history_file_path, watcher, desktop_notification, .. }: Config, + Config { + grpc, + max_history, + synchronize_selection_with_clipboard, + history_file_path, + watcher, + desktop_notification, + dbus, + metrics, + snippets, + .. + }: Config, ) -> Self { let grpc_listen_address = grpc.enable_http.then_some(grpc.socket_address()); let grpc_local_socket = grpc.enable_local_socket.then_some(grpc.local_socket); + let grpc_access_token = if let Some(file_path) = grpc.access_token_file_path { + if let Ok(token) = std::fs::read_to_string(file_path) { + Some(token.trim_end().to_string()) + } else { + grpc.access_token + } + } else { + grpc.access_token + }; let watcher = clipcat_server::ClipboardWatcherOptions::from(watcher); let desktop_notification = clipcat_server::config::DesktopNotificationConfig::from(desktop_notification); + let dbus = clipcat_server::config::DBusConfig::from(dbus); + let metrics = clipcat_server::config::MetricsConfig::from(metrics); + let snippets = + snippets.into_iter().map(clipcat_server::config::SnippetConfig::from).collect(); + Self { grpc_listen_address, grpc_local_socket, + grpc_access_token, max_history, + synchronize_selection_with_clipboard, history_file_path, watcher, + dbus, desktop_notification, + metrics, + snippets, } } } diff --git a/clipcatd/src/main.rs b/clipcatd/src/main.rs index 703c31c1..90167414 100644 --- a/clipcatd/src/main.rs +++ b/clipcatd/src/main.rs @@ -2,6 +2,13 @@ mod command; mod config; mod error; mod pid_file; +mod shadow { + #![allow(clippy::needless_raw_string_hashes)] + use shadow_rs::shadow; + shadow!(build); + + pub use self::build::*; +} use self::{command::Cli, error::CommandError}; diff --git a/crates/base/Cargo.toml b/crates/base/Cargo.toml index 6d996421..35864443 100644 --- a/crates/base/Cargo.toml +++ b/crates/base/Cargo.toml @@ -14,6 +14,8 @@ keywords.workspace = true [dependencies] serde = { version = "1", features = ["derive"] } +tokio = { version = "1" } + http = "1" bytes = "1" @@ -25,7 +27,7 @@ mime = "0.3" regex = "1" semver = "1" sha2 = "0.10" -snafu = "0.7" +snafu = "0.8" time = { version = "0.3", features = ["formatting", "local-offset", "macros"] } [lints] diff --git a/crates/base/src/lib.rs b/crates/base/src/lib.rs index 31a5f3b4..35e7adbb 100644 --- a/crates/base/src/lib.rs +++ b/crates/base/src/lib.rs @@ -26,6 +26,12 @@ pub use self::{ pub const PROJECT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const DBUS_SERVICE_NAME: &str = "org.clipcat.clipcat"; +pub const DBUS_OBJECT_PATH_PREFIX: &str = "/org/clipcat/clipcat"; +pub const DBUS_SYSTEM_OBJECT_PATH: &str = "/org/clipcat/clipcat/system"; +pub const DBUS_WATCHER_OBJECT_PATH: &str = "/org/clipcat/clipcat/watcher"; +pub const DBUS_MANAGER_OBJECT_PATH: &str = "/org/clipcat/clipcat/manager"; + lazy_static! { pub static ref PROJECT_SEMVER: semver::Version = semver::Version::parse(PROJECT_VERSION) .unwrap_or(semver::Version { @@ -59,6 +65,9 @@ pub const DEFAULT_GRPC_HOST: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); pub const DEFAULT_WEBUI_PORT: u16 = 45046; pub const DEFAULT_WEBUI_HOST: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); +pub const DEFAULT_METRICS_PORT: u16 = 45047; +pub const DEFAULT_METRICS_HOST: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); + pub const DEFAULT_MENU_PROMPT: &str = "Clipcat"; lazy_static::lazy_static! { diff --git a/crates/base/src/utils/fs.rs b/crates/base/src/utils/fs.rs new file mode 100644 index 00000000..7e0051dc --- /dev/null +++ b/crates/base/src/utils/fs.rs @@ -0,0 +1,31 @@ +use std::path::{Path, PathBuf}; + +pub fn read_dir_recursively

(dir_path: P) -> Vec +where + P: AsRef, +{ + let dir_path = dir_path.as_ref().to_path_buf(); + let mut files = Vec::new(); + let mut stack = vec![dir_path.clone()]; + while let Some(current_entry) = stack.pop() { + if current_entry.is_file() { + files.push(current_entry); + } else if current_entry.is_dir() { + if let Ok(dir_entry) = current_entry.read_dir() { + for entry in dir_entry.flatten() { + stack.push([&dir_path, &entry.path()].iter().collect()); + } + } + } + } + files.sort_unstable(); + files +} + +pub async fn read_dir_recursively_async

(dir_path: P) -> Vec +where + P: AsRef + Send, +{ + let dir_path = dir_path.as_ref().to_path_buf(); + tokio::task::spawn_blocking(move || read_dir_recursively(dir_path)).await.unwrap_or_default() +} diff --git a/crates/base/src/utils/mod.rs b/crates/base/src/utils/mod.rs index a809bdc6..110132c3 100644 --- a/crates/base/src/utils/mod.rs +++ b/crates/base/src/utils/mod.rs @@ -1,3 +1,4 @@ +pub mod fs; mod retry_interval; pub use self::retry_interval::RetryInterval; diff --git a/crates/cli/src/config/log.rs b/crates/cli/src/config/log.rs index 48ae845d..f47902b0 100644 --- a/crates/cli/src/config/log.rs +++ b/crates/cli/src/config/log.rs @@ -83,9 +83,8 @@ enum LogDriver { } impl LogDriver { - /// # Panics #[allow(clippy::type_repetition_in_bounds)] - fn layer(self) -> Box + Send + Sync + 'static> + fn layer(self) -> Option + Send + Sync + 'static>> where S: tracing::Subscriber, for<'a> S: LookupSpan<'a>, @@ -96,20 +95,14 @@ impl LogDriver { // Configure the writer based on the desired log target: match self { - Self::Stdout => Box::new(fmt.with_writer(std::io::stdout)), - Self::Stderr => Box::new(fmt.with_writer(std::io::stderr)), + Self::Stdout => Some(Box::new(fmt.with_writer(std::io::stdout))), + Self::Stderr => Some(Box::new(fmt.with_writer(std::io::stderr))), Self::File(path) => { - let file = OpenOptions::new() - .create(true) - .write(true) - .append(true) - .open(path) - .expect("failed to create log file"); - Box::new(fmt.with_writer(file)) - } - Self::Journald => { - Box::new(tracing_journald::layer().expect("failed to open journald socket")) + let file = + OpenOptions::new().create(true).write(true).append(true).open(path).ok()?; + Some(Box::new(fmt.with_writer(file))) } + Self::Journald => Some(Box::new(tracing_journald::layer().ok()?)), } } } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 757b30c8..da7416ad 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -25,7 +25,7 @@ prost-types = "0.12" mime = "0.3" semver = "1" -snafu = "0.7" +snafu = "0.8" clipcat-base = { path = "../base" } clipcat-proto = { path = "../proto" } diff --git a/crates/client/src/interceptor.rs b/crates/client/src/interceptor.rs new file mode 100644 index 00000000..9ff3c2c5 --- /dev/null +++ b/crates/client/src/interceptor.rs @@ -0,0 +1,32 @@ +use std::{fmt, sync::Arc}; + +use tonic::{metadata::AsciiMetadataValue, Request, Status}; + +#[derive(Clone, Debug, Default)] +pub struct Interceptor { + authorization_metadata_value: Arc>, +} + +impl Interceptor { + pub fn new(access_token: Option) -> Self + where + S: fmt::Display + Send, + { + let authorization_metadata_value = match access_token { + Some(token) if token.to_string().is_empty() => None, + Some(token) => AsciiMetadataValue::try_from(format!("Bearer {token}")).ok(), + None => None, + }; + + Self { authorization_metadata_value: Arc::new(authorization_metadata_value) } + } +} + +impl tonic::service::Interceptor for Interceptor { + fn call(&mut self, mut req: Request<()>) -> Result, Status> { + if let Some(ref token) = self.authorization_metadata_value.as_ref() { + drop(req.metadata_mut().insert("authorization", token.clone())); + } + Ok(req) + } +} diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 86a5c00c..34ed2cee 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1,13 +1,15 @@ pub mod error; +mod interceptor; mod manager; mod system; mod watcher; -use std::path::Path; +use std::{fmt, path::Path}; use snafu::ResultExt; use tokio::net::UnixStream; +use self::interceptor::Interceptor; pub use self::{ error::{Error, Result}, manager::Manager, @@ -18,17 +20,21 @@ pub use self::{ #[derive(Clone, Debug)] pub struct Client { channel: tonic::transport::Channel, + interceptor: Interceptor, } impl Client { /// # Errors - pub async fn new(grpc_endpoint: http::Uri) -> Result { - tracing::info!("Connect to server via endpoint `{}`", grpc_endpoint); + pub async fn new(grpc_endpoint: http::Uri, access_token: Option) -> Result + where + A: fmt::Display + Send, + { + tracing::info!("Connect to server via endpoint `{grpc_endpoint}`"); let scheme = grpc_endpoint.scheme(); if scheme == Some(&http::uri::Scheme::HTTP) { - Self::connect_http(grpc_endpoint).await + Self::connect_http(grpc_endpoint, access_token).await } else { - Self::connect_local_socket(grpc_endpoint.path()).await + Self::connect_local_socket(grpc_endpoint.path(), access_token).await } } @@ -37,7 +43,11 @@ impl Client { /// This function will an error if the server is not connected. // SAFETY: it will never panic because `grpc_endpoint` is a valid URL #[allow(clippy::missing_panics_doc)] - pub async fn connect_http(grpc_endpoint: http::Uri) -> Result { + pub async fn connect_http(grpc_endpoint: http::Uri, access_token: Option) -> Result + where + A: fmt::Display + Send, + { + let interceptor = Interceptor::new(access_token); let channel = tonic::transport::Endpoint::from_shared(grpc_endpoint.to_string()) .expect("`grpc_endpoint` is a valid URL; qed") .connect() @@ -45,7 +55,7 @@ impl Client { .with_context(|_| error::ConnectToClipcatServerViaHttpSnafu { endpoint: grpc_endpoint.clone(), })?; - Ok(Self { channel }) + Ok(Self { channel, interceptor }) } /// # Errors @@ -53,10 +63,12 @@ impl Client { /// This function will an error if the server is not connected. // SAFETY: it will never panic because `dummy_uri` is a valid URL #[allow(clippy::missing_panics_doc)] - pub async fn connect_local_socket

(socket_path: P) -> Result + pub async fn connect_local_socket(socket_path: P, access_token: Option) -> Result where P: AsRef + Send, + A: fmt::Display + Send, { + let interceptor = Interceptor::new(access_token); let socket_path = socket_path.as_ref().to_path_buf(); // We will ignore this uri because uds do not use it let dummy_uri = "http://[::]:50051"; @@ -74,6 +86,6 @@ impl Client { .with_context(|_| error::ConnectToClipcatServerViaLocalSocketSnafu { socket: socket_path.clone(), })?; - Ok(Self { channel }) + Ok(Self { channel, interceptor }) } } diff --git a/crates/client/src/manager.rs b/crates/client/src/manager.rs index 8196b770..03a60b21 100644 --- a/crates/client/src/manager.rs +++ b/crates/client/src/manager.rs @@ -60,7 +60,7 @@ pub trait Manager { #[async_trait] impl Manager for Client { async fn get(&self, id: u64) -> Result { - proto::ManagerClient::new(self.channel.clone()) + proto::ManagerClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) .get(Request::new(proto::GetRequest { id })) .await .map_err(|source| GetClipError::Status { source, id })? @@ -73,7 +73,7 @@ impl Manager for Client { &self, kind: ClipboardKind, ) -> Result { - proto::ManagerClient::new(self.channel.clone()) + proto::ManagerClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) .get_current_clip(Request::new(proto::GetCurrentClipRequest { kind: kind.into() })) .await .map_err(|source| GetCurrentClipError::Status { source, kind })? @@ -88,24 +88,26 @@ impl Manager for Client { data: &[u8], mime: mime::Mime, ) -> Result<(bool, u64), UpdateClipError> { - let proto::UpdateResponse { ok, new_id } = proto::ManagerClient::new(self.channel.clone()) - .update(Request::new(proto::UpdateRequest { - id, - data: data.to_owned(), - mime: mime.essence_str().to_owned(), - })) - .await - .map_err(|source| UpdateClipError::Status { source })? - .into_inner(); + let proto::UpdateResponse { ok, new_id } = + proto::ManagerClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) + .update(Request::new(proto::UpdateRequest { + id, + data: data.to_owned(), + mime: mime.essence_str().to_owned(), + })) + .await + .map_err(|source| UpdateClipError::Status { source })? + .into_inner(); Ok((ok, new_id)) } async fn mark(&self, id: u64, kind: ClipboardKind) -> Result { - let proto::MarkResponse { ok } = proto::ManagerClient::new(self.channel.clone()) - .mark(Request::new(proto::MarkRequest { id, kind: kind.into() })) - .await - .map_err(|source| MarkClipError::Status { source, id, kind })? - .into_inner(); + let proto::MarkResponse { ok } = + proto::ManagerClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) + .mark(Request::new(proto::MarkRequest { id, kind: kind.into() })) + .await + .map_err(|source| MarkClipError::Status { source, id, kind })? + .into_inner(); Ok(ok) } @@ -115,63 +117,68 @@ impl Manager for Client { mime: mime::Mime, clipboard_kind: ClipboardKind, ) -> Result { - let proto::InsertResponse { id } = proto::ManagerClient::new(self.channel.clone()) - .insert(Request::new(proto::InsertRequest { - kind: clipboard_kind.into(), - data: data.to_owned(), - mime: mime.essence_str().to_owned(), - })) - .await - .map_err(|source| InsertClipError::Status { source })? - .into_inner(); + let proto::InsertResponse { id } = + proto::ManagerClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) + .insert(Request::new(proto::InsertRequest { + kind: clipboard_kind.into(), + data: data.to_owned(), + mime: mime.essence_str().to_owned(), + })) + .await + .map_err(|source| InsertClipError::Status { source })? + .into_inner(); Ok(id) } async fn length(&self) -> Result { - let proto::LengthResponse { length } = proto::ManagerClient::new(self.channel.clone()) - .length(Request::new(())) - .await - .map_err(|source| GetLengthError::Status { source })? - .into_inner(); + let proto::LengthResponse { length } = + proto::ManagerClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) + .length(Request::new(())) + .await + .map_err(|source| GetLengthError::Status { source })? + .into_inner(); Ok(usize::try_from(length).unwrap_or(0)) } async fn list(&self, preview_length: usize) -> Result, ListClipError> { - let mut list: Vec<_> = proto::ManagerClient::new(self.channel.clone()) - .list(Request::new(proto::ListRequest { - preview_length: u64::try_from(preview_length).unwrap_or(30), - })) - .await - .map_err(|source| ListClipError::Status { source })? - .into_inner() - .metadata - .into_iter() - .map(ClipEntryMetadata::from) - .collect(); + let mut list: Vec<_> = + proto::ManagerClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) + .list(Request::new(proto::ListRequest { + preview_length: u64::try_from(preview_length).unwrap_or(30), + })) + .await + .map_err(|source| ListClipError::Status { source })? + .into_inner() + .metadata + .into_iter() + .map(ClipEntryMetadata::from) + .collect(); list.sort_unstable(); Ok(list) } async fn remove(&self, id: u64) -> Result { - let proto::RemoveResponse { ok } = proto::ManagerClient::new(self.channel.clone()) - .remove(Request::new(proto::RemoveRequest { id })) - .await - .map_err(|source| RemoveClipError::Status { source })? - .into_inner(); + let proto::RemoveResponse { ok } = + proto::ManagerClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) + .remove(Request::new(proto::RemoveRequest { id })) + .await + .map_err(|source| RemoveClipError::Status { source })? + .into_inner(); Ok(ok) } async fn batch_remove(&self, ids: &[u64]) -> Result, BatchRemoveClipError> { - let proto::BatchRemoveResponse { ids } = proto::ManagerClient::new(self.channel.clone()) - .batch_remove(Request::new(proto::BatchRemoveRequest { ids: Vec::from(ids) })) - .await - .map_err(|source| BatchRemoveClipError::Status { source })? - .into_inner(); + let proto::BatchRemoveResponse { ids } = + proto::ManagerClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) + .batch_remove(Request::new(proto::BatchRemoveRequest { ids: Vec::from(ids) })) + .await + .map_err(|source| BatchRemoveClipError::Status { source })? + .into_inner(); Ok(ids) } async fn clear(&self) -> Result<(), ClearClipError> { - proto::ManagerClient::new(self.channel.clone()) + proto::ManagerClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) .clear(Request::new(())) .await .map(|_| ()) diff --git a/crates/client/src/system.rs b/crates/client/src/system.rs index cd41f860..ea6f681a 100644 --- a/crates/client/src/system.rs +++ b/crates/client/src/system.rs @@ -13,7 +13,7 @@ pub trait System { impl System for Client { async fn get_version(&self) -> Result { let proto::GetSystemVersionResponse { major, minor, patch } = - proto::SystemClient::new(self.channel.clone()) + proto::SystemClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) .get_version(Request::new(())) .await .map_err(|source| GetSystemVersionError::Status { source })? diff --git a/crates/client/src/watcher.rs b/crates/client/src/watcher.rs index 22ca5eea..aa0689c1 100644 --- a/crates/client/src/watcher.rs +++ b/crates/client/src/watcher.rs @@ -22,38 +22,42 @@ pub trait Watcher { #[async_trait] impl Watcher for Client { async fn enable_watcher(&self) -> Result { - let proto::WatcherStateReply { state } = proto::WatcherClient::new(self.channel.clone()) - .enable_watcher(Request::new(())) - .await - .map_err(|source| EnableWatcherError::Status { source })? - .into_inner(); + let proto::WatcherStateReply { state } = + proto::WatcherClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) + .enable_watcher(Request::new(())) + .await + .map_err(|source| EnableWatcherError::Status { source })? + .into_inner(); Ok(state.into()) } async fn disable_watcher(&self) -> Result { - let proto::WatcherStateReply { state } = proto::WatcherClient::new(self.channel.clone()) - .disable_watcher(Request::new(())) - .await - .map_err(|source| DisableWatcherError::Status { source })? - .into_inner(); + let proto::WatcherStateReply { state } = + proto::WatcherClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) + .disable_watcher(Request::new(())) + .await + .map_err(|source| DisableWatcherError::Status { source })? + .into_inner(); Ok(state.into()) } async fn toggle_watcher(&self) -> Result { - let proto::WatcherStateReply { state } = proto::WatcherClient::new(self.channel.clone()) - .toggle_watcher(Request::new(())) - .await - .map_err(|source| ToggleWatcherError::Status { source })? - .into_inner(); + let proto::WatcherStateReply { state } = + proto::WatcherClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) + .toggle_watcher(Request::new(())) + .await + .map_err(|source| ToggleWatcherError::Status { source })? + .into_inner(); Ok(state.into()) } async fn get_watcher_state(&self) -> Result { - let proto::WatcherStateReply { state } = proto::WatcherClient::new(self.channel.clone()) - .get_watcher_state(Request::new(())) - .await - .map_err(|source| GetWatcherStateError::Status { source })? - .into_inner(); + let proto::WatcherStateReply { state } = + proto::WatcherClient::with_interceptor(self.channel.clone(), self.interceptor.clone()) + .get_watcher_state(Request::new(())) + .await + .map_err(|source| GetWatcherStateError::Status { source })? + .into_inner(); Ok(state.into()) } } diff --git a/crates/clipboard/Cargo.toml b/crates/clipboard/Cargo.toml index fe0dd617..7a0ebb86 100644 --- a/crates/clipboard/Cargo.toml +++ b/crates/clipboard/Cargo.toml @@ -25,7 +25,7 @@ bytes = "1" mime = "0.3" mio = { version = "0.8", features = ["os-ext"] } parking_lot = "0.12" -snafu = "0.7" +snafu = "0.8" clipcat-base = { path = "../base/" } @@ -43,7 +43,7 @@ tokio = { version = "1", features = [ "sync", ] } -sigfinn = "0.1" +sigfinn = "0.2" [[example]] name = "load" diff --git a/crates/clipboard/examples/store.rs b/crates/clipboard/examples/store.rs index 0fa0663b..ad72a2ba 100644 --- a/crates/clipboard/examples/store.rs +++ b/crates/clipboard/examples/store.rs @@ -50,7 +50,7 @@ async fn main() -> Result<(), Box> { term.store(true, Ordering::Relaxed); if let Err(err) = join_handle.await.expect("task is joinable") { - ExitStatus::Failure(err) + ExitStatus::FatalError(err) } else { ExitStatus::Success } diff --git a/crates/clipboard/src/listener/wayland/mod.rs b/crates/clipboard/src/listener/wayland/mod.rs index 4e1cee70..4d66c63f 100644 --- a/crates/clipboard/src/listener/wayland/mod.rs +++ b/crates/clipboard/src/listener/wayland/mod.rs @@ -125,8 +125,8 @@ fn build_thread( thread::sleep(POLLING_INTERVAL); } - notifier.close(); + drop(notifier); Ok(()) }) - .expect("build thread for listening X11 clipboard") + .expect("build thread for listening Wayland clipboard") } diff --git a/crates/clipboard/src/listener/x11/mod.rs b/crates/clipboard/src/listener/x11/mod.rs index 600558d3..dc2110e8 100644 --- a/crates/clipboard/src/listener/x11/mod.rs +++ b/crates/clipboard/src/listener/x11/mod.rs @@ -149,7 +149,7 @@ fn build_thread( retry_interval.clone(), &is_running, ) { - notifier.close(); + drop(notifier); return Err(err); } for observer in &event_observers { @@ -165,7 +165,7 @@ fn build_thread( } } - notifier.close(); + drop(notifier); Ok(()) }) .expect("build thread for listening X11 clipboard") diff --git a/crates/clipboard/src/local.rs b/crates/clipboard/src/local.rs index c9d96d94..cec7ca02 100644 --- a/crates/clipboard/src/local.rs +++ b/crates/clipboard/src/local.rs @@ -36,10 +36,6 @@ impl Clipboard { } } -impl Drop for Clipboard { - fn drop(&mut self) { self.publisher.close(); } -} - impl ClipboardSubscribe for Clipboard { type Subscriber = Subscriber; diff --git a/crates/clipboard/src/pubsub.rs b/crates/clipboard/src/pubsub.rs index 0e3dc433..6a3fe694 100644 --- a/crates/clipboard/src/pubsub.rs +++ b/crates/clipboard/src/pubsub.rs @@ -28,18 +28,16 @@ impl Publisher { *lock.lock() = (State::Running, Some(mime)); let _unused = condvar.notify_all(); } +} - pub fn close(&self) { +impl Drop for Publisher { + fn drop(&mut self) { let (lock, condvar) = &*self.0; *lock.lock() = (State::Stopped, None); let _unused = condvar.notify_all(); } } -impl Drop for Publisher { - fn drop(&mut self) { self.close(); } -} - #[derive(Clone, Debug)] pub struct Subscriber { inner: Arc<(StateData, Condvar)>, diff --git a/crates/clipboard/tests/default.rs b/crates/clipboard/tests/default.rs index 1d0219d3..ade26acb 100644 --- a/crates/clipboard/tests/default.rs +++ b/crates/clipboard/tests/default.rs @@ -7,16 +7,16 @@ mod common; use self::common::ClipboardTester; #[derive(Debug)] -pub struct DefaultClipboardTester { +pub struct Tester { kind: ClipboardKind, } -impl DefaultClipboardTester { +impl Tester { #[must_use] pub const fn new(kind: ClipboardKind) -> Self { Self { kind } } } -impl ClipboardTester for DefaultClipboardTester { +impl ClipboardTester for Tester { type Clipboard = Clipboard; fn new_clipboard(&self) -> Result { @@ -27,7 +27,7 @@ impl ClipboardTester for DefaultClipboardTester { #[test] fn test_x11_clipboard() -> Result<(), Error> { - match DefaultClipboardTester::new(ClipboardKind::Clipboard).run() { + match Tester::new(ClipboardKind::Clipboard).run() { Err(Error::X11Listener { error: X11ListenerError::Connect { .. } }) => { eprintln!("Could not connect to X11 server, skip the further test cases"); Ok(()) @@ -38,7 +38,7 @@ fn test_x11_clipboard() -> Result<(), Error> { #[test] fn test_x11_primary() -> Result<(), Error> { - match DefaultClipboardTester::new(ClipboardKind::Primary).run() { + match Tester::new(ClipboardKind::Primary).run() { Err(Error::X11Listener { error: X11ListenerError::Connect { .. } }) => { eprintln!("Could not connect to X11 server, skip the further test cases"); Ok(()) diff --git a/crates/clipboard/tests/mock.rs b/crates/clipboard/tests/local.rs similarity index 61% rename from crates/clipboard/tests/mock.rs rename to crates/clipboard/tests/local.rs index 273e3f78..b2e0c6fd 100644 --- a/crates/clipboard/tests/mock.rs +++ b/crates/clipboard/tests/local.rs @@ -5,22 +5,22 @@ mod common; use self::common::ClipboardTester; #[derive(Debug)] -pub struct LocalClipboardTester; +pub struct Tester; -impl Default for LocalClipboardTester { +impl Default for Tester { fn default() -> Self { Self::new() } } -impl LocalClipboardTester { +impl Tester { #[must_use] pub const fn new() -> Self { Self } } -impl ClipboardTester for LocalClipboardTester { +impl ClipboardTester for Tester { type Clipboard = LocalClipboard; fn new_clipboard(&self) -> Result { Ok(LocalClipboard::new()) } } #[test] -fn test_local() -> Result<(), Error> { LocalClipboardTester::new().run() } +fn test_local() -> Result<(), Error> { Tester::new().run() } diff --git a/crates/dbus-variant/Cargo.toml b/crates/dbus-variant/Cargo.toml new file mode 100644 index 00000000..ed301d98 --- /dev/null +++ b/crates/dbus-variant/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "clipcat-dbus-variant" +description = "Clipcat D-Bus variant" +version.workspace = true +authors.workspace = true +homepage.workspace = true +readme.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +categories.workspace = true +keywords.workspace = true + +[dependencies] +serde = { version = "1", features = ["derive"] } +zvariant = "3" + +mime = "0.3" +time = { version = "0.3", features = ["formatting", "macros"] } + +clipcat-base = { path = "../base" } + +[lints] +workspace = true diff --git a/crates/dbus-variant/src/entry.rs b/crates/dbus-variant/src/entry.rs new file mode 100644 index 00000000..1e0affbe --- /dev/null +++ b/crates/dbus-variant/src/entry.rs @@ -0,0 +1,70 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use zvariant::Type; + +use crate::ClipboardKind; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Type)] +pub struct Entry { + id: u64, + + data: Vec, + + clipboard_kind: ClipboardKind, + + mime: String, + + timestamp: i64, +} + +impl From for Entry { + fn from(entry: clipcat_base::ClipEntry) -> Self { + let mime = entry.mime().essence_str().to_owned(); + let data = entry.encoded().unwrap_or_default(); + let id = entry.id(); + let kind = entry.kind(); + let timestamp = entry.timestamp().unix_timestamp(); + + Self { id, data, clipboard_kind: kind.into(), mime, timestamp } + } +} + +impl From for clipcat_base::ClipEntry { + fn from(Entry { id: _, data, clipboard_kind, mime, timestamp }: Entry) -> Self { + let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).ok(); + let kind = clipcat_base::ClipboardKind::from(clipboard_kind); + let mime = mime::Mime::from_str(&mime).unwrap_or(mime::APPLICATION_OCTET_STREAM); + Self::new(&data, &mime, kind, timestamp).unwrap_or_default() + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Type)] +pub struct EntryMetadata { + id: u64, + mime: String, + kind: ClipboardKind, + timestamp: i64, + preview: String, +} + +impl From for EntryMetadata { + fn from(metadata: clipcat_base::ClipEntryMetadata) -> Self { + let clipcat_base::ClipEntryMetadata { id, kind: clipboard_kind, timestamp, mime, preview } = + metadata; + let mime = mime.essence_str().to_owned(); + let timestamp = timestamp.unix_timestamp(); + Self { id, preview, kind: clipboard_kind.into(), mime, timestamp } + } +} + +impl From for clipcat_base::ClipEntryMetadata { + fn from(EntryMetadata { id, mime, kind, timestamp, preview }: EntryMetadata) -> Self { + let timestamp = OffsetDateTime::from_unix_timestamp(timestamp) + .unwrap_or_else(|_| OffsetDateTime::now_utc()); + let clipboard_kind = clipcat_base::ClipboardKind::from(kind); + let mime = mime::Mime::from_str(&mime).unwrap_or(mime::APPLICATION_OCTET_STREAM); + Self { id, kind: clipboard_kind, timestamp, mime, preview } + } +} diff --git a/crates/dbus-variant/src/kind.rs b/crates/dbus-variant/src/kind.rs new file mode 100644 index 00000000..dded7f42 --- /dev/null +++ b/crates/dbus-variant/src/kind.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; +use zvariant::Type; + +#[derive( + Clone, Copy, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Type, +)] +pub enum Kind { + #[default] + Clipboard, + Primary, + Secondary, +} + +impl From for clipcat_base::ClipboardKind { + fn from(kind: Kind) -> Self { + match kind { + Kind::Clipboard => Self::Clipboard, + Kind::Primary => Self::Primary, + Kind::Secondary => Self::Secondary, + } + } +} + +impl From for Kind { + fn from(kind: clipcat_base::ClipboardKind) -> Self { + match kind { + clipcat_base::ClipboardKind::Clipboard => Self::Clipboard, + clipcat_base::ClipboardKind::Primary => Self::Primary, + clipcat_base::ClipboardKind::Secondary => Self::Secondary, + } + } +} diff --git a/crates/dbus-variant/src/lib.rs b/crates/dbus-variant/src/lib.rs new file mode 100644 index 00000000..96d58a4f --- /dev/null +++ b/crates/dbus-variant/src/lib.rs @@ -0,0 +1,9 @@ +mod entry; +mod kind; +mod watcher_state; + +pub use self::{ + entry::{Entry as ClipEntry, EntryMetadata as ClipEntryMetadata}, + kind::Kind as ClipboardKind, + watcher_state::WatcherState, +}; diff --git a/crates/dbus-variant/src/watcher_state.rs b/crates/dbus-variant/src/watcher_state.rs new file mode 100644 index 00000000..0edc10d0 --- /dev/null +++ b/crates/dbus-variant/src/watcher_state.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; +use zvariant::Type; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, Type)] +pub enum WatcherState { + Enabled = 0, + Disabled = 1, +} + +impl From for WatcherState { + fn from(state: i32) -> Self { + match state { + 0 => Self::Enabled, + _ => Self::Disabled, + } + } +} + +impl From for i32 { + fn from(state: WatcherState) -> Self { state as Self } +} + +impl From for clipcat_base::ClipboardWatcherState { + fn from(state: WatcherState) -> Self { + match state { + WatcherState::Enabled => Self::Enabled, + WatcherState::Disabled => Self::Disabled, + } + } +} + +impl From for WatcherState { + fn from(val: clipcat_base::ClipboardWatcherState) -> Self { + match val { + clipcat_base::ClipboardWatcherState::Enabled => Self::Enabled, + clipcat_base::ClipboardWatcherState::Disabled => Self::Disabled, + } + } +} diff --git a/crates/external-editor/Cargo.toml b/crates/external-editor/Cargo.toml index b67fa3ae..8766ea88 100644 --- a/crates/external-editor/Cargo.toml +++ b/crates/external-editor/Cargo.toml @@ -14,7 +14,7 @@ keywords.workspace = true [dependencies] tokio = { version = "1", features = ["fs", "process", "rt-multi-thread"] } -snafu = "0.7" +snafu = "0.8" clipcat-base = { path = "../base" } diff --git a/crates/metrics/Cargo.toml b/crates/metrics/Cargo.toml new file mode 100644 index 00000000..420f29a7 --- /dev/null +++ b/crates/metrics/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "clipcat-metrics" +description = "Clipcat metrics utilities" +version.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true +edition.workspace = true +categories.workspace = true +keywords.workspace = true + +[dependencies] +async-trait = "0.1" + +axum = { version = "0.6", features = ["headers"] } +tower = { version = "0.4", features = ["timeout"] } +tower-http = { version = "0.5", features = ["trace"] } + +bytes = "1" +lazy_static = "1" +mime = "0.3" +prometheus = "0.13" +snafu = "0.8" + +[lints] +workspace = true diff --git a/crates/metrics/src/error.rs b/crates/metrics/src/error.rs new file mode 100644 index 00000000..840d326c --- /dev/null +++ b/crates/metrics/src/error.rs @@ -0,0 +1,13 @@ +use snafu::{Backtrace, Snafu}; + +pub type Result = std::result::Result; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub))] +pub enum Error { + #[snafu(display("Could not setup metrics, error: {source}",))] + SetupMetrics { source: prometheus::Error, backtrace: Backtrace }, + + #[snafu(display("Error occurs while serving metrics server, error: {message}",))] + ServeMetricsServer { message: String }, +} diff --git a/crates/metrics/src/lib.rs b/crates/metrics/src/lib.rs new file mode 100644 index 00000000..6d64e090 --- /dev/null +++ b/crates/metrics/src/lib.rs @@ -0,0 +1,5 @@ +pub mod error; +mod server; +mod traits; + +pub use self::{error::Error, server::start_metrics_server, traits::Metrics}; diff --git a/crates/metrics/src/server.rs b/crates/metrics/src/server.rs new file mode 100644 index 00000000..fa766824 --- /dev/null +++ b/crates/metrics/src/server.rs @@ -0,0 +1,94 @@ +use std::{future::Future, net::SocketAddr, str::FromStr}; + +use axum::{ + body::Body, + extract::Extension, + http::{header, HeaderValue}, + response::Response, + routing, Router, +}; +use bytes::{BufMut, BytesMut}; +use lazy_static::lazy_static; +use mime::Mime; +use prometheus::{Encoder, TextEncoder}; + +use crate::{error::Error, traits}; + +lazy_static! { + static ref OPENMETRICS_TEXT: Mime = + Mime::from_str("application/openmetrics-text; version=1.0.0; charset=utf-8") + .expect("is valid mime type; qed"); + static ref ENCODER: TextEncoder = TextEncoder::new(); +} + +async fn metrics(Extension(metrics): Extension) -> Response +where + Metrics: traits::Metrics + 'static, +{ + let mut buffer = BytesMut::new().writer(); + ENCODER + .encode(&metrics.gather(), &mut buffer) + .expect("`Writer` should not encounter io error; qed"); + + let mut res = Response::new(Body::from(buffer.into_inner().freeze())); + drop( + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static(ENCODER.format_type())), + ); + res +} + +fn metrics_index(m: Metrics) -> Router +where + Metrics: traits::Metrics + 'static, +{ + Router::new().route("/metrics", routing::get(metrics::)).layer(Extension(m)) +} + +/// # Errors +/// +/// * if it cannot bind server +pub async fn start_metrics_server( + listen_address: SocketAddr, + metrics: Metrics, + shutdown_signal: ShutdownSignal, +) -> Result<(), Error> +where + Metrics: Clone + traits::Metrics + Send + 'static, + ShutdownSignal: Future + Send + 'static, +{ + let middleware_stack = tower::ServiceBuilder::new(); + + let router = Router::new() + .merge(metrics_index(metrics)) + .layer(middleware_stack) + .into_make_service_with_connect_info::(); + + axum::Server::bind(&listen_address) + .serve(router) + .with_graceful_shutdown(shutdown_signal) + .await + .map_err(|err| Error::ServeMetricsServer { message: err.to_string() }) +} + +#[cfg(test)] +mod tests { + use lazy_static::initialize; + + use crate::server::{ENCODER, OPENMETRICS_TEXT}; + + #[test] + fn test_lazy_static() { + initialize(&OPENMETRICS_TEXT); + initialize(&ENCODER); + } + + #[test] + fn test_openmetrics_text_content_type() { + assert_eq!(OPENMETRICS_TEXT.type_(), "application"); + assert_eq!(OPENMETRICS_TEXT.subtype(), "openmetrics-text"); + assert!(OPENMETRICS_TEXT.suffix().is_none()); + assert_eq!(OPENMETRICS_TEXT.get_param("charset").unwrap(), "utf-8"); + assert_eq!(OPENMETRICS_TEXT.get_param("version").unwrap(), "1.0.0"); + } +} diff --git a/crates/metrics/src/traits.rs b/crates/metrics/src/traits.rs new file mode 100644 index 00000000..b0546aa7 --- /dev/null +++ b/crates/metrics/src/traits.rs @@ -0,0 +1,3 @@ +pub trait Metrics: Clone + Send + Sync { + fn gather(&self) -> Vec; +} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index e9cd2a56..9cbfe4b2 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -20,7 +20,7 @@ serde_json = "1" async-trait = "0.1" futures = "0.3" -sigfinn = "0.1" +sigfinn = "0.2" tokio = { version = "1", features = [ "fs", "macros", @@ -31,15 +31,21 @@ tokio-stream = { version = "0.1", features = ["net"] } tonic = { version = "0.10", features = ["gzip"] } +zbus = { version = "3", default-features = false, features = ["tokio"] } +zvariant = "3" + hex = "0.4" humansize = "2" lazy_static = "1" mime = "0.3" +notify = "6" notify-rust = "4" parking_lot = "0.12" +prometheus = "0.13" regex = "1" semver = "1" -snafu = "0.7" +simdutf8 = "0.1" +snafu = "0.8" time = { version = "0.3", features = [ "formatting", "macros", @@ -47,9 +53,11 @@ time = { version = "0.3", features = [ "serde", ] } -clipcat-base = { path = "../base" } -clipcat-clipboard = { path = "../clipboard" } -clipcat-proto = { path = "../proto" } +clipcat-base = { path = "../base" } +clipcat-clipboard = { path = "../clipboard" } +clipcat-dbus-variant = { path = "../dbus-variant" } +clipcat-metrics = { path = "../metrics" } +clipcat-proto = { path = "../proto" } [lints] workspace = true diff --git a/crates/server/src/backend/mod.rs b/crates/server/src/backend/mod.rs index 80d64f4a..e67c69f8 100644 --- a/crates/server/src/backend/mod.rs +++ b/crates/server/src/backend/mod.rs @@ -25,7 +25,15 @@ pub fn new( where I: IntoIterator, { - Ok(Box::new(DefaultClipboardBackend::new(kinds, clip_filter, event_observers)?)) + DefaultClipboardBackend::new(kinds, clip_filter, event_observers).map_or_else::, + >, _, _>( + |err| { + tracing::warn!("{err}"); + Ok(Box::new(LocalClipboardBackend::new())) + }, + |backend| Ok(Box::new(backend)), + ) } /// # Errors @@ -37,5 +45,13 @@ pub fn new_shared( where I: IntoIterator, { - Ok(Arc::new(DefaultClipboardBackend::new(kinds, clip_filter, event_observers)?)) + DefaultClipboardBackend::new(kinds, clip_filter, event_observers).map_or_else::, + >, _, _>( + |err| { + tracing::warn!("{err}"); + Ok(Arc::new(LocalClipboardBackend::new())) + }, + |backend| Ok(Arc::new(backend)), + ) } diff --git a/crates/server/src/config.rs b/crates/server/src/config.rs index a71eae27..7e7f4827 100644 --- a/crates/server/src/config.rs +++ b/crates/server/src/config.rs @@ -8,13 +8,30 @@ pub struct Config { pub grpc_local_socket: Option, + pub grpc_access_token: Option, + pub max_history: usize, + pub synchronize_selection_with_clipboard: bool, + pub history_file_path: PathBuf, pub watcher: ClipboardWatcherOptions, + pub dbus: DBusConfig, + pub desktop_notification: DesktopNotificationConfig, + + pub metrics: MetricsConfig, + + pub snippets: Vec, +} + +#[derive(Clone, Debug)] +pub struct DBusConfig { + pub enable: bool, + + pub identifier: Option, } #[derive(Clone, Debug)] @@ -27,3 +44,17 @@ pub struct DesktopNotificationConfig { pub long_plaintext_length: usize, } + +#[derive(Clone, Debug)] +pub struct MetricsConfig { + pub enable: bool, + + pub listen_address: SocketAddr, +} + +#[derive(Clone, Debug)] +pub enum SnippetConfig { + Inline { name: String, content: String }, + File { name: String, path: PathBuf }, + Directory { name: String, path: PathBuf }, +} diff --git a/crates/server/src/dbus/manager.rs b/crates/server/src/dbus/manager.rs new file mode 100644 index 00000000..33e8b0f2 --- /dev/null +++ b/crates/server/src/dbus/manager.rs @@ -0,0 +1,148 @@ +#![allow(clippy::ignored_unit_patterns)] + +use std::{str::FromStr, sync::Arc}; + +use clipcat_dbus_variant as dbus_variant; +use tokio::sync::Mutex; +use zbus::dbus_interface; + +use crate::{metrics, notification, ClipboardManager}; + +pub struct ManagerService { + manager: Arc>>, +} + +impl ManagerService { + pub fn new(manager: Arc>>) -> Self { Self { manager } } +} + +#[dbus_interface(name = "org.clipcat.clipcat.Manager")] +impl ManagerService +where + Notification: notification::Notification + 'static, +{ + async fn insert(&self, kind: dbus_variant::ClipboardKind, data: &[u8], mime: &str) -> u64 { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let mime = mime::Mime::from_str(mime).unwrap_or(mime::APPLICATION_OCTET_STREAM); + let mut manager = self.manager.lock().await; + let id = manager.insert( + clipcat_base::ClipEntry::new(data, &mime, kind.into(), None).unwrap_or_default(), + ); + let _unused = manager.mark(id, kind.into()).await; + drop(manager); + id + } + + #[dbus_interface(property)] + async fn set_clipboard_text_contents(&self, data: &str) { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let kind = clipcat_base::ClipboardKind::Clipboard; + let mut manager = self.manager.lock().await; + let id = manager.insert( + clipcat_base::ClipEntry::new(data.as_bytes(), &mime::TEXT_PLAIN_UTF_8, kind, None) + .unwrap_or_default(), + ); + let _unused = manager.mark(id, kind).await; + drop(manager); + } + + #[dbus_interface(property)] + async fn clipboard_text_contents(&self) -> String { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let manager = self.manager.lock().await; + manager + .get_current_clip(clipcat_base::ClipboardKind::Clipboard) + .map(clipcat_base::ClipEntry::as_utf8_string) + .unwrap_or_default() + } + + async fn remove(&self, id: u64) -> bool { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let mut manager = self.manager.lock().await; + manager.remove(id) + } + + async fn batch_remove(&self, ids: Vec) -> Vec { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let mut manager = self.manager.lock().await; + ids.into_iter().filter(|&id| manager.remove(id)).collect() + } + + async fn clear(&self) { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let mut manager = self.manager.lock().await; + manager.clear(); + } + + async fn get(&self, id: u64) -> Option { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let manager = self.manager.lock().await; + manager.get(id).map(Into::into) + } + + async fn get_current_clip( + &self, + kind: dbus_variant::ClipboardKind, + ) -> Option { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let manager = self.manager.lock().await; + manager.get_current_clip(kind.into()).map(|clip| clip.clone().into()) + } + + async fn list(&self, preview_length: u64) -> Vec { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let manager = self.manager.lock().await; + manager + .list(usize::try_from(preview_length).unwrap_or(30)) + .into_iter() + .map(dbus_variant::ClipEntryMetadata::from) + .collect() + } + + async fn update(&self, id: u64, data: &[u8], mime: &str) -> (bool, u64) { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let (ok, new_id) = { + let mime = mime::Mime::from_str(mime).unwrap_or(mime::APPLICATION_OCTET_STREAM); + let mut manager = self.manager.lock().await; + manager.replace(id, data, &mime) + }; + (ok, new_id) + } + + async fn mark(&self, id: u64, kind: dbus_variant::ClipboardKind) -> bool { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let mut manager = self.manager.lock().await; + manager.mark(id, kind.into()).await.is_ok() + } + + #[dbus_interface(property)] + async fn length(&self) -> u64 { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + let manager = self.manager.lock().await; + manager.len() as u64 + } +} diff --git a/crates/server/src/dbus/mod.rs b/crates/server/src/dbus/mod.rs new file mode 100644 index 00000000..cd4d807f --- /dev/null +++ b/crates/server/src/dbus/mod.rs @@ -0,0 +1,5 @@ +mod manager; +mod system; +mod watcher; + +pub use self::{manager::ManagerService, system::SystemService, watcher::WatcherService}; diff --git a/crates/server/src/dbus/system.rs b/crates/server/src/dbus/system.rs new file mode 100644 index 00000000..9d4fe2ff --- /dev/null +++ b/crates/server/src/dbus/system.rs @@ -0,0 +1,22 @@ +use zbus::dbus_interface; + +use crate::metrics; + +pub struct SystemService {} + +impl SystemService { + #[inline] + pub const fn new() -> Self { Self {} } +} + +#[dbus_interface(name = "org.clipcat.clipcat.System")] +impl SystemService { + #[allow(clippy::unused_self)] + #[dbus_interface(property)] + fn get_version(&self) -> &str { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + clipcat_base::PROJECT_VERSION + } +} diff --git a/crates/server/src/dbus/watcher.rs b/crates/server/src/dbus/watcher.rs new file mode 100644 index 00000000..be63aa28 --- /dev/null +++ b/crates/server/src/dbus/watcher.rs @@ -0,0 +1,52 @@ +use clipcat_dbus_variant as dbus_variant; +use zbus::dbus_interface; + +use crate::{metrics, notification, ClipboardWatcherToggle}; + +pub struct WatcherService { + watcher_toggle: ClipboardWatcherToggle, +} + +impl WatcherService { + #[inline] + pub const fn new(watcher_toggle: ClipboardWatcherToggle) -> Self { + Self { watcher_toggle } + } +} + +#[dbus_interface(name = "org.clipcat.clipcat.Watcher")] +impl WatcherService +where + Notification: notification::Notification + 'static, +{ + fn enable(&self) -> dbus_variant::WatcherState { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + self.watcher_toggle.enable(); + self.watcher_toggle.state().into() + } + + fn disable(&self) -> dbus_variant::WatcherState { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + self.watcher_toggle.disable(); + self.watcher_toggle.state().into() + } + + fn toggle(&self) -> dbus_variant::WatcherState { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + self.watcher_toggle.toggle(); + self.watcher_toggle.state().into() + } + + fn get_state(&self) -> dbus_variant::WatcherState { + metrics::dbus::REQUESTS_TOTAL.inc(); + let _histogram_timer = metrics::dbus::REQUEST_DURATION_SECONDS.start_timer(); + + self.watcher_toggle.state().into() + } +} diff --git a/crates/server/src/error.rs b/crates/server/src/error.rs index e2055880..aeca9762 100644 --- a/crates/server/src/error.rs +++ b/crates/server/src/error.rs @@ -19,6 +19,9 @@ pub enum Error { #[snafu(display("Could not create HistoryManager, error: {source}"))] CreateHistoryManager { source: crate::history::Error }, + #[snafu(display("Could not create file watcher, error: {source}"))] + CreateFileWatcher { source: notify::Error }, + #[snafu(display("Could not load HistoryManager, error: {source}"))] LoadHistoryManager { source: crate::history::Error }, @@ -28,6 +31,20 @@ pub enum Error { #[snafu(display("Could not serve ClipboardWatcherWorker, error: {source}"))] ServeClipboardWatcherWorker { source: crate::watcher::Error }, + #[snafu(display("Error occurs while starting dbus service, error: {source}"))] + StartDBusService { source: zbus::Error }, + #[snafu(display("Could not generate clip filter, error: {source}"))] GenerateClipFilter { source: crate::watcher::ClipboardWatcherOptionsError }, + + #[snafu(display("{source}"))] + Metrics { source: clipcat_metrics::Error }, +} + +impl From for Error { + fn from(source: zbus::Error) -> Self { Self::StartDBusService { source } } +} + +impl From for Error { + fn from(source: clipcat_metrics::Error) -> Self { Self::Metrics { source } } } diff --git a/crates/server/src/grpc/interceptor.rs b/crates/server/src/grpc/interceptor.rs new file mode 100644 index 00000000..1d092714 --- /dev/null +++ b/crates/server/src/grpc/interceptor.rs @@ -0,0 +1,44 @@ +use std::fmt; + +use tonic::{metadata::AsciiMetadataValue, Request, Status}; + +use crate::metrics; + +#[derive(Clone, Debug, Default)] +pub struct Interceptor { + authorization_metadata_value: Option, +} + +impl Interceptor { + pub fn new(access_token: Option) -> Self + where + S: fmt::Display, + { + let authorization_metadata_value = match access_token { + Some(token) if token.to_string().is_empty() => None, + Some(token) => AsciiMetadataValue::try_from(format!("Bearer {token}")) + .map_err(|err| { + tracing::warn!("{err}"); + }) + .ok(), + None => None, + }; + + Self { authorization_metadata_value } + } +} + +impl tonic::service::Interceptor for Interceptor { + fn call(&mut self, req: Request<()>) -> Result, Status> { + metrics::grpc::REQUESTS_TOTAL.inc(); + + if let Some(ref expected) = self.authorization_metadata_value { + match req.metadata().get("authorization") { + Some(token) if expected == token => Ok(req), + _ => Err(Status::unauthenticated("No valid authorization token")), + } + } else { + Ok(req) + } + } +} diff --git a/crates/server/src/grpc/mod.rs b/crates/server/src/grpc/mod.rs index cd4d807f..f29b7f26 100644 --- a/crates/server/src/grpc/mod.rs +++ b/crates/server/src/grpc/mod.rs @@ -1,5 +1,9 @@ +mod interceptor; mod manager; mod system; mod watcher; -pub use self::{manager::ManagerService, system::SystemService, watcher::WatcherService}; +pub use self::{ + interceptor::Interceptor, manager::ManagerService, system::SystemService, + watcher::WatcherService, +}; diff --git a/crates/server/src/history/driver/fs/mod.rs b/crates/server/src/history/driver/fs/mod.rs index aa3f1fcf..6c210c56 100644 --- a/crates/server/src/history/driver/fs/mod.rs +++ b/crates/server/src/history/driver/fs/mod.rs @@ -287,15 +287,13 @@ impl Driver for FileSystemDriver { drop(self.clips_file.flush().await); - let mut entries = tokio::fs::read_dir(&image_dir_path) - .await - .with_context(|_| error::ReadDirectorySnafu { dir_path: image_dir_path.clone() })?; - - while let Ok(Some(entry)) = entries.next_entry().await { - let file_path = entry.path(); - if !image_files.contains(&file_path) { - tracing::debug!("Remove image file `{}`", file_path.display()); - drop(tokio::fs::remove_file(file_path).await); + if let Ok(mut entries) = tokio::fs::read_dir(&image_dir_path).await { + while let Ok(Some(entry)) = entries.next_entry().await { + let file_path = entry.path(); + if !image_files.contains(&file_path) { + tracing::debug!("Remove image file `{}`", file_path.display()); + drop(tokio::fs::remove_file(file_path).await); + } } } diff --git a/crates/server/src/history/mod.rs b/crates/server/src/history/mod.rs index 60d118f3..2d1ff3e7 100644 --- a/crates/server/src/history/mod.rs +++ b/crates/server/src/history/mod.rs @@ -32,18 +32,20 @@ impl HistoryManager { self.driver.put(data).await } - #[inline] #[allow(dead_code)] + #[inline] pub async fn clear(&mut self) -> Result<(), Error> { self.driver.clear().await } #[inline] pub async fn load(&mut self) -> Result, Error> { self.driver.load().await } + #[allow(dead_code)] #[inline] pub async fn save(&mut self, data: &[ClipEntry]) -> Result<(), Error> { self.driver.save(data).await } + #[allow(dead_code)] #[inline] pub async fn shrink_to(&mut self, min_capacity: usize) -> Result<(), Error> { self.driver.shrink_to(min_capacity).await diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index e38af525..e7d9ae07 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -1,20 +1,24 @@ pub mod backend; pub mod config; +mod dbus; mod error; mod grpc; mod history; mod manager; +mod metrics; mod notification; +mod snippets; mod watcher; use std::{future::Future, net::SocketAddr, path::PathBuf, pin::Pin, sync::Arc}; -use clipcat_base::ClipEntry; +use clipcat_base::ClipboardKind; use clipcat_proto::{ManagerServer, SystemServer, WatcherServer}; -use futures::{FutureExt, StreamExt}; +use futures::FutureExt; use notification::Notification; use sigfinn::{ExitStatus, Handle, LifecycleManager, Shutdown}; use snafu::ResultExt; +use snippets::SnippetWatcherEvent; use tokio::{ net::UnixListener, sync::{broadcast::error::RecvError, Mutex}, @@ -29,8 +33,10 @@ pub use self::{ use self::{ history::HistoryManager, manager::ClipboardManager, + metrics::Metrics, watcher::{ClipboardWatcher, ClipboardWatcherToggle, ClipboardWatcherWorker}, }; +use crate::snippets::SnippetWatcherEventReceiver; /// # Errors /// @@ -40,12 +46,16 @@ pub async fn serve_with_shutdown( Config { grpc_listen_address, grpc_local_socket, + grpc_access_token, max_history, history_file_path, + synchronize_selection_with_clipboard, watcher: watcher_opts, desktop_notification: desktop_notification_config, + dbus, + metrics: metrics_config, + snippets, }: Config, - snippets: &[ClipEntry], ) -> Result<()> { let clip_filter = Arc::new(watcher_opts.generate_clip_filter().context(error::GenerateClipFilterSnafu)?); @@ -64,7 +74,9 @@ pub async fn serve_with_shutdown( ) .context(error::CreateClipboardBackendSnafu)?; - let (clipboard_manager, history_manager) = { + let (clipboard_manager, history_manager, snippets_watcher, snippet_event_receiver) = { + let ((snippets_watcher, snippet_event_receiver), snippets) = + snippets::load_and_create_watcher(&snippets).await?; tracing::info!("History file path: `{path}`", path = history_file_path.display()); let mut history_manager = HistoryManager::new(&history_file_path) .await @@ -102,9 +114,14 @@ pub async fn serve_with_shutdown( clipboard_manager.import(&history_clips); tracing::info!("Import {snippet_count} snippet(s) into ClipboardManager"); - clipboard_manager.insert_snippets(snippets); + clipboard_manager.insert_snippets(&snippets); - (Arc::new(Mutex::new(clipboard_manager)), history_manager) + ( + Arc::new(Mutex::new(clipboard_manager)), + history_manager, + snippets_watcher, + snippet_event_receiver, + ) }; let (clipboard_watcher, clipboard_watcher_worker) = ClipboardWatcher::new( @@ -123,11 +140,23 @@ pub async fn serve_with_shutdown( ); } + if dbus.enable { + let _handle = lifecycle_manager.spawn( + "D-Bus", + create_dbus_service_future( + clipboard_watcher.get_toggle(), + clipboard_manager.clone(), + dbus.identifier, + ), + ); + } + if let Some(grpc_listen_address) = grpc_listen_address { let _handle = lifecycle_manager.spawn( "gRPC HTTP server", create_grpc_http_server_future( grpc_listen_address, + grpc_access_token.clone(), clipboard_watcher.get_toggle(), clipboard_manager.clone(), ), @@ -139,12 +168,22 @@ pub async fn serve_with_shutdown( "gRPC local socket server", create_grpc_local_socket_server_future( grpc_local_socket, + grpc_access_token, clipboard_watcher.get_toggle(), clipboard_manager.clone(), ), ); } + if metrics_config.enable { + let metrics = Metrics::new()?; + + let _handle = lifecycle_manager.spawn( + "Metrics server", + create_metrics_server_future(metrics_config.listen_address, metrics), + ); + } + let handle = lifecycle_manager.spawn( "Clipboard Watcher worker", create_clipboard_watcher_worker_future(clipboard_watcher_worker), @@ -156,6 +195,8 @@ pub async fn serve_with_shutdown( clipboard_watcher, clipboard_manager, history_manager, + synchronize_selection_with_clipboard, + snippet_event_receiver, handle, ), ); @@ -166,12 +207,14 @@ pub async fn serve_with_shutdown( tracing::error!("{err}"); Err(err) } else { + drop(snippets_watcher); Ok(()) } } fn create_grpc_local_socket_server_future( local_socket: PathBuf, + grpc_access_token: Option, clipboard_watcher_toggle: ClipboardWatcherToggle, clipboard_manager: Arc>>, ) -> impl FnOnce(Shutdown) -> Pin> + Send>> { @@ -183,7 +226,7 @@ fn create_grpc_local_socket_server_future( .await .context(error::CreateUnixListenerSnafu { socket_path: local_socket.clone() }) { - return ExitStatus::Failure(err); + return ExitStatus::FatalError(err); } } @@ -191,15 +234,23 @@ fn create_grpc_local_socket_server_future( .context(error::CreateUnixListenerSnafu { socket_path: local_socket.clone() }) { Ok(uds) => UnixListenerStream::new(uds), - Err(err) => return ExitStatus::Failure(err), + Err(err) => return ExitStatus::FatalError(err), }; + let interceptor = grpc::Interceptor::new(grpc_access_token); let result = tonic::transport::Server::builder() - .add_service(SystemServer::new(grpc::SystemService::new())) - .add_service(WatcherServer::new(grpc::WatcherService::new( - clipboard_watcher_toggle, - ))) - .add_service(ManagerServer::new(grpc::ManagerService::new(clipboard_manager))) + .add_service(SystemServer::with_interceptor( + grpc::SystemService::new(), + interceptor.clone(), + )) + .add_service(WatcherServer::with_interceptor( + grpc::WatcherService::new(clipboard_watcher_toggle), + interceptor.clone(), + )) + .add_service(ManagerServer::with_interceptor( + grpc::ManagerService::new(clipboard_manager), + interceptor, + )) .serve_with_incoming_shutdown(uds_stream, signal) .await .context(error::StartTonicServerSnafu); @@ -214,7 +265,27 @@ fn create_grpc_local_socket_server_future( tracing::info!("gRPC local socket server is shut down gracefully"); ExitStatus::Success } - Err(err) => ExitStatus::Failure(err), + Err(err) => ExitStatus::FatalError(err), + } + } + .boxed() + } +} + +fn create_dbus_service_future( + clipboard_watcher_toggle: ClipboardWatcherToggle, + clipboard_manager: Arc>>, + identifier: Option, +) -> impl FnOnce(Shutdown) -> Pin> + Send>> { + move |signal| { + async move { + match serve_dbus(clipboard_watcher_toggle, clipboard_manager, identifier, signal).await + { + Ok(()) => { + tracing::info!("D-Bus service is shut down gracefully"); + ExitStatus::Success + } + Err(err) => ExitStatus::FatalError(err), } } .boxed() @@ -248,7 +319,7 @@ fn create_clipboard_watcher_worker_future( tracing::info!("Clipboard Watcher worker is shut down gracefully"); ExitStatus::Success } - Err(err) => ExitStatus::Failure(err), + Err(err) => ExitStatus::FatalError(err), } } .boxed() @@ -257,6 +328,7 @@ fn create_clipboard_watcher_worker_future( fn create_grpc_http_server_future( listen_address: SocketAddr, + grpc_access_token: Option, clipboard_watcher_toggle: ClipboardWatcherToggle, clipboard_manager: Arc>>, ) -> impl FnOnce(Shutdown) -> Pin> + Send>> { @@ -264,12 +336,20 @@ fn create_grpc_http_server_future( async move { tracing::info!("Listen Clipcat gRPC endpoint on {listen_address}"); + let interceptor = grpc::Interceptor::new(grpc_access_token); let result = tonic::transport::Server::builder() - .add_service(SystemServer::new(grpc::SystemService::new())) - .add_service(WatcherServer::new(grpc::WatcherService::new( - clipboard_watcher_toggle, - ))) - .add_service(ManagerServer::new(grpc::ManagerService::new(clipboard_manager))) + .add_service(SystemServer::with_interceptor( + grpc::SystemService::new(), + interceptor.clone(), + )) + .add_service(WatcherServer::with_interceptor( + grpc::WatcherService::new(clipboard_watcher_toggle), + interceptor.clone(), + )) + .add_service(ManagerServer::with_interceptor( + grpc::ManagerService::new(clipboard_manager), + interceptor, + )) .serve_with_shutdown(listen_address, signal) .await .context(error::StartTonicServerSnafu); @@ -279,7 +359,7 @@ fn create_grpc_http_server_future( tracing::info!("gRPC HTTP server is shut down gracefully"); ExitStatus::Success } - Err(err) => ExitStatus::Failure(err), + Err(err) => ExitStatus::FatalError(err), } } .boxed() @@ -290,6 +370,8 @@ fn create_clipboard_worker_future( clipboard_watcher: ClipboardWatcher, clipboard_manager: Arc>>, history_manager: HistoryManager, + synchronize_selection_with_clipboard: bool, + snippet_event_receiver: SnippetWatcherEventReceiver, handle: Handle, ) -> impl FnOnce(Shutdown) -> Pin> + Send>> { move |shutdown_signal| { @@ -298,6 +380,8 @@ fn create_clipboard_worker_future( clipboard_watcher, clipboard_manager, history_manager, + synchronize_selection_with_clipboard, + snippet_event_receiver, handle, shutdown_signal, ) @@ -307,7 +391,31 @@ fn create_clipboard_worker_future( tracing::info!("Clipboard worker is shut down gracefully"); ExitStatus::Success } - Err(err) => ExitStatus::Failure(err), + Err(err) => ExitStatus::FatalError(err), + } + } + .boxed() + } +} + +fn create_metrics_server_future( + listen_address: SocketAddr, + metrics: Metrics, +) -> impl FnOnce(Shutdown) -> Pin> + Send>> +where + Metrics: clipcat_metrics::Metrics + 'static, +{ + move |signal| { + async move { + tracing::info!("Listen metrics endpoint on {listen_address}"); + let result = + clipcat_metrics::start_metrics_server(listen_address, metrics, signal).await; + match result { + Ok(()) => { + tracing::info!("Metrics server is shut down gracefully"); + ExitStatus::Success + } + Err(err) => ExitStatus::FatalError(Error::from(err)), } } .boxed() @@ -319,39 +427,94 @@ async fn serve_worker( clipboard_watcher: ClipboardWatcher, clipboard_manager: Arc>>, mut history_manager: HistoryManager, + synchronize_selection_with_clipboard: bool, + mut snippet_event_receiver: SnippetWatcherEventReceiver, handle: Handle, shutdown_signal: Shutdown, ) -> Result<()> { - let mut shutdown_signal = shutdown_signal.into_stream(); - let mut clip_recv = clipboard_watcher.subscribe(); - - loop { - let maybe_clip = tokio::select! { - clip = clip_recv.recv().fuse() => clip, - _ = shutdown_signal.next() => break, - }; + enum Event { + NewClip(clipcat_base::ClipEntry), + NewSnippet(clipcat_base::ClipEntry), + RemoveSnippet(u64), + Shutdown, + } - match maybe_clip { - Ok(clip) => { + let (send, mut recv) = tokio::sync::mpsc::unbounded_channel(); + let snippets_event_handle = tokio::spawn({ + let send = send.clone(); + async move { + while let Some(event) = snippet_event_receiver.recv().await { + let event = match event { + SnippetWatcherEvent::Add(clip) => Event::NewSnippet(clip), + SnippetWatcherEvent::Remove(id) => Event::RemoveSnippet(id), + }; + drop(send.send(event)); + } + } + }); + let clip_reciever_handle = tokio::spawn({ + let send = send.clone(); + async move { + let mut clip_recv = clipboard_watcher.subscribe(); + loop { + match clip_recv.recv().await { + Ok(clip) => drop(send.send(Event::NewClip(clip))), + Err(RecvError::Closed) => { + tracing::info!( + "ClipboardWatcher is closing, no further clip will be received" + ); + + tracing::info!("Internal shutdown signal is sent"); + handle.shutdown(); + + drop(send.send(Event::Shutdown)); + break; + } + Err(RecvError::Lagged(_)) => {} + } + } + } + }); + let shutdown_handle = tokio::spawn(async move { + shutdown_signal.await; + drop(send.send(Event::Shutdown)); + }); + + while let Some(event) = recv.recv().await { + match event { + Event::Shutdown => break, + Event::RemoveSnippet(clip_id) => { + let mut clipboard_manager = clipboard_manager.lock().await; + let _ = clipboard_manager.remove_snippet(clip_id); + } + Event::NewSnippet(snippet) => { + let mut clipboard_manager = clipboard_manager.lock().await; + clipboard_manager.insert_snippets(&[snippet]); + } + Event::NewClip(clip) => { tracing::debug!( "New clip: {kind} [{basic_info}]", kind = clip.kind(), basic_info = clip.basic_information() ); - let _unused = clipboard_manager.lock().await.insert(clip.clone()); + { + let mut clipboard_manager = clipboard_manager.lock().await; + let id = clipboard_manager.insert(clip.clone()); + if synchronize_selection_with_clipboard + && clip.kind() == ClipboardKind::Clipboard + { + if let Err(err) = + clipboard_manager.mark(id, clipcat_base::ClipboardKind::Primary).await + { + tracing::warn!("{err}"); + } + } + } + if let Err(err) = history_manager.put(&clip).await { tracing::error!("{err}"); } } - Err(RecvError::Closed) => { - tracing::info!("ClipboardWatcher is closing, no further clip will be received"); - - tracing::info!("Internal shutdown signal is sent"); - handle.shutdown(); - - break; - } - Err(RecvError::Lagged(_)) => {} } } @@ -368,5 +531,39 @@ async fn serve_worker( tracing::info!("Clips are stored in `{path}`", path = history_manager.path().display()); } + snippets_event_handle.abort(); + clip_reciever_handle.abort(); + shutdown_handle.abort(); + + Ok(()) +} + +async fn serve_dbus( + clipboard_watcher_toggle: ClipboardWatcherToggle, + clipboard_manager: Arc>>, + identifier: Option, + signal: Shutdown, +) -> Result<()> { + let dbus_service_name = identifier.map_or_else( + || clipcat_base::DBUS_SERVICE_NAME.to_string(), + |identifier| format!("{}.{identifier}", clipcat_base::DBUS_SERVICE_NAME), + ); + + tracing::info!("Provide Clipcat D-Bus service at {dbus_service_name}"); + + let system = dbus::SystemService::new(); + let watcher = dbus::WatcherService::new(clipboard_watcher_toggle); + let manager = dbus::ManagerService::new(clipboard_manager); + let _conn = zbus::ConnectionBuilder::session()? + .name(dbus_service_name)? + .serve_at(clipcat_base::DBUS_SYSTEM_OBJECT_PATH, system)? + .serve_at(clipcat_base::DBUS_WATCHER_OBJECT_PATH, watcher)? + .serve_at(clipcat_base::DBUS_MANAGER_OBJECT_PATH, manager)? + .build() + .await?; + + tracing::info!("D-Bus service is created"); + signal.await; + Ok(()) } diff --git a/crates/server/src/manager/mod.rs b/crates/server/src/manager/mod.rs index a893a53b..81c5da8c 100644 --- a/crates/server/src/manager/mod.rs +++ b/crates/server/src/manager/mod.rs @@ -167,6 +167,14 @@ where } } + pub fn remove_snippet(&mut self, id: u64) -> bool { + if self.snippet_ids.remove(&id) { + self.clips.remove(&id).is_some() + } else { + false + } + } + #[inline] pub fn remove(&mut self, id: u64) -> bool { self.remove_inner(id).is_some() } diff --git a/crates/server/src/metrics/dbus.rs b/crates/server/src/metrics/dbus.rs new file mode 100644 index 00000000..d85ed4d3 --- /dev/null +++ b/crates/server/src/metrics/dbus.rs @@ -0,0 +1,13 @@ +use lazy_static::lazy_static; +use prometheus::{Histogram, HistogramOpts, IntCounter}; + +lazy_static! { + pub static ref REQUESTS_TOTAL: IntCounter = + IntCounter::new("dbus_requests_total", "Total number of request from D-Bus") + .expect("setup metrics"); + pub static ref REQUEST_DURATION_SECONDS: Histogram = Histogram::with_opts(HistogramOpts::new( + "dbus_request_duration_seconds", + "Latencies of handling request with D-Bus in seconds" + )) + .expect("setup metrics"); +} diff --git a/crates/server/src/metrics/grpc.rs b/crates/server/src/metrics/grpc.rs new file mode 100644 index 00000000..1414fc45 --- /dev/null +++ b/crates/server/src/metrics/grpc.rs @@ -0,0 +1,8 @@ +use lazy_static::lazy_static; +use prometheus::IntCounter; + +lazy_static! { + pub static ref REQUESTS_TOTAL: IntCounter = + IntCounter::new("grpc_requests_total", "Total number of request from gRPC") + .expect("setup metrics"); +} diff --git a/crates/server/src/metrics/mod.rs b/crates/server/src/metrics/mod.rs new file mode 100644 index 00000000..cf749253 --- /dev/null +++ b/crates/server/src/metrics/mod.rs @@ -0,0 +1,43 @@ +pub mod dbus; +pub mod grpc; + +use clipcat_metrics::error; +use snafu::ResultExt; + +#[derive(Clone, Debug)] +pub struct Metrics { + registry: prometheus::Registry, +} + +impl Metrics { + pub fn new() -> Result { + let registry = prometheus::Registry::new(); + + // gRPC + registry + .register(Box::new(self::grpc::REQUESTS_TOTAL.clone())) + .context(error::SetupMetricsSnafu)?; + + // D-Bus + registry + .register(Box::new(self::dbus::REQUESTS_TOTAL.clone())) + .context(error::SetupMetricsSnafu)?; + registry + .register(Box::new(self::dbus::REQUEST_DURATION_SECONDS.clone())) + .context(error::SetupMetricsSnafu)?; + + Ok(Self { registry }) + } +} + +impl clipcat_metrics::Metrics for Metrics { + fn gather(&self) -> Vec { self.registry.gather() } +} + +#[cfg(test)] +mod tests { + use crate::metrics::Metrics; + + #[test] + fn test_new() { drop(Metrics::new().unwrap()); } +} diff --git a/crates/server/src/notification/mod.rs b/crates/server/src/notification/mod.rs index c0f46a16..c516e339 100644 --- a/crates/server/src/notification/mod.rs +++ b/crates/server/src/notification/mod.rs @@ -2,8 +2,9 @@ mod desktop; mod dummy; mod traits; +#[cfg(test)] +pub use self::dummy::Notification as DummyNotification; pub use self::{ desktop::{Notification as DesktopNotification, Worker as DesktopNotificationWorker}, - dummy::Notification as DummyNotification, traits::Notification, }; diff --git a/crates/server/src/snippets/event_handler.rs b/crates/server/src/snippets/event_handler.rs new file mode 100644 index 00000000..33430109 --- /dev/null +++ b/crates/server/src/snippets/event_handler.rs @@ -0,0 +1,108 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use clipcat_base::ClipEntry; +use notify::{event, Event, EventKind}; +use tokio::sync::mpsc; + +pub enum SnippetWatcherEvent { + Add(ClipEntry), + Remove(u64), +} + +pub struct SnippetWatcherEventReceiver { + event_receiver: mpsc::UnboundedReceiver, +} + +impl SnippetWatcherEventReceiver { + pub async fn recv(&mut self) -> Option { self.event_receiver.recv().await } +} + +pub struct EventHandler { + file_path_to_id: HashMap, + + event_sender: mpsc::UnboundedSender, +} + +impl EventHandler { + pub fn new(file_path_to_id: HashMap) -> (Self, SnippetWatcherEventReceiver) { + let (event_sender, event_receiver) = mpsc::unbounded_channel(); + (Self { file_path_to_id, event_sender }, SnippetWatcherEventReceiver { event_receiver }) + } + + pub fn on_snippet_modified(&mut self, paths: Vec) { + for file_path in paths { + tracing::info!("Snippet `{}` is modified", file_path.display()); + // insert new snippet to clipboard manager + if let Some(clip) = load(&file_path) { + let id = clip.id(); + if let Some(id) = self.file_path_to_id.insert(file_path.clone(), id) { + // remove old snippet from clipboard manager + tracing::info!("Remove clip {id}"); + drop(self.event_sender.send(SnippetWatcherEvent::Remove(id))); + } + drop(self.event_sender.send(SnippetWatcherEvent::Add(clip))); + } + } + } + + pub fn on_snippet_removed(&mut self, paths: Vec) { + for file_path in paths { + tracing::info!("Snippet `{}` is removed", file_path.display()); + if let Some(id) = self.file_path_to_id.remove(&file_path) { + // remove snippet from clipboard manager + tracing::info!("Remove clip {id}"); + drop(self.event_sender.send(SnippetWatcherEvent::Remove(id))); + } + } + } +} + +impl notify::EventHandler for EventHandler { + fn handle_event(&mut self, event: notify::Result) { + match event { + Ok(event) => match event.kind { + EventKind::Modify(event::ModifyKind::Data(_)) => { + self.on_snippet_modified(event.paths); + } + EventKind::Remove(event::RemoveKind::File) => self.on_snippet_removed(event.paths), + _ => {} + }, + Err(err) => tracing::warn!("Error occurs while watching file system, error: {err:?}"), + } + } +} + +fn load

(path: P) -> Option +where + P: AsRef, +{ + let path = path.as_ref().to_path_buf(); + let data = match std::fs::read(&path) { + Ok(data) => data, + Err(err) => { + tracing::warn!("Failed to load snippet from `{}`, error: {err}", path.display()); + return None; + } + }; + + if data.is_empty() { + tracing::warn!("Contents of `{}` is empty", path.display()); + return None; + } + + if let Err(err) = simdutf8::basic::from_utf8(&data) { + tracing::warn!("Contents of `{}` is not valid UTF-8, error: {err}", path.display()); + return None; + } + + clipcat_base::ClipEntry::new( + &data, + &mime::TEXT_PLAIN_UTF_8, + clipcat_base::ClipboardKind::Clipboard, + None, + ) + .ok() +} diff --git a/crates/server/src/snippets/mod.rs b/crates/server/src/snippets/mod.rs new file mode 100644 index 00000000..c64c5571 --- /dev/null +++ b/crates/server/src/snippets/mod.rs @@ -0,0 +1,105 @@ +mod event_handler; + +use std::{collections::HashMap, path::PathBuf}; + +use clipcat_base::ClipEntry; +use notify::{RecursiveMode, Watcher}; +use snafu::ResultExt; +use time::OffsetDateTime; + +use self::event_handler::EventHandler; +pub use self::event_handler::{SnippetWatcherEvent, SnippetWatcherEventReceiver}; +use crate::{config, error, error::Error}; + +async fn load(config: &config::SnippetConfig) -> HashMap> { + let (name, clip_contents) = match config { + config::SnippetConfig::Inline { name, content } => { + tracing::trace!("Load snippet `{name}`"); + (name, vec![(content.as_bytes().to_vec(), None)]) + } + config::SnippetConfig::File { name, path } => { + tracing::trace!("Load snippet `{name}` from file `{}`", path.display()); + let contents = tokio::fs::read(&path).await.map_or_else( + |err| { + tracing::warn!( + "Failed to load snippet from `{}`, error: {err}", + path.display() + ); + Vec::new() + }, + |content| vec![(content, Some(path.clone()))], + ); + (name, contents) + } + config::SnippetConfig::Directory { name, path } => { + tracing::trace!("Load snippet `{name}` from directory `{}`", path.display()); + let contents = futures::future::join_all( + clipcat_base::utils::fs::read_dir_recursively_async(&path) + .await + .into_iter() + .map(|file| (async move { (tokio::fs::read(&file).await.ok(), file) })), + ) + .await + .into_iter() + .filter_map(|(c, _file_path)| c.map(|c| (c, Some(path.clone())))) + .collect(); + (name, contents) + } + }; + + if clip_contents.is_empty() { + tracing::warn!("Snippet `{name}` is empty, ignored it"); + return HashMap::new(); + } + + clip_contents + .into_iter() + .filter_map(|(data, path)| { + if data.is_empty() { + tracing::warn!("Snippet `{name}` is empty, ignored it"); + return None; + } + + if let Err(err) = simdutf8::basic::from_utf8(&data) { + tracing::warn!("Snippet `{name}` is not valid UTF-8 string, error: {err}"); + return None; + } + + clipcat_base::ClipEntry::new( + &data, + &mime::TEXT_PLAIN_UTF_8, + clipcat_base::ClipboardKind::Clipboard, + Some(OffsetDateTime::UNIX_EPOCH), + ) + .ok() + .map(|clip| (clip, path)) + }) + .collect() +} + +pub async fn load_and_create_watcher( + snippets: &[config::SnippetConfig], +) -> Result<((notify::RecommendedWatcher, SnippetWatcherEventReceiver), Vec), Error> { + let mut file_path_to_id = HashMap::new(); + let mut file_paths = Vec::new(); + let mut new_clips = Vec::new(); + for snippet in snippets { + for (clip, file_path) in load(snippet).await { + if let Some(file_path) = file_path { + file_paths.push(file_path.clone()); + let _ = file_path_to_id.insert(file_path.clone(), clip.id()); + } + new_clips.push(clip); + } + } + + let (event_handler, event_receiver) = EventHandler::new(file_path_to_id); + let mut watcher = + notify::recommended_watcher(event_handler).context(error::CreateFileWatcherSnafu)?; + for file_path in file_paths { + if let Err(err) = watcher.watch(&file_path, RecursiveMode::Recursive) { + tracing::warn!("Could not watch file {}, error: {err}", file_path.display()); + } + } + Ok(((watcher, event_receiver), new_clips)) +} diff --git a/crates/server/src/watcher/mod.rs b/crates/server/src/watcher/mod.rs index 7d471098..8e3bfc53 100644 --- a/crates/server/src/watcher/mod.rs +++ b/crates/server/src/watcher/mod.rs @@ -73,46 +73,38 @@ impl Worker { #[allow(clippy::redundant_pub_crate)] pub async fn serve(self, shutdown_signal: sigfinn::Shutdown) -> Result<(), Error> { let enabled_kinds = self.opts.get_enable_kinds(); - let Self { - backend, - is_watching, - clip_sender, - clip_filter, - opts: ClipboardWatcherOptions { load_current, .. }, - } = self; + let Self { backend, is_watching, clip_sender, clip_filter, .. } = self; let mut subscriber = backend.subscribe()?; let mut shutdown_signal = shutdown_signal.into_stream(); let mut current_contents: [ClipboardContent; ClipboardKind::MAX_LENGTH] = [ClipboardContent::default(), ClipboardContent::default(), ClipboardContent::default()]; - if load_current { - for (kind, enable) in enabled_kinds - .iter() - .enumerate() - .map(|(kind, &enable)| (ClipboardKind::from(kind), enable)) - { - if enable { - match backend.load(kind, None).await { - Ok(data) => { - if !clip_filter.filter_clipboard_content(data.as_ref()) { - current_contents[usize::from(kind)] = data.clone(); - if let Err(_err) = clip_sender - .send(ClipEntry::from_clipboard_content(data, kind, None)) - { - tracing::info!("ClipEntry receiver is closed."); - return Err(Error::SendClipEntry); - } + for (kind, enable) in enabled_kinds + .iter() + .enumerate() + .map(|(kind, &enable)| (ClipboardKind::from(kind), enable)) + { + if enable { + match backend.load(kind, None).await { + Ok(data) => { + if !clip_filter.filter_clipboard_content(data.as_ref()) { + current_contents[usize::from(kind)] = data.clone(); + if let Err(_err) = clip_sender + .send(ClipEntry::from_clipboard_content(data, kind, None)) + { + tracing::info!("ClipEntry receiver is closed."); + return Err(Error::SendClipEntry); } } - Err( - BackendError::EmptyClipboard - | BackendError::MatchMime { .. } - | BackendError::UnknownContentType - | BackendError::UnsupportedClipboardKind { .. }, - ) => continue, - Err(error) => { - tracing::error!("Failed to load clipboard, error: {error}"); - } + } + Err( + BackendError::EmptyClipboard + | BackendError::MatchMime { .. } + | BackendError::UnknownContentType + | BackendError::UnsupportedClipboardKind { .. }, + ) => continue, + Err(error) => { + tracing::error!("Failed to load clipboard, error: {error}"); } } } diff --git a/crates/server/src/watcher/options.rs b/crates/server/src/watcher/options.rs index 22d1c963..a432cda1 100644 --- a/crates/server/src/watcher/options.rs +++ b/crates/server/src/watcher/options.rs @@ -7,8 +7,6 @@ use snafu::Snafu; #[allow(clippy::struct_excessive_bools)] #[derive(Clone, Debug)] pub struct Options { - pub load_current: bool, - pub enable_clipboard: bool, pub enable_primary: bool, @@ -60,7 +58,6 @@ impl Options { impl Default for Options { fn default() -> Self { Self { - load_current: true, enable_clipboard: true, enable_primary: true, enable_secondary: false, diff --git a/dev-support/bin/create-package b/dev-support/bin/create-package index 91a89402..3af13197 100755 --- a/dev-support/bin/create-package +++ b/dev-support/bin/create-package @@ -18,17 +18,17 @@ cp -v "target/$TARGET/release/clipcatctl" "$DIST" cp -v "target/$TARGET/release/clipcat-notify" "$DIST" cp -v "target/$TARGET/release/clipcat-menu" "$DIST" cp \ - LICENSE \ - README.md \ - "$DIST" + LICENSE \ + README.md \ + "$DIST" cd "$DIST" echo "Creating release archive..." case "$OS" in ubuntu-latest | macos-latest) - ARCHIVE="$DIST/$PACKAGE_NAME-$VERSION-$TARGET.tar.gz" - tar czvf "$ARCHIVE" -- * - echo "::set-output name=archive::$ARCHIVE" - ;; + ARCHIVE="$DIST/$PACKAGE_NAME-$VERSION-$TARGET.tar.gz" + tar czvf "$ARCHIVE" -- * + echo "archive=$ARCHIVE" >>"$GITHUB_OUTPUT" + ;; esac diff --git a/dev-support/bin/prepare-deb-package b/dev-support/bin/prepare-deb-package new file mode 100755 index 00000000..ff5f92a0 --- /dev/null +++ b/dev-support/bin/prepare-deb-package @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +PACKAGE_NAME="clipcat" +VERSION=$(basename "$REF") +DIST=$(pwd)/.debpkg + +echo "Packaging $PACKAGE_NAME $VERSION for $TARGET..." + +echo "Building $PACKAGE_NAME..." +RUSTFLAGS="$TARGET_RUSTFLAGS" cargo build --target "$TARGET" --release + +mkdir -p "$DIST/usr/bin" +mkdir -p "$DIST/usr/share/bash-completion/completions/" +mkdir -p "$DIST/usr/share/zsh/vendor-completions/" +mkdir -p "$DIST/usr/share/fish/vendor_completions.d/" + +for cmd in clipcatd clipcatctl clipcat-menu clipcat-notify; do + cp -v "target/$TARGET/release/${cmd}" "$DIST/usr/bin" + + "$DIST/usr/bin/${cmd}" completions bash >"$DIST/usr/share/bash-completion/completions/${cmd}" + chmod 644 "$DIST/usr/share/bash-completion/completions/${cmd}" + + "$DIST/usr/bin/${cmd}" completions zsh >"$DIST/usr/share/zsh/vendor-completions/_${cmd}" + chmod 644 "$DIST/usr/share/zsh/vendor-completions/_${cmd}" + + "$DIST/usr/bin/${cmd}" completions fish >"$DIST/usr/share/fish/vendor_completions.d/${cmd}.fish" + chmod 644 "$DIST/usr/share/fish/vendor_completions.d/${cmd}.fish" +done diff --git a/dev-support/bin/prepare-rpm-package b/dev-support/bin/prepare-rpm-package new file mode 100755 index 00000000..892f29bb --- /dev/null +++ b/dev-support/bin/prepare-rpm-package @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +PACKAGE_NAME="clipcat" +VERSION=$(basename "$REF") +DIST=$(pwd)/.rpmpkg + +echo "Packaging $PACKAGE_NAME $VERSION for $TARGET..." + +echo "Building $PACKAGE_NAME..." +RUSTFLAGS="$TARGET_RUSTFLAGS" cargo build --target "$TARGET" --release + +mkdir -p "$DIST/usr/bin" +mkdir -p "$DIST/usr/share/bash-completion/completions/" +mkdir -p "$DIST/usr/share/zsh/site-functions/" +mkdir -p "$DIST/usr/share/fish/vendor_completions.d/" + +for cmd in clipcatd clipcatctl clipcat-menu clipcat-notify; do + cp -v "target/$TARGET/release/${cmd}" "$DIST/usr/bin" + + "$DIST/usr/bin/${cmd}" completions bash >"$DIST/usr/share/bash-completion/completions/${cmd}" + chmod 644 "$DIST/usr/share/bash-completion/completions/${cmd}" + + "$DIST/usr/bin/${cmd}" completions zsh >"$DIST/usr/share/zsh/site-functions/_${cmd}" + chmod 644 "$DIST/usr/share/zsh/site-functions/_${cmd}" + + "$DIST/usr/bin/${cmd}" completions fish >"$DIST/usr/share/fish/vendor_completions.d/${cmd}.fish" + chmod 644 "$DIST/usr/share/fish/vendor_completions.d/${cmd}.fish" +done diff --git a/devshell/default.nix b/devshell/default.nix index ea6d40b8..83fd215a 100644 --- a/devshell/default.nix +++ b/devshell/default.nix @@ -38,6 +38,9 @@ pkgs.mkShell { clang-tools shellcheck + + pkg-config + libgit2 ]; shellHook = '' diff --git a/flake.lock b/flake.lock index 3ed89425..d427964b 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1702488130, - "narHash": "sha256-Bz4KTuBARAQY8952CpmYVD9o/LoScYjdw8KrK2OjEoA=", + "lastModified": 1703439018, + "narHash": "sha256-VT+06ft/x3eMZ1MJxWzQP3zXFGcrxGo5VR2rB7t88hs=", "owner": "ipetkov", "repo": "crane", - "rev": "33dbb6a8342e1cf6252c8976d02ff8a7632aa071", + "rev": "afdcd41180e3dfe4dac46b5ee396e3b12ccc967a", "type": "github" }, "original": { @@ -28,11 +28,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1702534966, - "narHash": "sha256-jMJ54aI5xk5vpoGIKFHw0HMOIS8a03tZ46gBtW+ghIk=", + "lastModified": 1704003651, + "narHash": "sha256-bA3d4E1CX5G7TVbKwJOm9jZfVOGOPp6u5CKEUzNsE8E=", "owner": "nix-community", "repo": "fenix", - "rev": "aa8f8228044e2fc224ceb9f751b5e0d5b580745a", + "rev": "c6d82e087ac96f24b90c5787a17e29a72566c2b4", "type": "github" }, "original": { @@ -61,11 +61,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1702312524, - "narHash": "sha256-gkZJRDBUCpTPBvQk25G0B7vfbpEYM5s5OZqghkjZsnE=", + "lastModified": 1703637592, + "narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a9bf124c46ef298113270b1f84a164865987a91c", + "rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8", "type": "github" }, "original": { @@ -86,11 +86,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1702503018, - "narHash": "sha256-KTz3cZL2NypvIPb594j9GUizhifhfX6BJ1ks58fTWbM=", + "lastModified": 1703965384, + "narHash": "sha256-3iyouqkBvhh/E48TkBlt4JmmcIEyfQwY7pokKBx9WNg=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "dd07f1f2fbfd7e6ea581240af07131a1b7368b0f", + "rev": "e872f5085cf5b0e44558442365c1c033d486eff2", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index ed6b6642..94f7654e 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ outputs = { self, nixpkgs, flake-utils, fenix, crane }: let name = "clipcat"; - version = "0.15.0"; + version = "0.16.0"; in (flake-utils.lib.eachDefaultSystem (system: