From 175d66de0dd3d21ae9e7d48bf54bb945e2236db1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:03:46 +0000 Subject: [PATCH 1/2] build(deps): bump tears from 0.9.3 to 0.10.2 Bumps [tears](https://github.com/akiomik/tears) from 0.9.3 to 0.10.2. - [Release notes](https://github.com/akiomik/tears/releases) - [Changelog](https://github.com/akiomik/tears/blob/main/CHANGELOG.md) - [Commits](https://github.com/akiomik/tears/compare/v0.9.3...v0.10.2) --- updated-dependencies: - dependency-name: tears dependency-version: 0.10.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18bc3e58..63c44910 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3355,9 +3355,9 @@ dependencies = [ [[package]] name = "tears" -version = "0.9.3" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4035efc0237686142c1b9e4d7160349a6017585b737a93c121124e2917d1d32" +checksum = "b888142d4e86d8c603141b7db17beb505fa62fb4bf2f8dbf03f594d215eb1d86" dependencies = [ "color-eyre", "crossterm 0.29.0", diff --git a/Cargo.toml b/Cargo.toml index a090ecf1..c1941872 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ signal-hook = "0.4" sorted-vec = "0.8" strip-ansi-escapes = "0.2" strum = { version = "0.28", features = ["derive"] } -tears = "0.9.1" +tears = "0.10.2" thousands = "0.2" tokio = { version = "1", features = ["full"] } tokio-util = "0.7" From 4648bc393cfe7d43a09c2cec8d4c7118c8291d20 Mon Sep 17 00:00:00 2001 From: Akiomi Kamakura Date: Tue, 28 Jul 2026 11:32:16 +0900 Subject: [PATCH 2/2] fix: migrate to tears 0.10 breaking API changes tears 0.10 replaces SubscriptionSource::id() with a Key associated type and key() method, removes SubscriptionId::of, makes Command::effect private in favor of Command::quit(), replaces Timer::try_new(u64) with Timer::new(NonZeroU64), and replaces Runtime::try_new(flags, u32) with the infallible Runtime::new(flags, FrameRate). --- src/infrastructure/subscription/media.rs | 7 ++- src/infrastructure/subscription/nostr.rs | 64 +++++++++++------------- src/main.rs | 30 ++++++++--- src/runtime.rs | 10 ++-- 4 files changed, 63 insertions(+), 48 deletions(-) diff --git a/src/infrastructure/subscription/media.rs b/src/infrastructure/subscription/media.rs index 7f6f4723..8e461a21 100644 --- a/src/infrastructure/subscription/media.rs +++ b/src/infrastructure/subscription/media.rs @@ -1,7 +1,7 @@ use futures::stream; use futures::stream::{BoxStream, StreamExt}; use nowhear::{MediaEvent, MediaSource, MediaSourceBuilder, MediaSourceError}; -use tears::{SubscriptionId, SubscriptionSource}; +use tears::SubscriptionSource; #[derive(Clone, Debug, Default)] pub struct MediaEvents; @@ -15,6 +15,7 @@ impl MediaEvents { impl SubscriptionSource for MediaEvents { type Output = Result; + type Key = (); fn stream(&self) -> BoxStream<'static, Self::Output> { stream::once(async { @@ -30,9 +31,7 @@ impl SubscriptionSource for MediaEvents { .boxed() } - fn id(&self) -> SubscriptionId { - SubscriptionId::of::(42) - } + fn key(&self) -> Self::Key {} } #[cfg(test)] diff --git a/src/infrastructure/subscription/nostr.rs b/src/infrastructure/subscription/nostr.rs index 7f19af95..74672332 100644 --- a/src/infrastructure/subscription/nostr.rs +++ b/src/infrastructure/subscription/nostr.rs @@ -6,7 +6,7 @@ use futures::{ StreamExt, }; use nostr_sdk::prelude::*; -use tears::{SubscriptionId, SubscriptionSource}; +use tears::SubscriptionSource; use tokio::sync::{broadcast, mpsc, RwLock}; use crate::domain::nostr::feed_filter::{ @@ -336,6 +336,7 @@ impl NostrEvents { impl SubscriptionSource for NostrEvents { type Output = Message; + type Key = u64; fn stream(&self) -> BoxStream<'static, Self::Output> { let (msg_tx, msg_rx) = mpsc::unbounded_channel(); @@ -365,11 +366,10 @@ impl SubscriptionSource for NostrEvents { .boxed() } - fn id(&self) -> SubscriptionId { - // Use the Arc pointer address as a unique ID - // Same Arc instance = same ID, different Client instance = different ID - let ptr = Arc::as_ptr(&self.client) as usize as u64; - SubscriptionId::of::(ptr) + fn key(&self) -> Self::Key { + // Use the Arc pointer address as the structural key + // Same Arc instance = same key, different Client instance = different key + Arc::as_ptr(&self.client) as usize as u64 } } @@ -395,44 +395,40 @@ mod tests { } #[test] - fn test_subscription_id_uses_arc_pointer() { + fn test_subscription_key_uses_arc_pointer() { use tears::SubscriptionSource; let client = Arc::new(Client::default()); let nostr_events1 = NostrEvents::new(Arc::clone(&client)); let nostr_events2 = nostr_events1.clone(); - // Same Arc should produce same ID + // Same Arc should produce same key assert_eq!( - nostr_events1.id(), - nostr_events2.id(), - "Cloned NostrEvents should share the same Arc and produce the same ID" + nostr_events1.key(), + nostr_events2.key(), + "Cloned NostrEvents should share the same Arc and produce the same key" ); - // Verify ID is not zero (regression test for the bug where ID was always 0) - let id1 = nostr_events1.id(); + // Verify key is not zero (regression test for the bug where ID was always 0) + let key1 = nostr_events1.key(); let ptr1 = Arc::as_ptr(&nostr_events1.client) as usize as u64; - assert_eq!( - SubscriptionId::of::(ptr1), - id1, - "ID should be based on Arc pointer address" - ); + assert_eq!(ptr1, key1, "Key should be based on Arc pointer address"); assert_ne!( ptr1, 0, "Arc pointer address should not be zero in normal circumstances" ); - // Reusing the same Arc should produce the same ID + // Reusing the same Arc should produce the same key let nostr_events3 = NostrEvents::new(Arc::clone(&client)); assert_eq!( - nostr_events1.id(), - nostr_events3.id(), - "Different NostrEvents instances with the same Arc should have the same ID" + nostr_events1.key(), + nostr_events3.key(), + "Different NostrEvents instances with the same Arc should have the same key" ); } #[test] - fn test_subscription_id_different_clients() { + fn test_subscription_key_different_clients() { use tears::SubscriptionSource; // Create two separate clients with different Arcs @@ -442,14 +438,14 @@ mod tests { let nostr_events1 = NostrEvents::new(Arc::clone(&client1)); let nostr_events2 = NostrEvents::new(Arc::clone(&client2)); - // Different Arc instances should produce different IDs + // Different Arc instances should produce different keys assert_ne!( - nostr_events1.id(), - nostr_events2.id(), - "Different Arc instances should produce different subscription IDs" + nostr_events1.key(), + nostr_events2.key(), + "Different Arc instances should produce different subscription keys" ); - // Verify both IDs use actual pointer addresses + // Verify both keys use actual pointer addresses let ptr1 = Arc::as_ptr(&nostr_events1.client) as usize as u64; let ptr2 = Arc::as_ptr(&nostr_events2.client) as usize as u64; assert_ne!( @@ -459,20 +455,20 @@ mod tests { } #[test] - fn test_subscription_id_different_arc_instances() { + fn test_subscription_key_different_arc_instances() { use tears::SubscriptionSource; let client = Client::default(); - // Creating separate Arc instances produces different IDs + // Creating separate Arc instances produces different keys let nostr_events1 = NostrEvents::new(Arc::new(client.clone())); let nostr_events2 = NostrEvents::new(Arc::new(client)); - // Different Arc instances should produce different IDs + // Different Arc instances should produce different keys assert_ne!( - nostr_events1.id(), - nostr_events2.id(), - "Different Arc instances produce different subscription IDs" + nostr_events1.key(), + nostr_events2.key(), + "Different Arc instances produce different subscription keys" ); // This demonstrates why you must share the same Arc diff --git a/src/main.rs b/src/main.rs index 04bc5d42..bb213d9d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,12 @@ #![deny(warnings)] +use std::num::{NonZeroU32, NonZeroU64}; + use clap::Parser; use color_eyre::eyre::{eyre, Result}; use nostr_sdk::prelude::*; use secrecy::ExposeSecret; -use tears::{subscription::time::Timer, Runtime}; +use tears::{subscription::time::Timer, FrameRate, Runtime}; use nostui::{ application::config::Config, @@ -25,9 +27,25 @@ fn tick_timer_from_rate(tick_rate: f64) -> Result { )); } - Timer::try_new(interval_ms as u64).ok_or_else(|| { + let interval_ms = NonZeroU64::new(interval_ms as u64).ok_or_else(|| { eyre!("tick rate is too high to produce a non-zero millisecond timer interval: {tick_rate}") - }) + })?; + + Ok(Timer::new(interval_ms)) +} + +fn frame_rate_from_value(frame_rate: f64) -> Result { + if !frame_rate.is_finite() || frame_rate <= 0.0 { + return Err(eyre!("frame rate must be a positive finite number")); + } + if frame_rate > f64::from(u32::MAX) { + return Err(eyre!("frame rate is too high: {frame_rate}")); + } + + let frames_per_second = NonZeroU32::new(frame_rate as u32) + .ok_or_else(|| eyre!("frame rate is too low to produce a non-zero FPS: {frame_rate}"))?; + + FrameRate::new(frames_per_second).map_err(|e| eyre!("invalid frame rate: {e}")) } async fn tokio_main() -> Result<()> { @@ -79,7 +97,7 @@ async fn tokio_main() -> Result<()> { "Starting Tears application with frame_rate: {}", args.frame_rate ); - let runtime = Runtime::::try_new(init_flags, args.frame_rate as u32)?; + let runtime = Runtime::::new(init_flags, frame_rate_from_value(args.frame_rate)?); let result = runtime.run(&mut terminal).await; // Restore terminal @@ -106,11 +124,11 @@ mod tests { fn tick_timer_from_rate_accepts_positive_tick_rate() { assert_eq!( tick_timer_from_rate(16.0).expect("tick rate should be valid"), - Timer::try_new(62).expect("timer interval should be valid") + Timer::new(NonZeroU64::new(62).expect("non-zero")) ); assert_eq!( tick_timer_from_rate(1000.0).expect("tick rate should be valid"), - Timer::try_new(1).expect("timer interval should be valid") + Timer::new(NonZeroU64::new(1).expect("non-zero")) ); } diff --git a/src/runtime.rs b/src/runtime.rs index 8329ab12..c88368a0 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -85,7 +85,7 @@ impl<'a> Application for TearsApp<'a> { // machine is to move the per-domain dispatch (`handle_timeline_msg`, // `handle_editor_msg`, `handle_nostr_msg`, ...) into `AppState::update(AppMsg)`, // leaving `TearsApp` as a thin tears adapter responsible only for IO-coupled - // concerns: key -> message mapping, subscriptions, and `Command::effect(Quit)`. + // concerns: key -> message mapping, subscriptions, and `Command::quit()`. // That would make `AppState` own both state and transitions, and would let its // fields become private (external code could only drive it via messages). fn update(&mut self, msg: AppMsg) -> Command { @@ -187,7 +187,7 @@ impl<'a> TearsApp<'a> { let _ = self.state.close_connection(); // Trigger the quit action - Command::effect(Action::Quit) + Command::quit() } SystemMsg::Resize(width, height) => { log::debug!("Terminal resized to {width}x{height}"); @@ -433,6 +433,8 @@ impl<'a> TearsApp<'a> { #[cfg(test)] mod tests { + use std::num::NonZeroU64; + use super::*; use crate::application::config::Config; use crate::domain::nostr::FeedKind; @@ -450,7 +452,7 @@ mod tests { pubkey: keys.public_key(), config, nostr_client: client, - tick_timer: Timer::try_new(62).expect("test timer interval must be valid"), + tick_timer: Timer::new(NonZeroU64::new(62).expect("non-zero")), }; let (app, _) = TearsApp::new(flags); @@ -676,7 +678,7 @@ mod tests { let cmd = app.update(quit_msg); assert!(cmd.is_some()); - // Command should be Action::Quit effect (we can't directly test this, + // Command should be a Command::quit() effect (we can't directly test this, // but the system should have processed it) // The test passes if no panic occurs }