Files
Lumotia/src-tauri/src/commands/paste.rs
Jake fe61661305 chore(lint): clean up clippy warnings across workspace
Auto-applied cargo clippy --fix across 11 files — needless return,
unnecessary cast, map_or simplification, repeat().take() → repeat_n(),
iter().any() → contains(), manual char comparison, lifetime elision,
push_str single-char, reference immediately dereferenced.

Also fixed three lints on file_storage.rs manually: two doc-list-item
overindentations, plus the same needless-return. Baseline main was
not clippy-clean with -D warnings before; after this pass one
needless_range_loop warning remains (live.rs:1089) that clippy's
suggested rewrite would make less readable — left for a dedicated
refactor session.

Build + workspace tests remain green (245 passing, 0 failing, 1
ignored).
2026-04-24 09:43:56 +01:00

791 lines
25 KiB
Rust

//! Auto-insert-at-cursor.
//!
//! `copy_to_clipboard` puts the transcript on the user's clipboard. That is
//! fine when the user wants to choose where it lands. It is friction when
//! they were about to paste into the already-focused window — which is
//! almost always, for a dictation tool.
//!
//! This module adds the follow-on step: send the platform's Ctrl+V / Cmd+V
//! keystroke to the focused window so the transcript lands where the cursor
//! already is. Each platform uses a different primitive (wtype / xdotool /
//! ydotool / osascript / SendKeys), so we probe and fall back.
//!
//! NOTE: focus must already be on the target window when the keystroke
//! fires. The global hotkey flow preserves this naturally; clicking Kon's
//! window does not. The frontend surfaces this caveat next to the toggle.
use std::process::Command;
use std::time::Duration;
use arboard::Clipboard;
use serde::Serialize;
use tauri::Manager;
/// Window after the paste keystroke at which we restore the user's
/// prior clipboard content. 300 ms is enough for even a slow Wayland
/// compositor to have fully delivered the synthesised Ctrl+V to the
/// focused app; longer risks the user manually pasting again and
/// stomping themselves. See Handy #921.
const CLIPBOARD_RESTORE_MS: u64 = 300;
/// Compositor settle time after hiding the preview overlay before firing
/// the paste keystroke. Empirically ~80ms is enough on KWin + Mutter
/// Wayland for focus to return to the previously-focused app; shorter
/// risks the keystroke still landing on the (now-invisible) overlay.
const PREVIEW_HIDE_SETTLE_MS: u64 = 80;
/// Gap between the undo keystroke and the follow-up paste in the
/// replace flow (brief item #17). Most GUI apps process undo
/// instantly, but a short beat keeps the paste from racing the undo
/// on slower-to-redraw targets (Electron apps, LibreOffice).
const UNDO_PASTE_GAP_MS: u64 = 60;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PasteOutcome {
/// The backend that executed the paste, if any (`"wtype"`, `"xdotool"`,
/// `"ydotool"`, `"osascript"`, `"sendkeys"`).
pub backend: Option<String>,
pub pasted: bool,
pub copied: bool,
/// Diagnostic message when either step failed. Present even on success
/// if the paste fell back to copy-only so the frontend can toast it.
pub message: Option<String>,
}
/// Copy `text` to the clipboard, then trigger a paste keystroke in the
/// focused window. Returns a structured result so the frontend can surface
/// partial success (clipboard set, paste failed).
///
/// Wayland compositor quirk: if Kon's always-on-top preview overlay is
/// visible when the keystroke fires, KWin / Mutter may resolve the keystroke
/// against the overlay (even with `focused: false` set at build time) and
/// the paste lands inside Kon instead of the previously-focused app. We hide
/// the preview window and give the compositor a beat to re-focus the real
/// target before dispatching. Matches OpenWhispr's PR #246 fix on GNOME.
#[tauri::command]
pub async fn paste_text(app: tauri::AppHandle, text: String) -> Result<PasteOutcome, String> {
let mut outcome = PasteOutcome {
backend: None,
pasted: false,
copied: false,
message: None,
};
let prior_clipboard = snapshot_clipboard_text();
match Clipboard::new().and_then(|mut cb| cb.set_text(&text)) {
Ok(()) => outcome.copied = true,
Err(err) => {
outcome.message = Some(format!("clipboard: {err}"));
return Ok(outcome);
}
}
// Brief item #10: if the focused window is a known terminal
// emulator, skip the synthesised Ctrl+V — it duplicates the
// keystroke through the PTY, which Kitty / Alacritty / Windows
// Terminal all demonstrably mishandle (Handy #692). Clipboard-only
// paste with a "Terminal detected" message lets the user finish
// with a manual right-click paste or Ctrl+Shift+V.
if let Some(term) = detect_focused_terminal() {
outcome.message = Some(format!(
"Terminal detected ({term}) — transcript is on your clipboard. \
Use Ctrl+Shift+V (or right-click → paste) to insert."
));
// Prior clipboard snapshot is intentionally NOT restored here:
// the user's path to recover the transcript is their own paste,
// which needs the transcript on the clipboard. The restore
// task below only runs when `outcome.pasted` is true.
return Ok(outcome);
}
hide_preview_overlay_for_paste(&app).await;
match trigger_paste_keystroke() {
Ok(backend) => {
outcome.backend = Some(backend);
outcome.pasted = true;
}
Err(err) => outcome.message = Some(err),
}
if outcome.pasted {
schedule_clipboard_restore(prior_clipboard, text);
}
Ok(outcome)
}
/// Snapshot the clipboard's current text, or `None` when the clipboard
/// holds non-text content (images, files, empty). Shared between
/// `paste_text` and `paste_text_replacing` so both halves of the
/// dictation flow obey the "never silently clobber the user's
/// clipboard" contract from Handy #921 (brief item #3).
fn snapshot_clipboard_text() -> Option<String> {
match Clipboard::new() {
Ok(mut cb) => cb.get_text().ok(),
Err(_) => None,
}
}
/// Fire-and-forget: restore `prior` to the clipboard after
/// `CLIPBOARD_RESTORE_MS`, but only if the clipboard still holds the
/// `transcript` we set (i.e., the user hasn't copied something new in
/// the window). No-op when `prior` is `None` — nothing safe to
/// restore.
fn schedule_clipboard_restore(prior: Option<String>, transcript: String) {
let Some(prior) = prior else {
return;
};
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(CLIPBOARD_RESTORE_MS)).await;
restore_prior_clipboard(&prior, &transcript);
});
}
/// Write `prior` back to the clipboard only if the current clipboard
/// content is still the transcript we just set — i.e. the user hasn't
/// already copied something new themselves in the last 300 ms. This
/// avoids stomping a deliberate Cmd+C that happened right after the
/// paste.
fn restore_prior_clipboard(prior: &str, transcript: &str) {
let mut cb = match Clipboard::new() {
Ok(cb) => cb,
Err(_) => return,
};
if should_restore(cb.get_text().ok().as_deref(), transcript) {
let _ = cb.set_text(prior.to_string());
}
}
/// Pure decision function: should we restore the prior clipboard?
///
/// - Some(current) == transcript → yes, the clipboard still holds
/// what we wrote, so restore is safe.
/// - Some(current) != transcript → no, user has already copied
/// something else in the 300 ms window; respect it.
/// - None → couldn't read the clipboard (image, no access, etc.);
/// conservative no-op.
fn should_restore(current: Option<&str>, transcript: &str) -> bool {
matches!(current, Some(text) if text == transcript)
}
/// Replace-flow paste (brief item #17): send the platform's undo
/// keystroke to the previously-focused app, wait a beat so the undo
/// takes effect, then paste `text`. The caller (preview overlay's
/// "Replace with raw" button) is responsible for only invoking this
/// when a paste actually happened moments earlier — we don't probe to
/// verify there's something to undo.
///
/// Returns the same PasteOutcome shape as `paste_text` so the frontend
/// can surface identical partial-success messaging.
#[tauri::command]
pub async fn paste_text_replacing(
app: tauri::AppHandle,
text: String,
) -> Result<PasteOutcome, String> {
let mut outcome = PasteOutcome {
backend: None,
pasted: false,
copied: false,
message: None,
};
// Same clipboard-preservation contract as `paste_text`: snapshot
// the user's prior clipboard before we write the raw transcript,
// restore it in the background after the paste keystroke fires.
// Without this, a successful revert leaves the raw transcript on
// the clipboard permanently — stomping whatever the user had
// copied before invoking replace-with-raw.
let prior_clipboard = snapshot_clipboard_text();
match Clipboard::new().and_then(|mut cb| cb.set_text(&text)) {
Ok(()) => outcome.copied = true,
Err(err) => {
outcome.message = Some(format!("clipboard: {err}"));
return Ok(outcome);
}
}
hide_preview_overlay_for_paste(&app).await;
if let Err(err) = trigger_undo_keystroke() {
outcome.message = Some(format!("undo: {err}"));
return Ok(outcome);
}
tokio::time::sleep(Duration::from_millis(UNDO_PASTE_GAP_MS)).await;
match trigger_paste_keystroke() {
Ok(backend) => {
outcome.backend = Some(backend);
outcome.pasted = true;
}
Err(err) => outcome.message = Some(err),
}
if outcome.pasted {
schedule_clipboard_restore(prior_clipboard, text);
}
Ok(outcome)
}
/// Hide the transcription-preview window if it's currently visible, then
/// sleep a short beat so the compositor can recompute focus. No-ops when
/// the window isn't registered yet (user never enabled the overlay) or
/// isn't currently shown.
async fn hide_preview_overlay_for_paste(app: &tauri::AppHandle) {
let Some(window) = app.get_webview_window("transcription-preview") else {
return;
};
let visible = window.is_visible().unwrap_or(false);
if !visible {
return;
}
if window.hide().is_err() {
return;
}
tokio::time::sleep(Duration::from_millis(PREVIEW_HIDE_SETTLE_MS)).await;
}
/// Report which paste backends the OS has available right now. Pure probe —
/// does not paste. Used by Settings to tell the user "install wtype" when
/// nothing is available on their session.
#[tauri::command]
pub fn detect_paste_backends() -> Vec<String> {
let mut available = Vec::new();
#[cfg(target_os = "linux")]
{
for tool in ["wtype", "xdotool", "ydotool"] {
if which_on_path(tool) {
available.push(tool.to_string());
}
}
}
#[cfg(target_os = "macos")]
{
available.push("osascript".to_string());
}
#[cfg(target_os = "windows")]
{
available.push("sendkeys".to_string());
}
available
}
/// Well-known terminal-emulator window classes / process names. Each
/// entry is matched case-insensitively against whatever the
/// focused-window probe returns on each platform. The list is
/// deliberately conservative — users can always copy + paste manually,
/// so false negatives are cheap; false positives silently break
/// auto-paste on a legitimate GUI, which is expensive.
/// Longest / most specific matches come first so that `classify_terminal`
/// returns the specific name when two needles would both hit
/// (e.g. "windowsterminal" contains "terminal"). Substring match is
/// otherwise blunt enough that a `foot`/`pwsh` match on a non-terminal
/// window would be vanishingly unlikely.
const KNOWN_TERMINAL_CLASSES: &[&str] = &[
// Windows — specific names first so they win over plain "terminal".
"windowsterminal",
"powershell",
"conhost",
"console",
"pwsh",
"cmd",
// macOS
"iterm2",
"iterm",
"terminal",
// Linux
"gnome-terminal-server",
"alacritty",
"konsole",
"wezterm",
"kitty",
"tilix",
"xterm",
"urxvt",
"st-256color",
"hyper",
"foot",
];
/// Return `Some(class)` when the focused window matches a known
/// terminal emulator, `None` otherwise. Pure by design: the actual
/// platform probe is isolated in `detect_focused_window_class` so the
/// classification rule is unit-testable.
fn detect_focused_terminal() -> Option<String> {
let raw = detect_focused_window_class()?;
classify_terminal(&raw)
}
/// Case-insensitive substring match of the focused window class /
/// process name against `KNOWN_TERMINAL_CLASSES`. Returns the matched
/// needle (for user-facing copy: "Terminal detected (kitty)") when
/// positive.
fn classify_terminal(raw: &str) -> Option<String> {
let haystack = raw.to_ascii_lowercase();
for needle in KNOWN_TERMINAL_CLASSES {
if haystack.contains(needle) {
return Some((*needle).to_string());
}
}
None
}
/// Probe the focused window's class name / process name. Returns
/// `None` when the probe tool is missing or fails — we fall back to
/// the default auto-paste path rather than guessing wrong and
/// breaking a GUI user's workflow.
fn detect_focused_window_class() -> Option<String> {
#[cfg(target_os = "linux")]
{
detect_focused_window_class_linux()
}
#[cfg(target_os = "macos")]
{
return detect_focused_window_class_macos();
}
#[cfg(target_os = "windows")]
{
return detect_focused_window_class_windows();
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
return None;
}
}
#[cfg(target_os = "linux")]
fn detect_focused_window_class_linux() -> Option<String> {
// X11: xdotool getactivewindow getwindowclassname
if let Ok(output) = Command::new("xdotool")
.args(["getactivewindow", "getwindowclassname"])
.output()
{
if output.status.success() {
let class = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !class.is_empty() {
return Some(class);
}
}
}
// Wayland: no reliable focused-window probe from an unprivileged
// client (by design). Return None and let the normal paste path
// run; users on Kitty-under-Wayland already lean on Ctrl+Shift+V
// themselves.
None
}
#[cfg(target_os = "macos")]
fn detect_focused_window_class_macos() -> Option<String> {
// `osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'`
let output = Command::new("osascript")
.args([
"-e",
"tell application \"System Events\" to get name of first application process whose frontmost is true",
])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
if name.is_empty() {
None
} else {
Some(name)
}
}
#[cfg(target_os = "windows")]
fn detect_focused_window_class_windows() -> Option<String> {
// PowerShell one-liner: (Get-Process -Id (Get-Process | Where-Object ...
// Keeping it simple here — GetForegroundWindow + GetWindowThreadProcessId
// via PowerShell works without a `windows` crate dep.
let script = r#"
$w = Add-Type -MemberDefinition '
[DllImport("user32.dll")] public static extern System.IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern int GetWindowThreadProcessId(System.IntPtr hWnd, out int pid);
' -Name W -PassThru
$pid = 0
[void]$w::GetWindowThreadProcessId($w::GetForegroundWindow(), [ref]$pid)
(Get-Process -Id $pid).ProcessName
"#;
let output = Command::new("powershell")
.args(["-NoProfile", "-Command", script])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
if name.is_empty() {
None
} else {
Some(name)
}
}
fn trigger_paste_keystroke() -> Result<String, String> {
#[cfg(target_os = "linux")]
{
linux_paste(
std::env::var("XDG_SESSION_TYPE").ok().as_deref(),
std::env::var_os("WAYLAND_DISPLAY").is_some(),
)
}
#[cfg(target_os = "macos")]
{
macos_paste()
}
#[cfg(target_os = "windows")]
{
windows_paste()
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
Err("auto-paste not implemented on this platform".into())
}
}
/// Send Ctrl+Z / Cmd+Z to the focused window. Mirrors the shape of
/// `trigger_paste_keystroke`: reuse the same per-OS backend ordering,
/// just swap the keycode. Used by `paste_text_replacing` for the
/// raw-revert flow (brief item #17).
fn trigger_undo_keystroke() -> Result<String, String> {
#[cfg(target_os = "linux")]
{
linux_undo(
std::env::var("XDG_SESSION_TYPE").ok().as_deref(),
std::env::var_os("WAYLAND_DISPLAY").is_some(),
)
}
#[cfg(target_os = "macos")]
{
macos_undo()
}
#[cfg(target_os = "windows")]
{
windows_undo()
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
Err("undo-keystroke not implemented on this platform".into())
}
}
#[cfg(target_os = "linux")]
fn linux_paste(
xdg_session_type: Option<&str>,
wayland_display_set: bool,
) -> Result<String, String> {
for tool in pick_linux_backend_order(xdg_session_type, wayland_display_set) {
match run_linux_tool(tool) {
Ok(()) => return Ok(tool.to_string()),
Err(_) => continue,
}
}
Err(
"No paste backend available. Install wtype (Wayland) or xdotool (X11) to enable \
auto-insert-at-cursor."
.into(),
)
}
#[cfg(target_os = "linux")]
fn pick_linux_backend_order(
xdg_session_type: Option<&str>,
wayland_display_set: bool,
) -> &'static [&'static str] {
let is_wayland = xdg_session_type
.map(|value| value.eq_ignore_ascii_case("wayland"))
.unwrap_or(false)
|| wayland_display_set;
if is_wayland {
&["wtype", "ydotool", "xdotool"]
} else {
&["xdotool", "ydotool", "wtype"]
}
}
#[cfg(target_os = "linux")]
fn run_linux_tool(tool: &str) -> Result<(), String> {
let output = match tool {
// wtype -M ctrl v -m ctrl (press ctrl, tap v, release ctrl)
"wtype" => Command::new("wtype")
.args(["-M", "ctrl", "v", "-m", "ctrl"])
.output(),
"xdotool" => Command::new("xdotool").args(["key", "ctrl+v"]).output(),
// ydotool linux input keycodes: 29=LEFTCTRL, 47=V. Format is
// `code:state` pairs. Requires ydotoold running with access to
// /dev/uinput.
"ydotool" => Command::new("ydotool")
.args(["key", "29:1", "47:1", "47:0", "29:0"])
.output(),
other => return Err(format!("unknown backend: {other}")),
}
.map_err(|e| format!("{tool} unavailable: {e}"))?;
if output.status.success() {
Ok(())
} else {
Err(format!(
"{tool} exit {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
))
}
}
#[cfg(target_os = "linux")]
fn linux_undo(xdg_session_type: Option<&str>, wayland_display_set: bool) -> Result<String, String> {
for tool in pick_linux_backend_order(xdg_session_type, wayland_display_set) {
match run_linux_undo_tool(tool) {
Ok(()) => return Ok(tool.to_string()),
Err(_) => continue,
}
}
Err("No undo backend available. Install wtype (Wayland) or xdotool (X11).".into())
}
#[cfg(target_os = "linux")]
fn run_linux_undo_tool(tool: &str) -> Result<(), String> {
let output = match tool {
// wtype -M ctrl z -m ctrl (press ctrl, tap z, release ctrl)
"wtype" => Command::new("wtype")
.args(["-M", "ctrl", "z", "-m", "ctrl"])
.output(),
"xdotool" => Command::new("xdotool").args(["key", "ctrl+z"]).output(),
// 29 = LEFTCTRL, 44 = Z in linux input keycodes.
"ydotool" => Command::new("ydotool")
.args(["key", "29:1", "44:1", "44:0", "29:0"])
.output(),
other => return Err(format!("unknown backend: {other}")),
}
.map_err(|e| format!("{tool} unavailable: {e}"))?;
if output.status.success() {
Ok(())
} else {
Err(format!(
"{tool} exit {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
))
}
}
#[cfg(target_os = "linux")]
fn which_on_path(tool: &str) -> bool {
Command::new("sh")
.args(["-c", &format!("command -v {tool}")])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
fn macos_paste() -> Result<String, String> {
let output = Command::new("osascript")
.args([
"-e",
"tell application \"System Events\" to keystroke \"v\" using command down",
])
.output()
.map_err(|e| format!("osascript: {e}"))?;
if output.status.success() {
Ok("osascript".into())
} else {
Err(format!(
"osascript exit {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
))
}
}
#[cfg(target_os = "macos")]
fn macos_undo() -> Result<String, String> {
let output = Command::new("osascript")
.args([
"-e",
"tell application \"System Events\" to keystroke \"z\" using command down",
])
.output()
.map_err(|e| format!("osascript: {e}"))?;
if output.status.success() {
Ok("osascript".into())
} else {
Err(format!(
"osascript exit {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
))
}
}
#[cfg(target_os = "windows")]
fn windows_paste() -> Result<String, String> {
// SendKeys("^v") simulates Ctrl+V in the foreground window. Requires
// no extra permissions on Windows. A native SendInput call would skip
// the PowerShell spawn but pulls in the windows crate; cost not worth
// the complexity until this hot path actually shows up.
let output = Command::new("powershell")
.args([
"-NoProfile",
"-Command",
"(New-Object -ComObject WScript.Shell).SendKeys('^v')",
])
.output()
.map_err(|e| format!("powershell: {e}"))?;
if output.status.success() {
Ok("sendkeys".into())
} else {
Err(format!(
"powershell exit {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
))
}
}
#[cfg(target_os = "windows")]
fn windows_undo() -> Result<String, String> {
let output = Command::new("powershell")
.args([
"-NoProfile",
"-Command",
"(New-Object -ComObject WScript.Shell).SendKeys('^z')",
])
.output()
.map_err(|e| format!("powershell: {e}"))?;
if output.status.success() {
Ok("sendkeys".into())
} else {
Err(format!(
"powershell exit {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
))
}
}
#[cfg(all(test, target_os = "linux"))]
mod tests_linux {
use super::pick_linux_backend_order;
#[test]
fn wayland_session_prefers_wtype_then_ydotool() {
assert_eq!(
pick_linux_backend_order(Some("wayland"), true),
&["wtype", "ydotool", "xdotool"]
);
}
#[test]
fn x11_session_prefers_xdotool() {
assert_eq!(
pick_linux_backend_order(Some("x11"), false),
&["xdotool", "ydotool", "wtype"]
);
}
#[test]
fn wayland_display_env_var_alone_is_enough() {
assert_eq!(
pick_linux_backend_order(None, true),
&["wtype", "ydotool", "xdotool"]
);
}
#[test]
fn uppercase_wayland_token_still_detects_wayland() {
assert_eq!(
pick_linux_backend_order(Some("WAYLAND"), false),
&["wtype", "ydotool", "xdotool"]
);
}
}
#[cfg(test)]
mod tests_clipboard_restore {
use super::should_restore;
#[test]
fn restores_when_clipboard_still_holds_transcript() {
assert!(should_restore(Some("dictated text"), "dictated text"));
}
#[test]
fn does_not_restore_when_user_copied_something_else() {
assert!(!should_restore(
Some("user copied this just now"),
"dictated text"
));
}
#[test]
fn does_not_restore_when_clipboard_unreadable() {
assert!(!should_restore(None, "dictated text"));
}
#[test]
fn does_not_restore_on_empty_mismatch() {
assert!(!should_restore(Some(""), "dictated text"));
}
}
#[cfg(test)]
mod tests_terminal_classification {
use super::classify_terminal;
#[test]
fn kitty_class_is_a_terminal() {
assert_eq!(classify_terminal("kitty"), Some("kitty".into()));
}
#[test]
fn alacritty_matches_case_insensitively() {
assert_eq!(classify_terminal("Alacritty"), Some("alacritty".into()));
}
#[test]
fn windows_terminal_matches_collapsed_name() {
// Windows reports the process name sans space: "WindowsTerminal"
assert_eq!(
classify_terminal("WindowsTerminal"),
Some("windowsterminal".into()),
);
}
#[test]
fn iterm_matches_iterm2_as_well() {
assert_eq!(classify_terminal("iTerm2"), Some("iterm2".into()));
}
#[test]
fn firefox_is_not_a_terminal() {
assert_eq!(classify_terminal("firefox"), None);
}
#[test]
fn empty_class_is_not_a_terminal() {
assert_eq!(classify_terminal(""), None);
}
#[test]
fn substring_match_survives_xdotool_decorations() {
// Some X11 window managers report "Alacritty.Alacritty"
assert_eq!(
classify_terminal("Alacritty.Alacritty"),
Some("alacritty".into()),
);
}
}