Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 110 additions & 10 deletions wezterm-font/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,85 @@ impl FontConfigInner {
Ok((handles, loaded))
}

/// Returns the download URL for well-known programming fonts,
/// or `None` for unrecognised font names.
fn known_font_url(font_name: &str) -> Option<&'static str> {
let lower = font_name.to_lowercase();
if lower.contains("jetbrains") {
Some("https://github.com/JetBrains/JetBrainsMono")
} else if lower.contains("fira") {
Some("https://github.com/tonsky/FiraCode")
} else if lower.contains("cascadia") {
Some("https://github.com/microsoft/cascadia-code")
} else if lower.contains("source code") {
Some("https://github.com/adobe-fonts/source-code-pro")
} else if lower.contains("hack") {
Some("https://github.com/source-foundry/Hack")
} else if lower.contains("iosevka") {
Some("https://github.com/be5invis/Iosevka")
} else if lower.contains("victor mono") {
Some("https://github.com/rubjo/victor-mono")
} else {
Comment on lines +802 to +818
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Match known fonts by normalized family name, not substring.

contains() can classify the wrong family here. A request for Fira Mono, for example, will currently get the Fira Code URL, so the remediation hint points users at the wrong font. Use a normalized allow-list of exact family names/aliases instead.

Proposed fix
     fn known_font_url(font_name: &str) -> Option<&'static str> {
-        let lower = font_name.to_lowercase();
-        if lower.contains("jetbrains") {
-            Some("https://github.com/JetBrains/JetBrainsMono")
-        } else if lower.contains("fira") {
-            Some("https://github.com/tonsky/FiraCode")
-        } else if lower.contains("cascadia") {
-            Some("https://github.com/microsoft/cascadia-code")
-        } else if lower.contains("source code") {
-            Some("https://github.com/adobe-fonts/source-code-pro")
-        } else if lower.contains("hack") {
-            Some("https://github.com/source-foundry/Hack")
-        } else if lower.contains("iosevka") {
-            Some("https://github.com/be5invis/Iosevka")
-        } else if lower.contains("victor mono") {
-            Some("https://github.com/rubjo/victor-mono")
-        } else {
-            None
-        }
+        match font_name.trim().to_ascii_lowercase().as_str() {
+            "jetbrains mono" | "jetbrainsmono" => {
+                Some("https://github.com/JetBrains/JetBrainsMono")
+            }
+            "fira code" | "firacode" => Some("https://github.com/tonsky/FiraCode"),
+            "cascadia code" | "cascadia" => {
+                Some("https://github.com/microsoft/cascadia-code")
+            }
+            "source code pro" | "source code" => {
+                Some("https://github.com/adobe-fonts/source-code-pro")
+            }
+            "hack" => Some("https://github.com/source-foundry/Hack"),
+            "iosevka" => Some("https://github.com/be5invis/Iosevka"),
+            "victor mono" => Some("https://github.com/rubjo/victor-mono"),
+            _ => None,
+        }
     }

