Zig parses config values (cli/args.zig, Config.zig field parsers, config/string.zig) from raw byte slices, and each field type decides whether it requires UTF-8 (InvalidValue otherwise) or stores bytes. The port escape-encoded raw bytes into Strings at the line reader and let fields parse the escaped text, so fields could observe encoding artifacts; successive audits kept finding such leaks (RV5-RV8). Field parsers now receive the original bytes: text fields validate UTF-8 where Zig does, byte/path fields store bytes (EncodedOs/OsString). This resolves all 9 RV8 findings (docs/rewrite/notes/review-rv8-codex.md), each with a regression test: Windows path conversion fails with BadPathName instead of substituting U+FFFD; Windows process launch uses strict UTF-8 like Zig's Command; the WTF-8 decoder accepts paired- surrogate encodings like Zig; literal U+FDD0 Unicode key triggers; selection-word-chars and clipboard-codepoint-map reject invalid UTF-8; font style / codepoint-map family names use original bytes; C title actions forward bytes; D-Bus new-window/Flatpak conversions match Zig. Design, sweep and validation: docs/rewrite/notes/final-bytes-refactor.md. Also moves the bundled iTerm2-Color-Schemes license out of resources/themes/ so +list-themes no longer lists it as a theme (output is now byte-identical to Ghostty 1.3.1). Done by Codex (gpt-6-astra), reviewed and verified by the orchestrator. Test Plan: - cargo test --workspace --no-fail-fast (3428 passed, exit 0) - just check-macos (both), just check-windows, just check-wasm - +show-config --default --docs, +list-keybinds, +list-actions --docs, +list-themes --plain compared with /usr/bin/ghostty
603 lines
22 KiB
Rust
603 lines
22 KiB
Rust
//! Command palette entries (ZIG/src/input/command.zig).
|
|
|
|
use std::sync::OnceLock;
|
|
|
|
use crate::binding::{
|
|
Action,
|
|
CloseTabMode,
|
|
CopyToClipboard,
|
|
GotoWindow,
|
|
InspectorMode,
|
|
NavigateSearch,
|
|
SplitDirection,
|
|
SplitFocusDirection,
|
|
WriteScreen,
|
|
WriteScreenAction,
|
|
WriteScreenFormat,
|
|
};
|
|
|
|
/// A command is a named binding action that can be executed from
|
|
/// something like a command palette.
|
|
///
|
|
/// A command must be associated with a binding; all commands can be
|
|
/// mapped to traditional `keybind` configurations. This restriction
|
|
/// makes it so that there is nothing special about commands and likewise
|
|
/// it makes it trivial and consistent to define custom commands.
|
|
///
|
|
/// For apprt implementers: a command palette doesn't have to make use
|
|
/// of all the fields here. We try to provide as much information as
|
|
/// possible to make it easier to implement a command palette in the way
|
|
/// that makes the most sense for the application.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct Command {
|
|
pub action: Action,
|
|
pub title: ghostty_os::osbytes::EncodedOs,
|
|
pub description: ghostty_os::osbytes::EncodedOs,
|
|
}
|
|
|
|
impl Command {
|
|
pub fn new(action: Action, title: &str, description: &str) -> Command {
|
|
Command {
|
|
action,
|
|
title: title.into(),
|
|
description: description.into(),
|
|
}
|
|
}
|
|
|
|
pub fn equal(&self, other: &Command) -> bool {
|
|
if self.action.hash() != other.action.hash() {
|
|
return false;
|
|
}
|
|
self.title == other.title && self.description == other.description
|
|
}
|
|
|
|
/// Implements a comparison function for sorting. The sorting is defined
|
|
/// by Ghostty to be what we prefer. If a caller wants some other
|
|
/// sorting, they should do it themselves.
|
|
pub fn less_than(lhs: &Command, rhs: &Command) -> bool {
|
|
cmp_ignore_case(&lhs.title, &rhs.title) == std::cmp::Ordering::Less
|
|
}
|
|
|
|
/// The `ghostty_command_s` equivalent: (action key name, action string,
|
|
/// title, description).
|
|
pub fn cval(&self) -> CommandC {
|
|
CommandC {
|
|
action_key: self.action.name().to_string(),
|
|
action: self.action.to_string(),
|
|
title: self.title.clone(),
|
|
description: self.description.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Owned form of `Command.C`.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct CommandC {
|
|
pub action_key: String,
|
|
pub action: String,
|
|
pub title: ghostty_os::osbytes::EncodedOs,
|
|
pub description: ghostty_os::osbytes::EncodedOs,
|
|
}
|
|
|
|
/// `std.ascii.orderIgnoreCase`
|
|
fn cmp_ignore_case(
|
|
a: &ghostty_os::osbytes::EncodedOs,
|
|
b: &ghostty_os::osbytes::EncodedOs,
|
|
) -> std::cmp::Ordering {
|
|
let a = a.to_bytes().into_iter().map(|c| c.to_ascii_lowercase());
|
|
let b = b.to_bytes().into_iter().map(|c| c.to_ascii_lowercase());
|
|
a.cmp(b)
|
|
}
|
|
|
|
/// The default command palette entries, sorted by title (ignoring case).
|
|
pub fn defaults() -> &'static [Command] {
|
|
static DEFAULTS: OnceLock<Vec<Command>> = OnceLock::new();
|
|
DEFAULTS.get_or_init(|| {
|
|
let mut result = default_commands();
|
|
result.sort_by(|a, b| cmp_ignore_case(&a.title, &b.title));
|
|
result
|
|
})
|
|
}
|
|
|
|
/// Defaults in C-compatible form.
|
|
pub fn defaults_c() -> Vec<CommandC> {
|
|
defaults().iter().map(Command::cval).collect()
|
|
}
|
|
|
|
// The set of commands associated with each action key by default. Not all
|
|
// actions should have commands. As a general guideline, an action should have
|
|
// a command only if it is useful and reasonable to appear in a command
|
|
// palette. Actions without commands (parameterized actions without obvious
|
|
// values, ignore/unbind, etc.) are: csi, esc, cursor_key, set_font_size,
|
|
// set_surface_title, set_tab_title, search, scroll_to_row,
|
|
// scroll_page_fractional, scroll_page_lines, adjust_selection, jump_to_prompt,
|
|
// write_scrollback_file, goto_tab, resize_split, activate_key_table,
|
|
// activate_key_table_once, deactivate_key_table, deactivate_all_key_tables,
|
|
// end_key_sequence, crash, toggle_command_palette, toggle_quick_terminal,
|
|
// toggle_visibility, previous_tab, next_tab, last_tab, ignore, unbind.
|
|
fn default_commands() -> Vec<Command> {
|
|
vec![
|
|
Command::new(
|
|
Action::Reset,
|
|
"Reset Terminal",
|
|
"Reset the terminal to a clean state.",
|
|
),
|
|
Command::new(
|
|
Action::CopyToClipboard(CopyToClipboard::Mixed),
|
|
"Copy to Clipboard",
|
|
"Copy the selected text to the clipboard in both plain and styled formats.",
|
|
),
|
|
Command::new(
|
|
Action::CopyToClipboard(CopyToClipboard::Plain),
|
|
"Copy Selection as Plain Text to Clipboard",
|
|
"Copy the selected text as plain text to the clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::CopyToClipboard(CopyToClipboard::Vt),
|
|
"Copy Selection as ANSI Sequences to Clipboard",
|
|
"Copy the selected text as ANSI escape sequences to the clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::CopyToClipboard(CopyToClipboard::Html),
|
|
"Copy Selection as HTML to Clipboard",
|
|
"Copy the selected text as HTML to the clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::CopyUrlToClipboard,
|
|
"Copy URL to Clipboard",
|
|
"Copy the URL under the cursor to the clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::CopyTitleToClipboard,
|
|
"Copy Terminal Title to Clipboard",
|
|
"Copy the terminal title to the clipboard. If the terminal title is not set this has no effect.",
|
|
),
|
|
Command::new(
|
|
Action::PasteFromClipboard,
|
|
"Paste from Clipboard",
|
|
"Paste the contents of the main clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::PasteFromSelection,
|
|
"Paste from Selection",
|
|
"Paste the contents of the selection clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::StartSearch,
|
|
"Start Search",
|
|
"Start a search if one isn't already active.",
|
|
),
|
|
Command::new(
|
|
Action::SearchSelection,
|
|
"Search Selection",
|
|
"Start a search for the current text selection.",
|
|
),
|
|
Command::new(
|
|
Action::EndSearch,
|
|
"End Search",
|
|
"End the current search if any and hide any GUI elements.",
|
|
),
|
|
Command::new(
|
|
Action::NavigateSearch(NavigateSearch::Next),
|
|
"Next Search Result",
|
|
"Navigate to the next search result, if any.",
|
|
),
|
|
Command::new(
|
|
Action::NavigateSearch(NavigateSearch::Previous),
|
|
"Previous Search Result",
|
|
"Navigate to the previous search result, if any.",
|
|
),
|
|
Command::new(
|
|
Action::IncreaseFontSize(1.0),
|
|
"Increase Font Size",
|
|
"Increase the font size by 1 point.",
|
|
),
|
|
Command::new(
|
|
Action::DecreaseFontSize(1.0),
|
|
"Decrease Font Size",
|
|
"Decrease the font size by 1 point.",
|
|
),
|
|
Command::new(
|
|
Action::ResetFontSize,
|
|
"Reset Font Size",
|
|
"Reset the font size to the default.",
|
|
),
|
|
Command::new(
|
|
Action::ClearScreen,
|
|
"Clear Screen",
|
|
"Clear the screen and scrollback.",
|
|
),
|
|
Command::new(
|
|
Action::SelectAll,
|
|
"Select All",
|
|
"Select all text on the screen.",
|
|
),
|
|
Command::new(
|
|
Action::ScrollToTop,
|
|
"Scroll to Top",
|
|
"Scroll to the top of the screen.",
|
|
),
|
|
Command::new(
|
|
Action::ScrollToBottom,
|
|
"Scroll to Bottom",
|
|
"Scroll to the bottom of the screen.",
|
|
),
|
|
Command::new(
|
|
Action::ScrollToSelection,
|
|
"Scroll to Selection",
|
|
"Scroll to the selected text.",
|
|
),
|
|
Command::new(
|
|
Action::ScrollPageUp,
|
|
"Scroll Page Up",
|
|
"Scroll the screen up by a page.",
|
|
),
|
|
Command::new(
|
|
Action::ScrollPageDown,
|
|
"Scroll Page Down",
|
|
"Scroll the screen down by a page.",
|
|
),
|
|
Command::new(
|
|
Action::WriteScreenFile(WriteScreen::COPY),
|
|
"Copy Screen to Temporary File and Copy Path",
|
|
"Copy the screen contents to a temporary file and copy the path to the clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::WriteScreenFile(WriteScreen::PASTE),
|
|
"Copy Screen to Temporary File and Paste Path",
|
|
"Copy the screen contents to a temporary file and paste the path to the file.",
|
|
),
|
|
Command::new(
|
|
Action::WriteScreenFile(WriteScreen::OPEN),
|
|
"Copy Screen to Temporary File and Open",
|
|
"Copy the screen contents to a temporary file and open it.",
|
|
),
|
|
Command::new(
|
|
Action::WriteScreenFile(WriteScreen {
|
|
action: WriteScreenAction::Copy,
|
|
emit: WriteScreenFormat::Html,
|
|
}),
|
|
"Copy Screen as HTML to Temporary File and Copy Path",
|
|
"Copy the screen contents as HTML to a temporary file and copy the path to the clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::WriteScreenFile(WriteScreen {
|
|
action: WriteScreenAction::Paste,
|
|
emit: WriteScreenFormat::Html,
|
|
}),
|
|
"Copy Screen as HTML to Temporary File and Paste Path",
|
|
"Copy the screen contents as HTML to a temporary file and paste the path to the file.",
|
|
),
|
|
Command::new(
|
|
Action::WriteScreenFile(WriteScreen {
|
|
action: WriteScreenAction::Open,
|
|
emit: WriteScreenFormat::Html,
|
|
}),
|
|
"Copy Screen as HTML to Temporary File and Open",
|
|
"Copy the screen contents as HTML to a temporary file and open it.",
|
|
),
|
|
Command::new(
|
|
Action::WriteScreenFile(WriteScreen {
|
|
action: WriteScreenAction::Copy,
|
|
emit: WriteScreenFormat::Vt,
|
|
}),
|
|
"Copy Screen as ANSI Sequences to Temporary File and Copy Path",
|
|
"Copy the screen contents as ANSI escape sequences to a temporary file and copy the path to the clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::WriteScreenFile(WriteScreen {
|
|
action: WriteScreenAction::Paste,
|
|
emit: WriteScreenFormat::Vt,
|
|
}),
|
|
"Copy Screen as ANSI Sequences to Temporary File and Paste Path",
|
|
"Copy the screen contents as ANSI escape sequences to a temporary file and paste the path to the file.",
|
|
),
|
|
Command::new(
|
|
Action::WriteScreenFile(WriteScreen {
|
|
action: WriteScreenAction::Open,
|
|
emit: WriteScreenFormat::Vt,
|
|
}),
|
|
"Copy Screen as ANSI Sequences to Temporary File and Open",
|
|
"Copy the screen contents as ANSI escape sequences to a temporary file and open it.",
|
|
),
|
|
Command::new(
|
|
Action::WriteSelectionFile(WriteScreen::COPY),
|
|
"Copy Selection to Temporary File and Copy Path",
|
|
"Copy the selection contents to a temporary file and copy the path to the clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::WriteSelectionFile(WriteScreen::PASTE),
|
|
"Copy Selection to Temporary File and Paste Path",
|
|
"Copy the selection contents to a temporary file and paste the path to the file.",
|
|
),
|
|
Command::new(
|
|
Action::WriteSelectionFile(WriteScreen::OPEN),
|
|
"Copy Selection to Temporary File and Open",
|
|
"Copy the selection contents to a temporary file and open it.",
|
|
),
|
|
Command::new(
|
|
Action::WriteSelectionFile(WriteScreen {
|
|
action: WriteScreenAction::Copy,
|
|
emit: WriteScreenFormat::Html,
|
|
}),
|
|
"Copy Selection as HTML to Temporary File and Copy Path",
|
|
"Copy the selection contents as HTML to a temporary file and copy the path to the clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::WriteSelectionFile(WriteScreen {
|
|
action: WriteScreenAction::Paste,
|
|
emit: WriteScreenFormat::Html,
|
|
}),
|
|
"Copy Selection as HTML to Temporary File and Paste Path",
|
|
"Copy the selection contents as HTML to a temporary file and paste the path to the file.",
|
|
),
|
|
Command::new(
|
|
Action::WriteSelectionFile(WriteScreen {
|
|
action: WriteScreenAction::Open,
|
|
emit: WriteScreenFormat::Html,
|
|
}),
|
|
"Copy Selection as HTML to Temporary File and Open",
|
|
"Copy the selection contents as HTML to a temporary file and open it.",
|
|
),
|
|
Command::new(
|
|
Action::WriteSelectionFile(WriteScreen {
|
|
action: WriteScreenAction::Copy,
|
|
emit: WriteScreenFormat::Vt,
|
|
}),
|
|
"Copy Selection as ANSI Sequences to Temporary File and Copy Path",
|
|
"Copy the selection contents as ANSI escape sequences to a temporary file and copy the path to the clipboard.",
|
|
),
|
|
Command::new(
|
|
Action::WriteSelectionFile(WriteScreen {
|
|
action: WriteScreenAction::Paste,
|
|
emit: WriteScreenFormat::Vt,
|
|
}),
|
|
"Copy Selection as ANSI Sequences to Temporary File and Paste Path",
|
|
"Copy the selection contents as ANSI escape sequences to a temporary file and paste the path to the file.",
|
|
),
|
|
Command::new(
|
|
Action::WriteSelectionFile(WriteScreen {
|
|
action: WriteScreenAction::Open,
|
|
emit: WriteScreenFormat::Vt,
|
|
}),
|
|
"Copy Selection as ANSI Sequences to Temporary File and Open",
|
|
"Copy the selection contents as ANSI escape sequences to a temporary file and open it.",
|
|
),
|
|
Command::new(Action::NewWindow, "New Window", "Open a new window."),
|
|
Command::new(Action::NewTab, "New Tab", "Open a new tab."),
|
|
Command::new(
|
|
Action::MoveTab(-1),
|
|
"Move Tab Left",
|
|
"Move the current tab to the left.",
|
|
),
|
|
Command::new(
|
|
Action::MoveTab(1),
|
|
"Move Tab Right",
|
|
"Move the current tab to the right.",
|
|
),
|
|
Command::new(
|
|
Action::ToggleTabOverview,
|
|
"Toggle Tab Overview",
|
|
"Toggle the tab overview.",
|
|
),
|
|
Command::new(
|
|
Action::PromptSurfaceTitle,
|
|
"Change Terminal Title…",
|
|
"Prompt for a new title for the current terminal.",
|
|
),
|
|
Command::new(
|
|
Action::PromptTabTitle,
|
|
"Change Tab Title…",
|
|
"Prompt for a new title for the current tab.",
|
|
),
|
|
Command::new(
|
|
Action::NewSplit(SplitDirection::Left),
|
|
"Split Left",
|
|
"Split the terminal to the left.",
|
|
),
|
|
Command::new(
|
|
Action::NewSplit(SplitDirection::Right),
|
|
"Split Right",
|
|
"Split the terminal to the right.",
|
|
),
|
|
Command::new(
|
|
Action::NewSplit(SplitDirection::Up),
|
|
"Split Up",
|
|
"Split the terminal up.",
|
|
),
|
|
Command::new(
|
|
Action::NewSplit(SplitDirection::Down),
|
|
"Split Down",
|
|
"Split the terminal down.",
|
|
),
|
|
Command::new(
|
|
Action::GotoSplit(SplitFocusDirection::Previous),
|
|
"Focus Split: Previous",
|
|
"Focus the previous split, if any.",
|
|
),
|
|
Command::new(
|
|
Action::GotoSplit(SplitFocusDirection::Next),
|
|
"Focus Split: Next",
|
|
"Focus the next split, if any.",
|
|
),
|
|
Command::new(
|
|
Action::GotoSplit(SplitFocusDirection::Left),
|
|
"Focus Split: Left",
|
|
"Focus the split to the left, if it exists.",
|
|
),
|
|
Command::new(
|
|
Action::GotoSplit(SplitFocusDirection::Right),
|
|
"Focus Split: Right",
|
|
"Focus the split to the right, if it exists.",
|
|
),
|
|
Command::new(
|
|
Action::GotoSplit(SplitFocusDirection::Up),
|
|
"Focus Split: Up",
|
|
"Focus the split above, if it exists.",
|
|
),
|
|
Command::new(
|
|
Action::GotoSplit(SplitFocusDirection::Down),
|
|
"Focus Split: Down",
|
|
"Focus the split below, if it exists.",
|
|
),
|
|
Command::new(
|
|
Action::GotoWindow(GotoWindow::Previous),
|
|
"Focus Window: Previous",
|
|
"Focus the previous window, if any.",
|
|
),
|
|
Command::new(
|
|
Action::GotoWindow(GotoWindow::Next),
|
|
"Focus Window: Next",
|
|
"Focus the next window, if any.",
|
|
),
|
|
Command::new(
|
|
Action::ToggleSplitZoom,
|
|
"Toggle Split Zoom",
|
|
"Toggle the zoom state of the current split.",
|
|
),
|
|
Command::new(
|
|
Action::ToggleReadonly,
|
|
"Toggle Read-Only Mode",
|
|
"Toggle read-only mode for the current surface.",
|
|
),
|
|
Command::new(
|
|
Action::EqualizeSplits,
|
|
"Equalize Splits",
|
|
"Equalize the size of all splits.",
|
|
),
|
|
Command::new(
|
|
Action::ResetWindowSize,
|
|
"Reset Window Size",
|
|
"Reset the window size to the default.",
|
|
),
|
|
Command::new(
|
|
Action::Inspector(InspectorMode::Toggle),
|
|
"Toggle Inspector",
|
|
"Toggle the inspector.",
|
|
),
|
|
Command::new(
|
|
Action::ShowGtkInspector,
|
|
"Show the GTK Inspector",
|
|
"Show the GTK inspector.",
|
|
),
|
|
Command::new(
|
|
Action::ShowOnScreenKeyboard,
|
|
"Show On-Screen Keyboard",
|
|
"Show the on-screen keyboard if present.",
|
|
),
|
|
Command::new(Action::OpenConfig, "Open Config", "Open the config file."),
|
|
Command::new(
|
|
Action::ReloadConfig,
|
|
"Reload Config",
|
|
"Reload the config file.",
|
|
),
|
|
Command::new(
|
|
Action::CloseSurface,
|
|
"Close Terminal",
|
|
"Close the current terminal.",
|
|
),
|
|
Command::new(
|
|
Action::CloseTab(CloseTabMode::This),
|
|
"Close Tab",
|
|
"Close the current tab.",
|
|
),
|
|
Command::new(
|
|
Action::CloseTab(CloseTabMode::Other),
|
|
"Close Other Tabs",
|
|
"Close all tabs in this window except the current one.",
|
|
),
|
|
Command::new(
|
|
Action::CloseTab(CloseTabMode::Right),
|
|
"Close Tabs to the Right",
|
|
"Close all tabs to the right of the current one.",
|
|
),
|
|
Command::new(
|
|
Action::CloseWindow,
|
|
"Close Window",
|
|
"Close the current window.",
|
|
),
|
|
Command::new(
|
|
Action::CloseAllWindows,
|
|
"Close All Windows",
|
|
"Close all windows.",
|
|
),
|
|
Command::new(
|
|
Action::ToggleMaximize,
|
|
"Toggle Maximize",
|
|
"Toggle the maximized state of the current window.",
|
|
),
|
|
Command::new(
|
|
Action::ToggleFullscreen,
|
|
"Toggle Fullscreen",
|
|
"Toggle the fullscreen state of the current window.",
|
|
),
|
|
Command::new(
|
|
Action::ToggleWindowDecorations,
|
|
"Toggle Window Decorations",
|
|
"Toggle the window decorations.",
|
|
),
|
|
Command::new(
|
|
Action::ToggleWindowFloatOnTop,
|
|
"Toggle Float on Top",
|
|
"Toggle the float on top state of the current window.",
|
|
),
|
|
Command::new(
|
|
Action::ToggleSecureInput,
|
|
"Toggle Secure Input",
|
|
"Toggle secure input mode.",
|
|
),
|
|
Command::new(
|
|
Action::ToggleMouseReporting,
|
|
"Toggle Mouse Reporting",
|
|
"Toggle whether mouse events are reported to terminal applications.",
|
|
),
|
|
Command::new(
|
|
Action::ToggleBackgroundOpacity,
|
|
"Toggle Background Opacity",
|
|
"Toggle the background opacity of a window that started transparent.",
|
|
),
|
|
Command::new(
|
|
Action::CheckForUpdates,
|
|
"Check for Updates",
|
|
"Check for updates to the application.",
|
|
),
|
|
Command::new(Action::Undo, "Undo", "Undo the last action."),
|
|
Command::new(Action::Redo, "Redo", "Redo the last undone action."),
|
|
Command::new(Action::Quit, "Quit", "Quit the application."),
|
|
Command::new(
|
|
Action::Text("👻".into()),
|
|
"Ghostty",
|
|
"Put a little Ghostty in your terminal.",
|
|
),
|
|
]
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn command_defaults() {
|
|
// This just ensures that defaults is analyzed and works.
|
|
assert_ne!(defaults().len(), 0);
|
|
assert_eq!(defaults().len(), defaults_c().len());
|
|
}
|
|
|
|
#[test]
|
|
fn command_defaults_sorted_and_roundtrip() {
|
|
let d = defaults();
|
|
for w in d.windows(2) {
|
|
assert_ne!(
|
|
cmp_ignore_case(&w[0].title, &w[1].title),
|
|
std::cmp::Ordering::Greater
|
|
);
|
|
}
|
|
// Round trip every default action through its string form.
|
|
// (`text` is stored raw and escaped on format, so it is excluded.)
|
|
for c in d.iter().filter(|c| !matches!(c.action, Action::Text(_))) {
|
|
let s = c.action.to_string();
|
|
assert_eq!(Action::parse(&s).expect("parse"), c.action, "{s}");
|
|
}
|
|
}
|
|
}
|