use std::sync::LazyLock;
const TIPS_TOML: &str = concat!(
include_str!("templates/tips/getting-started.toml"),
include_str!("templates/tips/tui-navigation.toml"),
include_str!("templates/tips/the-board.toml"),
include_str!("templates/tips/the-detail-pane.toml"),
include_str!("templates/tips/relationships-and-parents.toml"),
include_str!("templates/tips/cycles-milestones-rollover.toml"),
include_str!("templates/tips/cycle-schedules.toml"),
include_str!("templates/tips/recurring-issues.toml"),
include_str!("templates/tips/triage-inbox.toml"),
include_str!("templates/tips/projects-epics-roadmap.toml"),
include_str!("templates/tips/health-and-stats.toml"),
include_str!("templates/tips/saved-views-and-filters.toml"),
include_str!("templates/tips/templates.toml"),
include_str!("templates/tips/work-in-progress-limits.toml"),
include_str!("templates/tips/workflow-categories.toml"),
include_str!("templates/tips/batch-editing.toml"),
include_str!("templates/tips/archiving.toml"),
include_str!("templates/tips/external-sync.toml"),
include_str!("templates/tips/mcp-and-agents.toml"),
include_str!("templates/tips/configuration-and-storage.toml"),
include_str!("templates/tips/about-this-dialog.toml"),
);
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
struct TipsFile {
#[serde(default)]
categories: Vec<Category>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
struct Category {
name: String,
#[serde(default)]
tips: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tip {
pub category: String,
pub text: String,
}
static TIPS: LazyLock<Vec<Tip>> = LazyLock::new(|| parse(TIPS_TOML).unwrap_or_default());
#[must_use]
pub fn all() -> &'static [Tip] {
&TIPS
}
pub fn parse(text: &str) -> std::result::Result<Vec<Tip>, String> {
let file: TipsFile = toml::from_str(text).map_err(|e| e.to_string())?;
let mut tips = Vec::new();
for category in file.categories {
for text in category.tips {
tips.push(Tip {
category: category.name.clone(),
text,
});
}
}
Ok(tips)
}
#[must_use]
pub fn random_index(len: usize, seed: u128) -> usize {
if len == 0 {
return 0;
}
let mut x = seed as u64;
x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
x ^= x >> 31;
(x % len as u64) as usize
}
#[must_use]
pub fn random_start(len: usize) -> usize {
let seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
random_index(len, seed)
}
#[cfg(test)]
mod tests {
#[allow(clippy::wildcard_imports)]
use super::*;
#[test]
fn embedded_tips_are_well_formed_and_plentiful() {
let tips = parse(TIPS_TOML).expect("tips.toml must parse");
assert!(
tips.len() >= 100,
"tix #1632 asks for at least 100 tips, found {}",
tips.len()
);
let categories: std::collections::BTreeSet<&str> =
tips.iter().map(|t| t.category.as_str()).collect();
assert!(
categories.len() >= 10,
"the tips must be categorized, found {} categories",
categories.len()
);
for tip in &tips {
assert!(!tip.text.trim().is_empty(), "a tip is blank");
assert!(
!tip.category.trim().is_empty(),
"a tip (\"{}\") has no category",
tip.text
);
}
let unique: std::collections::BTreeSet<&str> =
tips.iter().map(|t| t.text.as_str()).collect();
assert_eq!(unique.len(), tips.len(), "a tip is duplicated");
}
#[test]
fn all_is_populated_from_the_embedded_file() {
assert_eq!(all().len(), parse(TIPS_TOML).unwrap().len());
}
#[test]
fn parse_rejects_a_malformed_file() {
assert!(parse("categories = 3").is_err());
}
#[test]
fn an_empty_category_list_yields_no_tips() {
assert_eq!(parse("").unwrap(), [] as [crate::tips::Tip; 0]);
}
#[test]
fn random_index_is_in_range_and_total_at_zero() {
assert_eq!(random_index(0, 12345), 0);
for seed in 0..1000u128 {
let index = random_index(7, seed);
assert!(index < 7, "index {index} out of range");
}
assert!(random_start(10) < 10);
}
#[test]
fn random_index_spreads_its_seeds() {
let bucket: std::collections::BTreeSet<usize> =
(0..64u128).map(|seed| random_index(16, seed)).collect();
assert!(
bucket.len() > 4,
"the seed mix is not spreading: {bucket:?}"
);
}
}