As per coding guidelines, "Use allow-lists for input validation to prevent injection attacks".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn known_font_url(font_name: &str) -> Option<&'static str> {
let lower = font_name.to_lowercase();
if lower.contains("jetbrains") {
Some("https://github.com/JetBrains/JetBrainsMono")
} else if lower.contains("fira") {
Some("https://github.com/tonsky/FiraCode")
} else if lower.contains("cascadia") {
Some("https://github.com/microsoft/cascadia-code")
} else if lower.contains("source code") {
Some("https://github.com/adobe-fonts/source-code-pro")
} else if lower.contains("hack") {
Some("https://github.com/source-foundry/Hack")
} else if lower.contains("iosevka") {
Some("https://github.com/be5invis/Iosevka")
} else if lower.contains("victor mono") {
Some("https://github.com/rubjo/victor-mono")
} else {
fn known_font_url(font_name: &str) -> Option<&'static str> {
match font_name.trim().to_ascii_lowercase().as_str() {
"jetbrains mono" | "jetbrainsmono" => {
Some("https://github.com/JetBrains/JetBrainsMono")
}
"fira code" | "firacode" => Some("https://github.com/tonsky/FiraCode"),
"cascadia code" | "cascadia" => {
Some("https://github.com/microsoft/cascadia-code")
}
"source code pro" | "source code" => {
Some("https://github.com/adobe-fonts/source-code-pro")
}
"hack" => Some("https://github.com/source-foundry/Hack"),
"iosevka" => Some("https://github.com/be5invis/Iosevka"),
"victor mono" => Some("https://github.com/rubjo/victor-mono"),
_ => None,
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wezterm-font/src/lib.rs` around lines 802 - 818, known_font_url currently
uses substring matching (contains) which misclassifies families (e.g., "Fira
Mono" matches "fira" and returns the Fira Code URL); replace the contains-based
logic with an explicit allow-list mapping of normalized family names/aliases to
their URLs and perform exact equality checks on a normalized family string
(e.g., lowercased and trimmed). Update known_font_url to look up the normalized
font_name in that mapping (a static slice/array of (&str, &str) or a lazy/static
HashMap) and return the mapped Option<&'static str> for exact matches only,
ensuring aliases like "fira mono" map to the correct URL and preventing
unintended substring matches.

None
}
}
Comment on lines +802 to +821
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This if/else if chain can be refactored to be more maintainable and idiomatic. Using a static array of tuples and a loop would make it easier to add or modify fonts in the future and is generally more readable than a long chain of if/else if statements.

    fn known_font_url(font_name: &str) -> Option<&'static str> {
        let lower = font_name.to_lowercase();
        const FONT_URLS: &[(&str, &str)] = &[
            ("jetbrains", "https://github.com/JetBrains/JetBrainsMono"),
            ("fira", "https://github.com/tonsky/FiraCode"),
            ("cascadia", "https://github.com/microsoft/cascadia-code"),
            ("source code", "https://github.com/adobe-fonts/source-code-pro"),
            ("hack", "https://github.com/source-foundry/Hack"),
            ("iosevka", "https://github.com/be5invis/Iosevka"),
            ("victor mono", "https://github.com/rubjo/victor-mono"),
        ];

        for (name, url) in FONT_URLS {
            if lower.contains(name) {
                return Some(url);
            }
        }
        None
    }


/// Suggest installation command for a missing font based on the platform
fn suggest_font_install(&self, font_name: &str) -> String {
let font_slug = font_name.to_lowercase().replace(" ", "-");
let download_hint = match Self::known_font_url(font_name) {
Some(url) => format!("\n - Or download from: {}", url),
None => String::from("\n - Or manually download the font from its official website"),
};

#[cfg(target_os = "linux")]
{
// Map well-known fonts to their Debian/Ubuntu package names
let lower = font_name.to_lowercase();
let deb_package = if lower.contains("jetbrains") {
"fonts-jetbrains-mono".to_string()
} else if lower.contains("fira") {
"fonts-firacode".to_string()
} else {
format!("fonts-{}", font_slug)
};

format!(
"On Linux, you can install '{}' using your package manager:\n\
- Debian/Ubuntu: sudo apt install {}\n\
- Fedora: sudo dnf install {}\n\
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The suggested Fedora package name, which is derived from font_slug, is likely incorrect for most of the well-known fonts. For example, for 'Fira Code', the package is fira-code-fonts, but the suggestion would be sudo dnf install fira-code. This will lead to a 'package not found' error for users on Fedora.

To improve this, you could create a fedora_package variable with correct names for known fonts, similar to how deb_package is handled. If a consistent naming pattern can't be found for all fonts, it might be safer to suggest a search command, like sudo dnf search <keywords>.

- Or manually install to ~/.local/share/fonts/{}",
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Linux install suggestion, the last {} placeholder is being used to append download_hint after the ~/.local/share/fonts/ path. This works, but it’s hard to read/maintain because the placeholder visually looks like part of the path. Consider removing the {} from the path and appending download_hint explicitly at the end of the format string (with a preceding newline) so the intent is clearer.

Suggested change
- Or manually install to ~/.local/share/fonts/{}",
- Or manually install to ~/.local/share/fonts/{}\n{}",

Copilot uses AI. Check for mistakes.
font_name, deb_package, font_slug, download_hint
)
Comment on lines +843 to +849
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix the Linux install message formatting.

The last {} currently receives download_hint, so the rendered output turns into ~/.local/share/fonts/<download hint> and drops the intended path/package text. That makes the Linux remediation message malformed right in the user-facing error path.

Proposed fix
             format!(
                 "On Linux, you can install '{}' using your package manager:\n\
                   - Debian/Ubuntu: sudo apt install {}\n\
                   - Fedora: sudo dnf install {}\n\
-                  - Or manually install to ~/.local/share/fonts/{}",
-                font_name, deb_package, font_slug, download_hint
+                  - Or manually install to ~/.local/share/fonts/{}{}",
+                font_name, deb_package, font_slug, font_slug, download_hint
             )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
format!(
"On Linux, you can install '{}' using your package manager:\n\
- Debian/Ubuntu: sudo apt install {}\n\
- Fedora: sudo dnf install {}\n\
- Or manually install to ~/.local/share/fonts/{}",
font_name, deb_package, font_slug, download_hint
)
format!(
"On Linux, you can install '{}' using your package manager:\n\
- Debian/Ubuntu: sudo apt install {}\n\
- Fedora: sudo dnf install {}\n\
- Or manually install to ~/.local/share/fonts/{}{}",
font_name, deb_package, font_slug, font_slug, download_hint
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wezterm-font/src/lib.rs` around lines 843 - 849, The Linux install message
places download_hint into the final "{}" placeholder causing the path to render
incorrectly; update the format argument order (or placeholders) so the final
placeholder uses font_slug (or the intended package/path variable) instead of
download_hint—specifically adjust the format call that references font_name,
deb_package, font_slug, download_hint so the last placeholder is populated by
font_slug and download_hint is used only where its hint text belongs.

}

#[cfg(target_os = "macos")]
{
format!(
"On macOS, you can install '{}' using:\n\
- Homebrew: brew install --cask font-{}{}",
font_name, font_slug, download_hint
)
}

#[cfg(target_os = "windows")]
{
format!(
"On Windows, you can install '{}' by:\n\
- Using winget: winget search {}{}",
font_name, font_name, download_hint
)
}

#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
format!(
"Please install '{}' using your system's package manager.{}",
font_name, download_hint
)
}
}

