Merge branch 'main' into main

This commit is contained in:
github-actions[bot]
2026-04-29 17:40:28 +00:00
committed by GitHub
43 changed files with 714 additions and 134 deletions

View File

@@ -6,6 +6,7 @@ const APP_CONFIG_DIR: &str = "com.tolaria.app";
const LEGACY_APP_CONFIG_DIR: &str = "com.laputa.app";
const SUPPORTED_DEFAULT_AI_AGENTS: &[&str] = &["claude_code", "codex", "opencode", "pi", "gemini"];
pub const DEFAULT_HIDE_GITIGNORED_FILES: bool = true;
const SUPPORTED_NOTE_WIDTH_MODES: &[&str] = &["normal", "wide"];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Settings {
@@ -21,6 +22,7 @@ pub struct Settings {
pub release_channel: Option<String>,
pub theme_mode: Option<String>,
pub ui_language: Option<String>,
pub note_width_mode: Option<String>,
pub initial_h1_auto_rename_enabled: Option<bool>,
pub default_ai_agent: Option<String>,
pub hide_gitignored_files: Option<bool>,
@@ -65,6 +67,13 @@ pub fn normalize_theme_mode(value: Option<&str>) -> Option<String> {
}
}
pub fn normalize_note_width_mode(value: Option<&str>) -> Option<String> {
match value.map(|candidate| candidate.trim().to_ascii_lowercase()) {
Some(mode) if SUPPORTED_NOTE_WIDTH_MODES.contains(&mode.as_str()) => Some(mode),
_ => None,
}
}
pub fn should_hide_gitignored_files(settings: &Settings) -> bool {
settings
.hide_gitignored_files
@@ -123,6 +132,7 @@ fn normalize_settings(settings: Settings) -> Settings {
release_channel: normalize_release_channel(settings.release_channel.as_deref()),
theme_mode: normalize_theme_mode(settings.theme_mode.as_deref()),
ui_language: normalize_ui_language(settings.ui_language.as_deref()),
note_width_mode: normalize_note_width_mode(settings.note_width_mode.as_deref()),
initial_h1_auto_rename_enabled: settings.initial_h1_auto_rename_enabled,
default_ai_agent: normalize_default_ai_agent(settings.default_ai_agent.as_deref()),
hide_gitignored_files: settings.hide_gitignored_files,
@@ -266,6 +276,7 @@ mod tests {
release_channel: Some("alpha".to_string()),
theme_mode: Some("dark".to_string()),
ui_language: Some("zh-Hans".to_string()),
note_width_mode: Some("wide".to_string()),
initial_h1_auto_rename_enabled: Some(false),
default_ai_agent: Some("codex".to_string()),
hide_gitignored_files: Some(false),
@@ -294,6 +305,7 @@ mod tests {
release_channel: Some("alpha".to_string()),
theme_mode: Some("dark".to_string()),
ui_language: Some("zh-Hans".to_string()),
note_width_mode: Some("wide".to_string()),
initial_h1_auto_rename_enabled: Some(false),
default_ai_agent: Some("codex".to_string()),
hide_gitignored_files: Some(false),
@@ -307,6 +319,7 @@ mod tests {
assert_eq!(loaded.release_channel.as_deref(), Some("alpha"));
assert_eq!(loaded.theme_mode.as_deref(), Some("dark"));
assert_eq!(loaded.ui_language.as_deref(), Some("zh-Hans"));
assert_eq!(loaded.note_width_mode.as_deref(), Some("wide"));
assert_eq!(loaded.initial_h1_auto_rename_enabled, Some(false));
assert_eq!(loaded.default_ai_agent.as_deref(), Some("codex"));
assert_eq!(loaded.hide_gitignored_files, Some(false));
@@ -332,6 +345,7 @@ mod tests {
release_channel: Some(" alpha ".to_string()),
theme_mode: Some(" dark ".to_string()),
ui_language: Some(" zh-cn ".to_string()),
note_width_mode: Some(" WIDE ".to_string()),
default_ai_agent: Some(" codex ".to_string()),
..Default::default()
});
@@ -339,6 +353,7 @@ mod tests {
assert_eq!(loaded.release_channel.as_deref(), Some("alpha"));
assert_eq!(loaded.theme_mode.as_deref(), Some("dark"));
assert_eq!(loaded.ui_language.as_deref(), Some("zh-Hans"));
assert_eq!(loaded.note_width_mode.as_deref(), Some("wide"));
assert_eq!(loaded.default_ai_agent.as_deref(), Some("codex"));
}
@@ -416,6 +431,15 @@ mod tests {
assert!(loaded.theme_mode.is_none());
}
#[test]
fn test_invalid_note_width_mode_is_filtered() {
let loaded = save_and_reload(Settings {
note_width_mode: Some("expanded".to_string()),
..Default::default()
});
assert!(loaded.note_width_mode.is_none());
}
#[test]
fn test_invalid_ui_language_is_filtered() {
let loaded = save_and_reload(Settings {

View File

@@ -54,6 +54,9 @@ pub struct VaultEntry {
/// Default view mode for the note list when viewing instances of this Type.
/// Stored as a string: "all", "editor-list", or "editor-only".
pub view: Option<String>,
/// Rich-editor width mode for this note. None means use the app default.
#[serde(rename = "noteWidth")]
pub note_width: Option<String>,
/// Whether this Type is visible in the sidebar. Defaults to true when absent.
pub visible: Option<bool>,
/// Whether this note has been explicitly organized (removed from Inbox).

View File

@@ -40,6 +40,8 @@ pub(crate) struct Frontmatter {
pub sort: Option<StringOrList>,
#[serde(default)]
pub view: Option<StringOrList>,
#[serde(rename = "_width", alias = "width", default)]
pub note_width: Option<StringOrList>,
#[serde(default)]
pub visible: Option<bool>,
#[serde(
@@ -203,6 +205,8 @@ fn parse_frontmatter(data: &HashMap<String, serde_json::Value>) -> Frontmatter {
"_sort",
"sort",
"view",
"_width",
"width",
"visible",
"notion_id",
"Status",
@@ -238,6 +242,8 @@ const SKIP_KEYS: &[&str] = &[
"template",
"sort",
"view",
"_width",
"width",
"visible",
"status",
"_organized",
@@ -332,6 +338,16 @@ pub(crate) fn resolve_is_a(fm_is_a: Option<StringOrList>) -> Option<String> {
fm_is_a.and_then(|a| a.into_vec().into_iter().next())
}
pub(crate) fn resolve_note_width(note_width: Option<StringOrList>) -> Option<String> {
match note_width
.and_then(StringOrList::into_scalar)
.map(|value| value.trim().to_ascii_lowercase())
{
Some(mode) if mode == "normal" || mode == "wide" => Some(mode),
_ => None,
}
}
/// Convert gray_matter::Pod to serde_json::Value
fn pod_to_json(pod: gray_matter::Pod) -> serde_json::Value {
match pod {

View File

@@ -41,7 +41,7 @@ pub use views::{
};
use file::read_file_metadata;
use frontmatter::{extract_fm_and_rels, resolve_is_a};
use frontmatter::{extract_fm_and_rels, resolve_is_a, resolve_note_width};
use parsing::{count_body_words, extract_outgoing_links, extract_snippet, extract_title};
use gray_matter::engine::YAML;
@@ -151,6 +151,7 @@ pub fn parse_md_file(path: &Path, git_dates: Option<(u64, u64)>) -> Result<Vault
template: frontmatter.template.and_then(|v| v.into_scalar()),
sort: frontmatter.sort.and_then(|v| v.into_scalar()),
view: frontmatter.view.and_then(|v| v.into_scalar()),
note_width: resolve_note_width(frontmatter.note_width),
visible: frontmatter.visible,
organized: frontmatter.organized.unwrap_or(false),
favorite: frontmatter.favorite.unwrap_or(false),

View File

@@ -71,3 +71,29 @@ fn ignores_unknown_underscore_keys_in_properties_and_relationships() {
Some("Luca")
);
}
#[test]
fn parses_note_width_without_property_leaks() {
let dir = TempDir::new().unwrap();
let entry = parse_test_entry(
&dir,
"note.md",
"---\ntype: Note\n_width: wide\n---\n# Note\n",
);
assert_eq!(entry.note_width.as_deref(), Some("wide"));
assert!(!entry.properties.contains_key("_width"));
assert!(!entry.relationships.contains_key("_width"));
}
#[test]
fn ignores_invalid_note_width_modes() {
let dir = TempDir::new().unwrap();
let entry = parse_test_entry(
&dir,
"note.md",
"---\ntype: Note\n_width: expanded\n---\n# Note\n",
);
assert_eq!(entry.note_width, None);
}