use std::time::{Duration, Instant};
use serde::Deserialize;
use crate::TextSnippet;
#[derive(Debug, Clone)]
pub(crate) struct CalculatorResult {
pub expression: String,
pub result: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InlineProvider {
Calculator,
Currency,
Units,
Color,
Time,
Uuid,
Password,
}
#[derive(Debug, Clone)]
pub(crate) struct InlineRow {
pub title: String,
pub subtitle: Option<String>,
pub copy_value: String,
pub alt_copy_value: Option<String>,
pub swatch: Option<iced::Color>,
pub preview: Option<super::preview::PreviewData>,
}
#[derive(Debug, Clone)]
pub(crate) struct InlineResult {
pub provider: InlineProvider,
pub rows: Vec<InlineRow>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct CurrencyConversionRequest {
pub amount: f64,
pub from_currency: String,
pub to_currency: String,
}
#[derive(Debug, Clone)]
pub(crate) struct CurrencyResult {
pub request: CurrencyConversionRequest,
pub converted_amount: f64,
pub rate: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct MultiCurrencyRequest {
pub amount: f64,
pub from_currency: String,
pub to_currencies: Vec<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct MultiCurrencyResult {
pub amount: f64,
pub from_currency: String,
pub conversions: Vec<CurrencyConversion>,
}
#[derive(Debug, Clone)]
pub(crate) struct CurrencyConversion {
pub to_currency: String,
pub converted_amount: f64,
pub rate: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct ScriptFilterIcon {
pub path: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)]
pub(crate) struct ScriptFilterItemText {
pub copy: Option<String>,
pub largetype: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum RawPreviewImage {
Path(String),
Source {
path: Option<String>,
base64: Option<String>,
},
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
enum RawPreviewSection {
Text {
heading: Option<String>,
content: String,
},
Image {
path: Option<String>,
base64: Option<String>,
},
File {
path: String,
},
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub(crate) struct RawPreviewSpec {
title: Option<String>,
subtitle: Option<String>,
badge: Option<String>,
icon: Option<String>,
text: Option<String>,
image: Option<RawPreviewImage>,
sections: Option<Vec<RawPreviewSection>>,
metadata: Vec<(String, String)>,
refresh: Option<f64>,
dynamic: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ScriptFilterPreviewError {
Run(String),
Invalid(String),
}
impl ScriptFilterPreviewError {
pub(crate) fn into_message(self) -> String {
match self {
Self::Run(message) | Self::Invalid(message) => message,
}
}
}
pub(crate) const MIN_PREVIEW_REFRESH: Duration = Duration::from_millis(500);
const MAX_PREVIEW_REFRESH_SECS: f64 = 3600.0;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PreparedPreviewBody {
Text {
heading: Option<String>,
content: String,
},
Image(String),
ImageData(iced::widget::image::Handle),
File(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct PreparedPreview {
pub title: Option<String>,
pub subtitle: Option<String>,
pub badge: Option<String>,
pub icon: Option<String>,
pub bodies: Vec<PreparedPreviewBody>,
pub replaces_body: bool,
pub metadata: Vec<(String, String)>,
pub refresh: Option<Duration>,
pub dynamic: bool,
}
impl PreparedPreview {
pub fn file_path(&self) -> Option<&str> {
self.bodies.iter().find_map(|body| match body {
PreparedPreviewBody::File(path) => Some(path.as_str()),
_ => None,
})
}
}
fn prepare_image(path: Option<String>, data: Option<String>) -> Option<PreparedPreviewBody> {
use base64::Engine;
if let Some(data) = data {
let encoded = data
.split_once(";base64,")
.map_or(data.as_str(), |(_, rest)| rest);
let cleaned: String = encoded.chars().filter(|c| !c.is_whitespace()).collect();
return Some(
match base64::engine::general_purpose::STANDARD.decode(cleaned) {
Ok(bytes) => {
PreparedPreviewBody::ImageData(iced::widget::image::Handle::from_bytes(bytes))
}
Err(error) => PreparedPreviewBody::Text {
heading: Some("Image".to_string()),
content: format!("Invalid base64 image: {error}"),
},
},
);
}
path.map(PreparedPreviewBody::Image)
}
impl From<RawPreviewSpec> for PreparedPreview {
fn from(raw: RawPreviewSpec) -> Self {
let replaces_body = raw.sections.is_some() || raw.text.is_some() || raw.image.is_some();
let bodies = if let Some(sections) = raw.sections {
sections
.into_iter()
.filter_map(|section| match section {
RawPreviewSection::Text { heading, content } => {
Some(PreparedPreviewBody::Text { heading, content })
}
RawPreviewSection::Image { path, base64 } => prepare_image(path, base64),
RawPreviewSection::File { path } => Some(PreparedPreviewBody::File(path)),
})
.collect()
} else {
let image = raw.image.and_then(|image| match image {
RawPreviewImage::Path(path) => prepare_image(Some(path), None),
RawPreviewImage::Source { path, base64 } => prepare_image(path, base64),
});
let text = raw.text.map(|content| PreparedPreviewBody::Text {
heading: None,
content,
});
image.into_iter().chain(text).collect()
};
Self {
title: raw.title,
subtitle: raw.subtitle,
badge: raw.badge,
icon: raw.icon,
bodies,
replaces_body,
metadata: raw.metadata,
refresh: raw
.refresh
.filter(|secs| secs.is_finite() && *secs > 0.0)
.map(|secs| {
Duration::from_secs_f64(secs.min(MAX_PREVIEW_REFRESH_SECS))
.max(MIN_PREVIEW_REFRESH)
}),
dynamic: raw.dynamic.unwrap_or(true),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum RawItemPreview {
Text(String),
Rich(Box<RawPreviewSpec>),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "RawItemPreview")]
pub(crate) enum ScriptFilterPreview {
Text(String),
Rich(Box<PreparedPreview>),
}
impl From<RawItemPreview> for ScriptFilterPreview {
fn from(raw: RawItemPreview) -> Self {
match raw {
RawItemPreview::Text(text) => Self::Text(text),
RawItemPreview::Rich(spec) => Self::Rich(Box::new((*spec).into())),
}
}
}
impl ScriptFilterPreview {
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(text) => Some(text),
Self::Rich(_) => None,
}
}
pub fn as_rich(&self) -> Option<&PreparedPreview> {
match self {
Self::Text(_) => None,
Self::Rich(preview) => Some(preview),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct ScriptFilterItem {
pub title: String,
#[serde(default = "crate::default_true")]
pub valid: bool,
pub subtitle: Option<String>,
pub arg: Option<String>,
pub icon: Option<ScriptFilterIcon>,
#[serde(default)]
pub indent: u32,
#[serde(default)]
pub preview: Option<ScriptFilterPreview>,
#[serde(default)]
pub variables: std::collections::HashMap<String, String>,
#[serde(default)]
pub quicklookurl: Option<String>,
#[serde(default)]
pub text: Option<ScriptFilterItemText>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct DynamicPreview {
pub preview: Option<PreparedPreview>,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct ScriptFilterResult {
pub items: Vec<ScriptFilterItem>,
pub default_icon: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct FileBrowserEntry {
pub name: String,
pub full_path: String,
pub is_dir: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct WebSearchActiveState {
pub name: String,
pub query: String,
pub url_template: String,
pub icon: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct CachedRate {
pub rate: f64,
pub timestamp: Instant,
}
impl CachedRate {
const TTL: Duration = Duration::from_secs(3600);
pub(crate) fn new(rate: f64) -> Self {
Self {
rate,
timestamp: Instant::now(),
}
}
pub(crate) fn is_valid(&self) -> bool {
self.timestamp.elapsed() < Self::TTL
}
}
#[derive(Debug, Clone)]
pub(crate) struct EmojiEntry {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone)]
pub(crate) enum Message {
SearchChanged(String),
MoveUp,
MoveDown,
Submit,
SubmitRepeat,
Cancel,
ItemClicked(usize),
InlineSelected(usize),
InlineCopied(u64, std::result::Result<(), String>),
CurrencyConversionResult(
CurrencyConversionRequest,
std::result::Result<CurrencyResult, String>,
),
CurrencyResultCopied,
MultiCurrencyConversionResult(
MultiCurrencyRequest,
std::result::Result<MultiCurrencyResult, String>,
),
MultiCurrencyResultCopied(usize),
ScriptFilterResult(u64, std::result::Result<ScriptFilterResult, String>),
ScriptFilterItemSelected(usize),
ScriptFilterPreviewDue(u64, usize),
ScriptFilterPreview(
u64,
usize,
usize,
std::result::Result<Option<PreparedPreview>, ScriptFilterPreviewError>,
),
FileBrowserPreviewDue(String),
FileBrowserPreview(
super::file_preview::FilePreviewKey,
super::file_preview::FilePreviewResult,
),
TextSnippetCommandResult(u64, std::result::Result<Vec<TextSnippet>, String>),
TextSnippetSelected(usize),
TextSnippetClipboard(u64, ClipboardData),
TextSnippetExpanded(
u64,
std::result::Result<super::placeholders::Expanded, String>,
),
HyprPastePrepared(u64, std::result::Result<super::hyprland::Hyprland, String>),
WebSearchSelected,
FileBrowserItemSelected(usize),
FileBrowserRowPressed {
index: usize,
path: String,
},
FileBrowserRowReleased(usize),
FileBrowserHover(usize),
FileBrowserUnhover(usize),
MouseReleased,
WaylandHandles(Option<super::dnd::RawWaylandHandles>),
DragEvent(super::dnd::DragEvent),
TabComplete,
MainViewCycle(bool),
FileBrowserToggleHidden,
FileBrowserCopyPath,
FileBrowserCopyImage,
FileBrowserCopyCompleted(String, std::result::Result<(), String>),
FileBrowserOpenSelected,
FileBrowserOpenFileManager,
FileBrowserCycleSort,
FileBrowserCycleShow,
ModifiersChanged(iced::keyboard::Modifiers),
HistoryPrevious,
HistoryNext,
ToggleHelp,
TogglePreview,
PreviewScrollDown,
PreviewScrollUp,
WindowResized(iced::Size),
HelpEntrySelected(usize),
EmojiSelected(usize),
EmojiDataLoaded(Vec<EmojiEntry>),
CommandListSelected(usize),
CommandListLoaded(
u64,
std::result::Result<Vec<super::command_list::CommandItem>, String>,
),
CommandListActionDone(
super::command_list::CommandListAddon,
u64,
String,
std::result::Result<(), String>,
),
FaviconsFetched(u64, bool),
TabPreviewLoaded(u64, String, Option<Vec<super::preview::PreviewBody>>),
SlackSearchDue(u64, String),
SlackPreviewLoaded(u64, String, Option<super::slack::Preview>),
MainViewLoaded(
usize,
u64,
std::result::Result<Vec<super::command_list::CommandItem>, String>,
),
MainViewSnippetsLoaded(usize, std::result::Result<Vec<TextSnippet>, String>),
MainViewScriptFilter(
usize,
u64,
Option<std::result::Result<ScriptFilterResult, String>>,
),
DevtoolsSelected(usize),
DevtoolsHashes(u64, Vec<super::devtools::DevRow>),
FallbackSelected(usize),
KeywordSuggestionSelected(usize),
ClipboardPage(u64, std::result::Result<super::clipboard::Page, String>),
ClipboardPreview(
u64,
String,
std::result::Result<super::clipboard::ClipPreview, String>,
),
ClipboardSelected(usize),
ClipboardActionDone(
u64,
std::result::Result<Option<super::hyprland::Hyprland>, String>,
),
ClipboardRefresh,
ClipboardToggleImages,
ClipboardDelete,
ClipboardDeleteDone(u64, std::result::Result<String, String>),
PromptChanged(crate::secret::Secret),
VaultDone(u64, super::vault::Report),
VaultSearchDone(u64, std::collections::HashSet<String>),
VaultShortcut(super::vault::EntryAction),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct ClipboardData {
pub current: Option<std::result::Result<String, String>>,
pub history: std::collections::BTreeMap<usize, std::result::Result<String, String>>,
}
impl ClipboardData {
pub fn merge(&mut self, other: ClipboardData) {
if other.current.is_some() {
self.current = other.current;
}
self.history.extend(other.history);
}
}