fn resolve_font_helper(
&self,
style: &TextStyle,
Expand Down Expand Up @@ -824,37 +903,58 @@ impl FontConfigInner {
let is_primary = config.font.font.iter().any(|a| a == attr);
let derived_from_primary = config.font.font.iter().any(|a| a.family == attr.family);

// Determine which fallback font will be used.
// `handles` should always have at least one entry (the system
// default) so the `unwrap_or` branch is a safety-net only.
let fallback_font = handles
.first()
.map(|h| h.names().family_name.clone())
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParsedFont::names() returns a Names struct with fields like family and full_name; there is no family_name field. As written, this won’t compile. Use h.names().family.clone() (or full_name if you prefer) when deriving fallback_font.

Suggested change
.map(|h| h.names().family_name.clone())
.map(|h| h.names().family.clone())

Copilot uses AI. Check for mistakes.
.unwrap_or_else(|| FontAttributes::default().family);

let explanation = if is_primary {
// This is the primary font selection
format!(
"Unable to load a font specified by your font={} configuration",
attr
"Font '{}' not found, using '{}' instead",
attr.family, fallback_font
)
} else if derived_from_primary {
// it came from font_rules and may have been derived from
// their primary font (we can't know for sure)
format!(
"Unable to load a font matching one of your font_rules: {}. \
"Font '{}' specified in font_rules not found, using '{}' instead. \
Note that wezterm will synthesize font_rules to select bold \
and italic fonts based on your primary font configuration",
attr
attr.family, fallback_font
)
} else {
format!(
"Unable to load a font matching one of your font_rules: {}",
attr
"Font '{}' specified in font_rules not found, using '{}' instead",
attr.family, fallback_font
)
};

// Get platform-specific installation suggestion
let install_suggestion = self.suggest_font_install(&attr.family);

// Log which font is being used
log::warn!(
"Font '{}' not found, falling back to '{}'",
attr.family,
fallback_font
);

config::show_error(&format!(
"{}. Fallback(s) are being used instead, and the terminal \
may not render as intended{}. See \
https://docs.cxlinux.com/terminal/fonts for more information",
explanation, styled_extra
"{}.\n\n{}{}",
explanation, install_suggestion, styled_extra
));
}
}

// Log which font is actually being used for the primary font
if let Some(first_handle) = handles.first() {
log::info!("Using font: {}", first_handle.names().full_name);
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log::info!("Using font: ...") in resolve_font_helper will run on every cache miss (and potentially twice when cap-height scaling triggers), which can be quite noisy at the default log level. Consider downgrading to debug/trace, or gating it so it only logs once per config generation / only when the primary font differs from the requested family.

Suggested change
log::info!("Using font: {}", first_handle.names().full_name);
log::debug!("Using font: {}", first_handle.names().full_name);

Copilot uses AI. Check for mistakes.
}

Ok((new_shaper(&*config, &handles)?, handles))
}

Expand Down
Loading