feat: implement confetti command

This commit is contained in:
ByteAtATime 2025-11-30 13:41:20 -08:00
parent 92ea0e8346
commit faf926288f
No known key found for this signature in database
4 changed files with 160 additions and 1 deletions

View file

@ -13,7 +13,7 @@ clap = { version = "4", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# pinned to latest commit as of 2025-11-21
iced = { git = "https://github.com/iced-rs/iced", rev = "8bfd099c5929d927a3fdde666d4c645d0bd83cb7", features = ["advanced", "canvas", "image", "markdown", "svg", "tokio"] }
iced = { git = "https://github.com/iced-rs/iced", rev = "8bfd099c5929d927a3fdde666d4c645d0bd83cb7", features = ["advanced", "canvas", "image", "markdown", "svg", "tokio", "wgpu"] }
reqwest = { version = "0.12.8", features = ["blocking", "rustls-tls"] }
tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros", "net", "sync", "process"] }
phf = { version = "0.13.1", features = ["macros"] }

View file

@ -61,6 +61,7 @@ enum Command {
Toggle,
Dev,
Settings,
Confetti,
}
fn boot() -> (State, iced::Task<Message>) {
@ -184,6 +185,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Some(Command::Daemon) => run_daemon(),
Some(Command::Dev) => run_dev(),
Some(Command::Settings) => settings::run(),
Some(Command::Confetti) => run_confetti(),
None => run_application(),
}
}
@ -298,3 +300,152 @@ fn run_daemon() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[cfg(target_os = "linux")]
fn run_confetti() -> Result<(), Box<dyn std::error::Error>> {
use crate::components::confetti::{Manager, Options};
use iced::widget::canvas;
use iced::{Color, Length, Point, Rectangle, Size, Task, time, window};
use iced_layershell::reexport::{Anchor, KeyboardInteractivity, Layer};
use iced_layershell::settings::{LayerShellSettings, StartMode};
use iced_layershell::to_layer_message;
struct ConfettiState {
manager: Manager,
window_id: Option<window::Id>,
fired: bool,
}
#[to_layer_message]
#[derive(Debug, Clone)]
enum ConfettiMessage {
Tick,
WindowOpened(window::Id),
MonitorSize(Option<Size>),
}
fn boot() -> (ConfettiState, Task<ConfettiMessage>) {
(
ConfettiState {
manager: Manager::new(),
window_id: None,
fired: false,
},
Task::none(),
)
}
fn update(state: &mut ConfettiState, message: ConfettiMessage) -> Task<ConfettiMessage> {
match message {
ConfettiMessage::Tick => {
state.manager.update();
Task::none()
}
ConfettiMessage::WindowOpened(id) => {
state.window_id = Some(id);
window::size(id).map(|arg0: iced::Size| ConfettiMessage::MonitorSize(Some(arg0)))
}
ConfettiMessage::MonitorSize(size) => {
if state.fired {
return Task::none();
}
state.fired = true;
// distance = (velocity * cos(angle))/(1 - decay)
// rearranging for velocity, distance(1-decay)/cos(angle)
// (width/2)(1-0.97)/cos(45) = width / 47.1
let size = size.unwrap_or(Size::new(1920.0, 1080.0));
let options = Options {
particle_count: 300,
spread: 45.0,
start_velocity: size.width / 47.1,
gravity: size.width / 960.0,
decay: 0.97,
ticks: 160.0, // about 2.5 seconds, seems like a reasonable duration
origin: Point { x: 1.0, y: 1.0 },
scalar: 1.2,
..Default::default()
};
state.manager.fire_with_bounds(
Options {
angle: 135.0,
origin: Point { x: 1.0, y: 1.0 },
..options.clone()
},
Rectangle::new(Point::ORIGIN, size),
);
state.manager.fire_with_bounds(
Options {
angle: 45.0,
origin: Point { x: 0.0, y: 1.0 },
..options
},
Rectangle::new(Point::ORIGIN, size),
);
Task::none()
}
_ => Task::none(),
}
}
fn view(state: &ConfettiState) -> iced::Element<'_, ConfettiMessage> {
iced::widget::container(
canvas(&state.manager)
.width(Length::Fill)
.height(Length::Fill),
)
.width(Length::Fill)
.height(Length::Fill)
.style(|_| iced::widget::container::Style {
background: Some(Color::TRANSPARENT.into()),
..Default::default()
})
.into()
}
fn subscription(state: &ConfettiState) -> iced::Subscription<ConfettiMessage> {
use std::time::Duration;
let tick = time::every(Duration::from_millis(1000 / 60)).map(|_| ConfettiMessage::Tick);
let window_events = if state.window_id.is_none() {
iced::event::listen_with(|event, _status, id| {
if let iced::Event::Window(iced::window::Event::Opened { .. }) = event {
return Some(ConfettiMessage::WindowOpened(id));
}
None
})
} else {
iced::Subscription::none()
};
iced::Subscription::batch([tick, window_events])
}
iced_layershell::application(boot, || String::from("flare-confetti"), update, view)
.subscription(subscription)
.style(|_state, theme| iced::theme::Style {
background_color: Color::TRANSPARENT,
text_color: theme.palette().text,
})
.layer_settings(LayerShellSettings {
start_mode: StartMode::Active,
layer: Layer::Overlay,
anchor: Anchor::Top | Anchor::Bottom | Anchor::Left | Anchor::Right,
keyboard_interactivity: KeyboardInteractivity::None,
exclusive_zone: 0,
events_transparent: true,
..Default::default()
})
.run()
.map_err(|e| e.to_string().into())
}
#[cfg(not(target_os = "linux"))]
fn run_confetti() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("Confetti is not supported yet!");
Ok(())
}

View file

@ -31,6 +31,7 @@ pub enum Message {
ExtensionLaunched(Result<(SidecarWriter, Arc<Mutex<SidecarReader>>), String>),
LaunchApp(AppEntry),
ResetFrecency(String),
TriggerConfetti,
PopToRoot,
WindowOpened(window::Id),

View file

@ -220,6 +220,13 @@ pub fn update(state: &mut State, message: Message) -> Task<Message> {
}
state.update_selected_actions();
}
Message::TriggerConfetti => {
let current_exe =
std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("flare"));
let _ = std::process::Command::new(current_exe)
.arg("confetti")
.spawn();
}
Message::PopToRoot => {
state.writer = None;
state.reader = None;