treewide: replace std hashers with rustc_hash alternatives; fix clippy

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I766c36cb53d3d7f9e85b91a67c4131a66a6a6964
This commit is contained in:
raf 2026-03-19 22:34:30 +03:00
commit c6efd3661f
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
53 changed files with 343 additions and 394 deletions

View file

@ -28,6 +28,7 @@ gloo-timers = { workspace = true }
rand = { workspace = true }
urlencoding = { workspace = true }
pinakes-plugin-api = { workspace = true }
rustc-hash = { workspace = true }
[lints]
workspace = true

View file

@ -1,7 +1,6 @@
use std::collections::HashMap;
use anyhow::Result;
use reqwest::{Client, header};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
/// Payload for import events: (path, tag_ids, new_tags, collection_id)
@ -66,7 +65,7 @@ pub struct MediaResponse {
pub description: Option<String>,
#[serde(default)]
pub has_thumbnail: bool,
pub custom_fields: HashMap<String, CustomFieldResponse>,
pub custom_fields: FxHashMap<String, CustomFieldResponse>,
pub created_at: String,
pub updated_at: String,
#[serde(default)]
@ -395,7 +394,7 @@ pub struct BookMetadataResponse {
pub format: Option<String>,
pub authors: Vec<BookAuthorResponse>,
#[serde(default)]
pub identifiers: HashMap<String, Vec<String>>,
pub identifiers: FxHashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
@ -1680,7 +1679,7 @@ impl ApiClient {
/// Returns a map of CSS property names to values.
pub async fn get_plugin_ui_theme_extensions(
&self,
) -> Result<HashMap<String, String>> {
) -> Result<FxHashMap<String, String>> {
Ok(
self
.client

View file

@ -1,9 +1,8 @@
//! Graph visualization component for markdown note connections.
//!
//! Renders a force-directed graph showing connections between notes.
use std::collections::HashMap;
use dioxus::prelude::*;
use rustc_hash::FxHashMap;
use crate::client::{
ApiClient,
@ -298,7 +297,7 @@ fn ForceDirectedGraph(
// Create id to position map
let nodes_read = physics_nodes.read();
let id_to_pos: HashMap<&str, (f64, f64)> = nodes_read
let id_to_pos: FxHashMap<&str, (f64, f64)> = nodes_read
.iter()
.map(|n| (n.id.as_str(), (n.x, n.y)))
.collect();

View file

@ -1,6 +1,5 @@
use std::collections::HashSet;
use dioxus::prelude::*;
use rustc_hash::FxHashSet;
use super::utils::{format_size, type_badge_class};
use crate::client::{
@ -50,7 +49,7 @@ pub fn Import(
let mut filter_max_size = use_signal(|| 0u64); // 0 means no limit
// File selection state
let mut selected_file_paths = use_signal(HashSet::<String>::new);
let mut selected_file_paths = use_signal(FxHashSet::<String>::default);
let current_mode = *import_mode.read();
@ -475,7 +474,7 @@ pub fn Import(
button {
class: "btn btn-sm btn-ghost",
onclick: move |_| {
selected_file_paths.set(HashSet::new());
selected_file_paths.set(FxHashSet::default());
},
"Deselect All"
}
@ -496,12 +495,12 @@ pub fn Import(
let filtered_paths = filtered_paths.clone();
move |_| {
if all_filtered_selected {
let filtered_set: HashSet<String> = filtered_paths
let filtered_set: FxHashSet<String> = filtered_paths
.iter()
.cloned()
.collect();
let sel = selected_file_paths.read().clone();
let new_sel: HashSet<String> = sel
let new_sel: FxHashSet<String> = sel
.difference(&filtered_set)
.cloned()
.collect();
@ -599,7 +598,7 @@ pub fn Import(
let new_tags = parse_new_tags(&new_tags_input.read());
let col_id = selected_collection.read().clone();
on_import_batch.call((paths, tag_ids, new_tags, col_id));
selected_file_paths.set(HashSet::new());
selected_file_paths.set(FxHashSet::default());
selected_tags.set(Vec::new());
new_tags_input.set(String::new());
selected_collection.set(None);
@ -644,7 +643,7 @@ pub fn Import(
selected_tags.set(Vec::new());
new_tags_input.set(String::new());
selected_collection.set(None);
selected_file_paths.set(HashSet::new());
selected_file_paths.set(FxHashSet::default());
}
}
},

View file

@ -316,6 +316,10 @@ fn escape_html_attr(text: &str) -> String {
/// Sanitize HTML using ammonia with a safe allowlist.
/// This prevents XSS attacks by removing dangerous elements and attributes.
#[expect(
clippy::disallowed_types,
reason = "ammonia::Builder requires std HashSet"
)]
fn sanitize_html(html: &str) -> String {
use std::collections::HashSet;

View file

@ -3,8 +3,6 @@
//! This module provides the action execution system that handles
//! user interactions with plugin UI elements.
use std::collections::HashMap;
use pinakes_plugin_api::{
ActionDefinition,
ActionRef,
@ -12,6 +10,7 @@ use pinakes_plugin_api::{
SpecialAction,
UiElement,
};
use rustc_hash::FxHashMap;
use super::data::to_reqwest_method;
use crate::client::ApiClient;
@ -48,7 +47,7 @@ pub enum ActionResult {
pub async fn execute_action(
client: &ApiClient,
action_ref: &ActionRef,
page_actions: &HashMap<String, ActionDefinition>,
page_actions: &FxHashMap<String, ActionDefinition>,
form_data: Option<&serde_json::Value>,
) -> Result<ActionResult, String> {
match action_ref {
@ -224,9 +223,10 @@ mod tests {
async fn test_named_action_unknown_returns_none() {
let client = crate::client::ApiClient::default();
let action_ref = ActionRef::Name("my-action".to_string());
let result = execute_action(&client, &action_ref, &HashMap::new(), None)
.await
.unwrap();
let result =
execute_action(&client, &action_ref, &FxHashMap::default(), None)
.await
.unwrap();
assert!(matches!(result, ActionResult::None));
}
@ -235,11 +235,11 @@ mod tests {
use pinakes_plugin_api::ActionDefinition;
let client = crate::client::ApiClient::default();
let mut page_actions = HashMap::new();
let mut page_actions = FxHashMap::default();
page_actions.insert("do-thing".to_string(), ActionDefinition {
method: pinakes_plugin_api::HttpMethod::Post,
path: "/api/v1/nonexistent-endpoint".to_string(),
params: HashMap::new(),
params: FxHashMap::default(),
success_message: None,
error_message: None,
navigate_to: None,
@ -267,9 +267,10 @@ mod tests {
let client = crate::client::ApiClient::default();
let action_ref = ActionRef::Special(SpecialAction::Refresh);
let result = execute_action(&client, &action_ref, &HashMap::new(), None)
.await
.unwrap();
let result =
execute_action(&client, &action_ref, &FxHashMap::default(), None)
.await
.unwrap();
assert!(matches!(result, ActionResult::Refresh));
}
@ -281,9 +282,10 @@ mod tests {
let action_ref = ActionRef::Special(SpecialAction::Navigate {
to: "/dashboard".to_string(),
});
let result = execute_action(&client, &action_ref, &HashMap::new(), None)
.await
.unwrap();
let result =
execute_action(&client, &action_ref, &FxHashMap::default(), None)
.await
.unwrap();
assert!(
matches!(result, ActionResult::Navigate(ref p) if p == "/dashboard")
);
@ -299,9 +301,10 @@ mod tests {
key: "count".to_string(),
value: expr.clone(),
});
let result = execute_action(&client, &action_ref, &HashMap::new(), None)
.await
.unwrap();
let result =
execute_action(&client, &action_ref, &FxHashMap::default(), None)
.await
.unwrap();
match result {
ActionResult::UpdateState { key, value_expr } => {
assert_eq!(key, "count");
@ -317,9 +320,10 @@ mod tests {
let client = crate::client::ApiClient::default();
let action_ref = ActionRef::Special(SpecialAction::CloseModal);
let result = execute_action(&client, &action_ref, &HashMap::new(), None)
.await
.unwrap();
let result =
execute_action(&client, &action_ref, &FxHashMap::default(), None)
.await
.unwrap();
assert!(matches!(result, ActionResult::CloseModal));
}
}

View file

@ -2,14 +2,12 @@
//!
//! Provides data fetching and caching for plugin data sources.
use std::{
collections::{HashMap, HashSet},
time::Duration,
};
use std::time::Duration;
use dioxus::prelude::*;
use dioxus_core::Task;
use pinakes_plugin_api::{DataSource, Expression, HttpMethod};
use rustc_hash::{FxHashMap, FxHashSet};
use super::expr::{evaluate_expression, value_to_display_string};
use crate::client::ApiClient;
@ -17,9 +15,9 @@ use crate::client::ApiClient;
/// Cached data for a plugin page
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PluginPageData {
data: HashMap<String, serde_json::Value>,
loading: HashSet<String>,
errors: HashMap<String, String>,
data: FxHashMap<String, serde_json::Value>,
loading: FxHashSet<String>,
errors: FxHashMap<String, String>,
}
impl PluginPageData {
@ -105,7 +103,7 @@ async fn fetch_endpoint(
client: &ApiClient,
path: &str,
method: HttpMethod,
params: &HashMap<String, Expression>,
params: &FxHashMap<String, Expression>,
ctx: &serde_json::Value,
allowed_endpoints: &[String],
) -> Result<serde_json::Value, String> {
@ -174,9 +172,9 @@ async fn fetch_endpoint(
/// Returns an error if any data source fails to fetch
pub async fn fetch_page_data(
client: &ApiClient,
data_sources: &HashMap<String, DataSource>,
data_sources: &FxHashMap<String, DataSource>,
allowed_endpoints: &[String],
) -> Result<HashMap<String, serde_json::Value>, String> {
) -> Result<FxHashMap<String, serde_json::Value>, String> {
// Group non-Transform sources into dedup groups.
//
// For Endpoint sources, two entries are in the same group when they share
@ -300,7 +298,7 @@ pub async fn fetch_page_data(
})
.collect();
let mut results: HashMap<String, serde_json::Value> = HashMap::new();
let mut results: FxHashMap<String, serde_json::Value> = FxHashMap::default();
for group_result in futures::future::join_all(futs).await {
for (name, value) in group_result? {
results.insert(name, value);
@ -375,7 +373,7 @@ pub async fn fetch_page_data(
/// immediate re-fetch outside of the polling interval.
pub fn use_plugin_data(
client: Signal<ApiClient>,
data_sources: HashMap<String, DataSource>,
data_sources: FxHashMap<String, DataSource>,
refresh: Signal<u32>,
allowed_endpoints: Vec<String>,
) -> Signal<PluginPageData> {
@ -564,7 +562,7 @@ mod tests {
use crate::client::ApiClient;
let client = ApiClient::default();
let mut sources = HashMap::new();
let mut sources = FxHashMap::default();
sources.insert("nums".to_string(), DataSource::Static {
value: serde_json::json!([1, 2, 3]),
});
@ -586,7 +584,7 @@ mod tests {
use crate::client::ApiClient;
let client = ApiClient::default();
let mut sources = HashMap::new();
let mut sources = FxHashMap::default();
// The Transform expression accesses "raw" from the context
sources.insert("derived".to_string(), DataSource::Transform {
source_name: "raw".to_string(),
@ -611,7 +609,7 @@ mod tests {
use crate::client::ApiClient;
let client = ApiClient::default();
let mut sources = HashMap::new();
let mut sources = FxHashMap::default();
sources.insert("raw".to_string(), DataSource::Static {
value: serde_json::json!(42),
});
@ -634,7 +632,7 @@ mod tests {
use crate::client::ApiClient;
let client = ApiClient::default();
let mut sources = HashMap::new();
let mut sources = FxHashMap::default();
// Two Static sources with the same payload; dedup is for Endpoint sources,
// but both names must appear in the output regardless.
sources.insert("a".to_string(), DataSource::Static {
@ -662,7 +660,7 @@ mod tests {
use crate::client::ApiClient;
let client = ApiClient::default();
let mut sources = HashMap::new();
let mut sources = FxHashMap::default();
// Two endpoints with identical (path, method, params=empty) but different
// transforms. Both should produce the same error when the path is blocked.
sources.insert("x".to_string(), DataSource::Endpoint {
@ -707,7 +705,7 @@ mod tests {
use crate::client::ApiClient;
let client = ApiClient::default();
let mut sources = HashMap::new();
let mut sources = FxHashMap::default();
sources.insert("raw_data".to_string(), DataSource::Static {
value: serde_json::json!({"count": 42, "name": "test"}),
});
@ -741,7 +739,7 @@ mod tests {
use crate::client::ApiClient;
let client = ApiClient::default();
let mut sources = HashMap::new();
let mut sources = FxHashMap::default();
sources.insert("items".to_string(), DataSource::Endpoint {
path: "/api/v1/media".to_string(),
method: HttpMethod::Get,

View file

@ -16,10 +16,9 @@
//! }
//! ```
use std::collections::HashMap;
use dioxus::prelude::*;
use pinakes_plugin_api::{UiPage, UiWidget};
use rustc_hash::FxHashMap;
use crate::client::ApiClient;
@ -43,11 +42,11 @@ pub struct PluginRegistry {
/// API client for fetching pages from server
client: ApiClient,
/// Cached pages: (`plugin_id`, `page_id`) -> `PluginPage`
pages: HashMap<(String, String), PluginPage>,
pages: FxHashMap<(String, String), PluginPage>,
/// Cached widgets: (`plugin_id`, `widget_id`) -> `UiWidget`
widgets: Vec<(String, UiWidget)>,
/// Merged CSS custom property overrides from all enabled plugins
theme_vars: HashMap<String, String>,
theme_vars: FxHashMap<String, String>,
}
impl PluginRegistry {
@ -55,14 +54,14 @@ impl PluginRegistry {
pub fn new(client: ApiClient) -> Self {
Self {
client,
pages: HashMap::new(),
pages: FxHashMap::default(),
widgets: Vec::new(),
theme_vars: HashMap::new(),
theme_vars: FxHashMap::default(),
}
}
/// Get merged CSS custom property overrides from all loaded plugins.
pub fn theme_vars(&self) -> &HashMap<String, String> {
pub fn theme_vars(&self) -> &FxHashMap<String, String> {
&self.theme_vars
}
@ -230,8 +229,8 @@ mod tests {
gap: 16,
padding: None,
},
data_sources: HashMap::new(),
actions: HashMap::new(),
data_sources: FxHashMap::default(),
actions: FxHashMap::default(),
}
}
@ -491,8 +490,8 @@ mod tests {
gap: 16,
padding: None,
},
data_sources: HashMap::new(),
actions: HashMap::new(),
data_sources: FxHashMap::default(),
actions: FxHashMap::default(),
};
registry.register_page("test-plugin".to_string(), invalid_page, vec![]);
@ -517,8 +516,8 @@ mod tests {
gap: 0,
padding: None,
},
data_sources: HashMap::new(),
actions: HashMap::new(),
data_sources: FxHashMap::default(),
actions: FxHashMap::default(),
};
registry.register_page("p".to_string(), invalid_page, vec![]);
assert_eq!(registry.all_pages().len(), 0);

View file

@ -4,8 +4,6 @@
//! elements. Data-driven elements resolve their data from a [`PluginPageData`]
//! context that is populated by the `use_plugin_data` hook.
use std::collections::HashMap;
use dioxus::prelude::*;
use pinakes_plugin_api::{
ActionDefinition,
@ -23,6 +21,7 @@ use pinakes_plugin_api::{
UiElement,
UiPage,
};
use rustc_hash::{FxHashMap, FxHashSet};
use super::{
actions::execute_action,
@ -49,13 +48,13 @@ pub struct RenderContext {
pub navigate: Signal<Option<String>>,
pub refresh: Signal<u32>,
pub modal: Signal<Option<UiElement>>,
pub local_state: Signal<HashMap<String, serde_json::Value>>,
pub local_state: Signal<FxHashMap<String, serde_json::Value>>,
}
/// Build the expression evaluation context from page data and local state.
fn build_ctx(
data: &PluginPageData,
local_state: &HashMap<String, serde_json::Value>,
local_state: &FxHashMap<String, serde_json::Value>,
) -> serde_json::Value {
let mut base = data.as_json();
if let serde_json::Value::Object(ref mut obj) = base {
@ -101,7 +100,7 @@ pub fn PluginViewRenderer(props: PluginViewProps) -> Element {
let mut navigate = use_signal(|| None::<String>);
let refresh = use_signal(|| 0u32);
let mut modal = use_signal(|| None::<UiElement>);
let local_state = use_signal(HashMap::<String, serde_json::Value>::new);
let local_state = use_signal(FxHashMap::<String, serde_json::Value>::default);
let ctx = RenderContext {
client: props.client,
feedback,
@ -169,7 +168,7 @@ struct PluginTabsProps {
tabs: Vec<TabDefinition>,
default_tab: usize,
data: PluginPageData,
actions: HashMap<String, ActionDefinition>,
actions: FxHashMap<String, ActionDefinition>,
ctx: RenderContext,
}
@ -232,7 +231,7 @@ struct PluginDataTableProps {
page_size: usize,
row_actions: Vec<pinakes_plugin_api::RowAction>,
data: PluginPageData,
actions: HashMap<String, ActionDefinition>,
actions: FxHashMap<String, ActionDefinition>,
ctx: RenderContext,
}
@ -472,7 +471,7 @@ fn PluginDataTable(props: PluginDataTableProps) -> Element {
pub fn render_element(
element: &UiElement,
data: &PluginPageData,
actions: &HashMap<String, ActionDefinition>,
actions: &FxHashMap<String, ActionDefinition>,
ctx: RenderContext,
) -> Element {
match element {
@ -1188,7 +1187,7 @@ fn render_chart_data(
Some(serde_json::Value::Array(arr)) if !arr.is_empty() => {
if arr.first().map(|v| v.is_object()).unwrap_or(false) {
// Object rows: collect unique keys preserving insertion order
let mut seen = std::collections::HashSet::new();
let mut seen = FxHashSet::default();
let cols: Vec<String> = arr
.iter()
.filter_map(|r| r.as_object())

View file

@ -4,10 +4,9 @@
//! predefined locations. Unlike full pages, widgets have no data sources of
//! their own and render with empty data context.
use std::collections::HashMap;
use dioxus::prelude::*;
use pinakes_plugin_api::{ActionDefinition, UiWidget, widget_location};
use rustc_hash::FxHashMap;
use super::{
data::PluginPageData,
@ -120,7 +119,7 @@ pub fn WidgetViewRenderer(props: WidgetViewRendererProps) -> Element {
let navigate = use_signal(|| None::<String>);
let refresh = use_signal(|| 0u32);
let modal = use_signal(|| None::<pinakes_plugin_api::UiElement>);
let local_state = use_signal(HashMap::<String, serde_json::Value>::new);
let local_state = use_signal(FxHashMap::<String, serde_json::Value>::default);
let ctx = RenderContext {
client: props.client,
feedback,
@ -129,7 +128,7 @@ pub fn WidgetViewRenderer(props: WidgetViewRendererProps) -> Element {
modal,
local_state,
};
let empty_actions: HashMap<String, ActionDefinition> = HashMap::new();
let empty_actions: FxHashMap<String, ActionDefinition> = FxHashMap::default();
rsx! {
div {
class: "plugin-widget",
@ -142,6 +141,8 @@ pub fn WidgetViewRenderer(props: WidgetViewRendererProps) -> Element {
#[cfg(test)]
mod tests {
use rustc_hash::FxHashSet;
use super::*;
#[test]
@ -159,7 +160,7 @@ mod tests {
WidgetLocation::SettingsSection,
];
let strings: Vec<&str> = locations.iter().map(|l| l.as_str()).collect();
let unique: std::collections::HashSet<_> = strings.iter().collect();
let unique: FxHashSet<_> = strings.iter().collect();
assert_eq!(
strings.len(),
unique.len(),