use std::io::{Read, Write}; use std::sync::Arc; use cosmic::Element; use cosmic::app::{Core, Settings, Task}; use cosmic::iced::{Alignment, Length}; use cosmic::widget::{button, column, container, image, row, scrollable, text_input}; use ferese_theme::material::ModalMaterial; use ferese_theme::{Palette, accent_button, controls, text}; use serde::{Deserialize, Serialize}; #[derive(Clone, Serialize, Deserialize)] pub(crate) struct Prompt { pub title: String, pub description: String, pub accept: String, pub parent: String, pub image: Option, #[serde(default)] pub shortcuts: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] pub(crate) struct ShortcutField { pub id: String, pub description: String, pub trigger: String, } #[derive(Clone, Debug)] enum Message { ThemeChanged(Box), Opened(cosmic::iced::window::Id), Attached(crate::parent::Attachment), Accept, Cancel, Shortcut(usize, String), } struct Consent { core: Core, prompt: Prompt, material: Option, parent: Option>, palette: Palette, font: cosmic::font::Font, background: cosmic::iced::Color, } pub(crate) fn run() -> Result<(), Box> { let mut input = String::new(); std::io::stdin().take(256 * 1024 + 1).read_to_string(&mut input)?; if input.len() > 256 * 1024 { return Err("Consent request is too large".into()); } let prompt: Prompt = serde_json::from_str(&input)?; let (theme, font, background, _) = crate::picker::appearance(); let height = if prompt.image.is_some() || !prompt.shortcuts.is_empty() { 430. } else { 220. }; cosmic::app::run::( Settings::default() .size(cosmic::iced::Size::new(440., height)) .client_decorations(false) .transparent(true) .theme(theme) .default_font(font) .default_text_size(14.) .is_daemon(false), (prompt, font, background), )?; Ok(()) } impl cosmic::Application for Consent { type Executor = cosmic::executor::Default; type Flags = (Prompt, cosmic::font::Font, cosmic::iced::Color); type Message = Message; const APP_ID: &'static str = "dev.ferese.PortalDialog"; fn core(&self) -> &Core { &self.core } fn core_mut(&mut self) -> &mut Core { &mut self.core } fn init(mut core: Core, (prompt, font, background): Self::Flags) -> (Self, Task) { core.window.show_headerbar = false; core.window.border_padding = Some(0); core.window.content_container = false; core.window.use_template = false; let app = Self { core, prompt, material: None, parent: None, palette: Palette::from_resolved(&ferese_theme::service::current().presented), font, background, }; (app, Task::none()) } fn subscription(&self) -> cosmic::iced::Subscription { cosmic::iced::Subscription::batch([ ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))), cosmic::iced::event::listen_with(|event, _, id| match event { cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::Opened(id)), cosmic::iced::Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed { key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape), .. }) => Some(Message::Cancel), _ => None, }), ]) } fn update(&mut self, message: Message) -> Task { match message { Message::ThemeChanged(snapshot) => { self.palette = Palette::from_resolved(&snapshot.presented).flat(); self.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family)); self.background = cosmic::iced::Color { a: ferese_theme::service::opacity(&snapshot.presented), ..self.palette.sidebar }; return cosmic::command::set_theme(self.palette.native_theme()); } Message::Opened(id) => { let parent = self.prompt.parent.clone(); return cosmic::iced::window::run(id, move |window| { let parent = crate::parent::Parent::attach(window, &parent)?.map(Arc::new); Ok((parent, ModalMaterial::attach(window))) }) .map(|result| cosmic::Action::App(Message::Attached(result))); } Message::Attached(Ok((parent, material))) => { self.parent = parent; self.material = material.ok(); } Message::Attached(Err(error)) => { eprintln!("ferese portal: {error}"); return cosmic::iced::exit(); } Message::Accept => { if self.prompt.shortcuts.is_empty() { println!("true"); } else { println!("{}", serde_json::to_string(&self.prompt.shortcuts).unwrap()); } let _ = std::io::stdout().flush(); return cosmic::iced::exit(); } Message::Cancel => return cosmic::iced::exit(), Message::Shortcut(index, value) => { if let Some(shortcut) = self.prompt.shortcuts.get_mut(index) { shortcut.trigger = value; } } } Task::none() } fn style(&self) -> Option { Some(self.palette.application_style(if self.material.is_some() { cosmic::iced::Color::TRANSPARENT } else { self.background })) } fn view(&self) -> Element<'_, Message> { let text = |value: String| text(value, self.font); let mut details = column![text(self.prompt.description.clone())].spacing(12); for (index, shortcut) in self.prompt.shortcuts.iter().enumerate() { details = details.push( column![ text(shortcut.description.clone()), text_input("Ctrl+Alt+k (leave empty to disable)", &shortcut.trigger) .font(self.font) .style(controls::settings_input(self.palette)) .on_input(move |value| Message::Shortcut(index, value)) ] .spacing(6), ); } let mut content = column![ text(self.prompt.title.clone()).size(22), scrollable(details).height(Length::Fill) ] .spacing(16); if let Some(path) = &self.prompt.image { content = content.push( image(image::Handle::from_path(path)) .height(Length::Fill) .width(Length::Fill) .content_fit(cosmic::iced::ContentFit::Contain), ); } content = content.push( row![ button::custom(text("Cancel".into())) .class(cosmic::theme::Button::Text) .on_press(Message::Cancel), button::custom(text(self.prompt.accept.clone())) .class(accent_button()) .on_press(Message::Accept) ] .spacing(12) .align_y(Alignment::Center), ); container(content).padding(24).width(Length::Fill).into() } }