Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions scripts/linux/install-shell-integration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Type=Application
Version=1.0
Name=Compare with ${PRODUCT_NAME}
GenericName=File Compare
Comment=Open paths with ${PRODUCT_NAME} via shell-compare
Comment=Compare files or folders with ${PRODUCT_NAME}
Exec="${exec_path}" --shell-compare %f
Icon=utilities-file-compare
Terminal=false
Expand All @@ -54,4 +54,4 @@ if command -v update-desktop-database >/dev/null 2>&1; then
fi

echo "Installed $desktop"
echo "CLI: open-diff-cli shell-compare [--select-left] <path>"
echo "Try: open-diff-cli --help or open-diff-cli left right or open-diff-cli shell-compare [--select-left] <path>"
107 changes: 92 additions & 15 deletions src-tauri/crates/cli-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ where
/// Accept `open-diff-app LEFT RIGHT` (and optional open switches) without an `open` verb.
fn parse_bare_open_compare(first: &str, rest: Vec<String>) -> Result<CliInvocation, CliParseError> {
if normalized_switch(first).is_some() {
return Err(usage_error(format!("unknown command: {first}")));
return Err(usage_error(unknown_command_message(first)));
}

let mut open_args = Vec::with_capacity(rest.len() + 1);
Expand All @@ -269,20 +269,39 @@ fn parse_bare_open_compare(first: &str, rest: Vec<String>) -> Result<CliInvocati
error
} else {
CliParseError {
message: format!("unknown command: {first_path}"),
message: unknown_command_message(&first_path),
exit_code: error.exit_code,
}
}
})
}

fn unknown_command_message(command: &str) -> String {
format!(
"Unknown command `{command}`. Compare two paths with `open-diff-cli left right`, or run `open-diff-cli --help`."
)
}

fn with_help_hint(message: impl Into<String>) -> String {
format!("{} Run `open-diff-cli --help` for usage.", message.into())
}

pub fn cli_exit_code_value(exit_code: CliExitCode) -> i32 {
exit_code as i32
}

pub fn cli_help_text() -> String {
let mut lines = vec![
"Open Diff command-line helper".to_owned(),
String::new(),
"Quick start:".to_owned(),
" open-diff-cli left.txt right.txt open two paths in the app".to_owned(),
" open-diff-cli compare a.txt b.txt compare text files in the terminal".to_owned(),
" open-diff-cli compare-folders A B compare folders in the terminal".to_owned(),
String::new(),
"Usage: open-diff-cli <command> [args]".to_owned(),
" open-diff-cli <left> <right> same as open (desktop file handoff)".to_owned(),
String::new(),
"Commands:".to_owned(),
" compare [--quiet] <left> <right>".to_owned(),
" compare-folders [--quiet] <left> <right>".to_owned(),
Expand All @@ -294,7 +313,6 @@ pub fn cli_help_text() -> String {
" script <script-path>".to_owned(),
" open-session <store-root> <name>".to_owned(),
" open [options] <left> <right>".to_owned(),
" <left> <right> same as open (GUI / desktop %F handoff)".to_owned(),
" --session <type> folder-compare, folder-sync, folder-merge,".to_owned(),
" text-compare, text-merge, text-edit, text-patch,".to_owned(),
" table-compare, hex-compare, picture-compare,".to_owned(),
Expand All @@ -315,6 +333,11 @@ pub fn cli_help_text() -> String {
" sync-preview|sync [--quiet] <left> <right>".to_owned(),
" merge-text --automerge [--favor-left|--favor-right] <base> <left> <right> [output]"
.to_owned(),
String::new(),
"Desktop / file manager:".to_owned(),
" Install shell integration from Options → Shell, or pass two paths so the".to_owned(),
" app opens them (Linux .desktop Exec uses %F for selected files).".to_owned(),
String::new(),
"Switches accept --name, -name, or /name forms.".to_owned(),
"Exit codes:".to_owned(),
];
Expand Down Expand Up @@ -753,7 +776,9 @@ fn parse_shell_compare(args: Vec<String>) -> Result<CliInvocation, CliParseError
}

let Some(path) = path else {
return Err(usage_error("shell-compare requires PATH"));
return Err(usage_error(with_help_hint(
"shell-compare needs a path, for example: open-diff-cli shell-compare notes.txt",
)));
};

Ok(CliInvocation {
Expand Down Expand Up @@ -810,7 +835,9 @@ fn parse_svn_diff_config(args: Vec<String>) -> Result<CliInvocation, CliParseErr

fn parse_script_file(args: Vec<String>) -> Result<CliInvocation, CliParseError> {
if args.len() != 1 {
return Err(usage_error("script requires SCRIPT_PATH"));
return Err(usage_error(with_help_hint(
"script needs a script file path",
)));
}

Ok(CliInvocation {
Expand Down Expand Up @@ -898,7 +925,9 @@ fn parse_compare_files(args: Vec<String>) -> Result<CliInvocation, CliParseError
}
}
if positionals.len() != 2 {
return Err(usage_error("compare requires LEFT and RIGHT paths"));
return Err(usage_error(with_help_hint(
"compare needs two paths, for example: open-diff-cli compare left.txt right.txt",
)));
}

Ok(CliInvocation {
Expand Down Expand Up @@ -926,7 +955,7 @@ fn parse_compare_folders(args: Vec<String>) -> Result<CliInvocation, CliParseErr
}
}
if positionals.len() != 2 {
return Err(usage_error("compare-folders requires LEFT and RIGHT paths"));
return Err(usage_error(with_help_hint("compare-folders needs two folder paths, for example: open-diff-cli compare-folders left right")));
}

Ok(CliInvocation {
Expand Down Expand Up @@ -1044,14 +1073,16 @@ fn parse_open_compare(args: Vec<String>) -> Result<CliInvocation, CliParseError>

if left.is_none() && right.is_none() {
if positionals.len() != 2 {
return Err(usage_error(
"open requires LEFT and RIGHT paths (or --left/--right)",
));
return Err(usage_error(with_help_hint(
"open needs two paths, for example: open-diff-cli open left.txt right.txt",
)));
}
left = Some(positionals[0].clone());
right = Some(positionals[1].clone());
} else if left.is_none() || right.is_none() {
return Err(usage_error("open requires both --left and --right"));
return Err(usage_error(with_help_hint(
"open needs both --left and --right paths",
)));
} else if !positionals.is_empty() {
return Err(usage_error(
"open does not accept extra positional paths with --left/--right",
Expand Down Expand Up @@ -1090,7 +1121,9 @@ fn parse_sync_preview(args: Vec<String>) -> Result<CliInvocation, CliParseError>
}
}
if positionals.len() != 2 {
return Err(usage_error("sync-preview requires LEFT and RIGHT paths"));
return Err(usage_error(with_help_hint(
"sync-preview needs two folder paths",
)));
}

Ok(CliInvocation {
Expand Down Expand Up @@ -1236,7 +1269,9 @@ pub fn preview_folder_sync_cli(

fn parse_open_session(args: Vec<String>) -> Result<CliInvocation, CliParseError> {
if args.len() != 2 {
return Err(usage_error("open-session requires STORE_ROOT and NAME"));
return Err(usage_error(with_help_hint(
"open-session needs a store root and session name",
)));
}

Ok(CliInvocation {
Expand Down Expand Up @@ -1354,11 +1389,39 @@ fn session_type_label(session_type: &session_core::SessionType) -> &'static str

fn runtime_error(error: impl std::fmt::Debug) -> CliRuntimeError {
CliRuntimeError {
message: format!("{error:?}"),
message: plain_runtime_error_message(&format!("{error:?}")),
exit_code: CliExitCode::IoError,
}
}

fn plain_runtime_error_message(raw: &str) -> String {
if let Some(inner) = raw
.strip_prefix("NotFound(")
.and_then(|value| value.strip_suffix(')'))
{
let path = inner.trim().trim_matches('"');
return format!("Path could not be found: {path}");
}

if let Some(inner) = raw
.strip_prefix("Io(")
.and_then(|value| value.strip_suffix(')'))
{
let detail = inner.trim().trim_matches('"');
return format!("Could not read or write a file: {detail}");
}

if raw.contains("UnsupportedEncoding") {
return "That file uses an unsupported text encoding.".to_owned();
}

if raw.len() > 180 {
return format!("{}...", &raw[..177]);
}

raw.to_owned()
}

fn merge_options_for_favor(favor: Option<CliTextMergeFavor>) -> merge_core::TextMergeOptions {
merge_core::TextMergeOptions {
conflict_policy: match favor {
Expand Down Expand Up @@ -1558,13 +1621,26 @@ mod tests {
#[test]
fn parses_open_session_flags_and_help_text() {
let help = cli_help_text();
assert!(help.contains("Quick start:"));
assert!(help.contains("open-diff-cli left.txt right.txt"));
assert!(help.contains("open [options]"));
assert!(help.contains("--center <path>"));
assert!(help.contains("--left-readonly"));
assert!(help.contains("Desktop / file manager:"));
assert!(!help.contains("%F handoff"));
assert!(help.contains("Exit codes:"));
assert!(help.contains("0 success"));
assert!(help.contains("1 differences detected"));

let bare_unknown = parse_cli_args(["open-diff-cli", "not-a-command"]).expect_err("typo");
assert!(bare_unknown.message.contains("Unknown command"));
assert!(bare_unknown.message.contains("--help"));

let compare_usage =
parse_cli_args(["open-diff-cli", "compare"]).expect_err("compare needs paths");
assert!(compare_usage.message.contains("two paths"));
assert!(compare_usage.message.contains("--help"));

let open = parse_cli_args([
"open-diff-cli",
"open",
Expand Down Expand Up @@ -1922,7 +1998,8 @@ mod tests {
.expect_err("missing right path should fail");

assert_eq!(error.exit_code, CliExitCode::UsageError);
assert!(error.message.contains("compare requires"));
assert!(error.message.contains("two paths"));
assert!(error.message.contains("--help"));
}

#[test]
Expand Down
3 changes: 3 additions & 0 deletions src/app/unixShellScripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ describe('unix shell integration scripts', () => {
expect(script).toContain('MimeType=')
expect(script).toContain('inode/directory')
expect(script).toContain('Actions=SelectLeft')
expect(script).toContain('Compare files or folders')
expect(script).toContain('open-diff-cli --help')
expect(script).not.toContain('via shell-compare')
})

it('uninstalls the linux desktop entry', () => {
Expand Down
10 changes: 6 additions & 4 deletions src/i18n/locales/de-DE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -918,16 +918,17 @@ export const deDE: LanguagePack = {
'ui.installUnixShellIntegration': 'Shell-Integration installieren',
'ui.removeUnixShellIntegration': 'Shell-Integration entfernen',
'ui.unixShellHint':
'Installiert einen .desktop Open-With-Eintrag unter Linux oder einen shell-compare-Helfer plus Open-With-App-Stub unter macOS. Nutzt CLI --shell-compare.',
'ui.unixShellCliHint': 'Auch verfügbar: open-diff-cli shell-compare [--select-left] <path>',
'Fügt unter Linux „Öffnen mit“ hinzu oder installiert unter macOS einen Helfer zum Vergleichen aus dem Dateimanager.',
'ui.unixShellCliHint':
'Im Terminal: open-diff-cli left right oder open-diff-cli shell-compare [--select-left] <path> oder open-diff-cli --help',
'ui.registerShellExtension': 'Explorer-Kontextmenü installieren',
'ui.installExplorerContextMenu': 'Explorer-Kontextmenü installieren',
'ui.removeExplorerContextMenu': 'Explorer-Kontextmenü entfernen',
'status.shellUnregisterScriptGenerated':
'Windows only. Unregister script generated but not applied.',
'ui.windowsOnly': 'Nur Windows',
'ui.shellExtensionHint':
'Fügt „Compare with Open Diff“ und „Select Left File/Folder for Compare“ im Explorer hinzu.',
'Fügt „Mit Open Diff vergleichen“ und „Links zum Vergleichen wählen“ im Explorer hinzu.',
'ui.shellExtensionFlowHint':
'Zwei Elemente auswählen und Compare wählen, oder zuerst Select Left und dann Compare auf der anderen Seite.',
'ui.policyRemoteDisabled':
Expand Down Expand Up @@ -1249,7 +1250,8 @@ export const deDE: LanguagePack = {
'ui.textEditingHint': 'Default wrap applies when opening new text compare and edit sessions.',
'ui.openWithHint': 'Enabled applications appear in Folder Compare Open With actions.',
'ui.applicationName': 'Application name',
'ui.shellOptionsHint': 'Register shell integration so the OS can open paths in OpenDiff.',
'ui.shellOptionsHint':
'Explorer oder Dateimanager sollen Pfade in Open Diff öffnen. Im Terminal: open-diff-cli --help oder open-diff-cli left.txt right.txt.',
'ui.backupRetentionCount': 'Nummerierte Sicherungen behalten',
'ui.commandsVisibility': 'Befehle',
'ui.commandsVisibilityHint':
Expand Down
10 changes: 6 additions & 4 deletions src/i18n/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -901,14 +901,15 @@ export const enUS: LanguagePack = {
'ui.installUnixShellIntegration': 'Install shell integration',
'ui.removeUnixShellIntegration': 'Remove shell integration',
'ui.unixShellHint':
'Installs a .desktop Open With entry on Linux, or a shell-compare helper + Open With app stub on macOS. Reuses CLI --shell-compare.',
'ui.unixShellCliHint': 'Also available: open-diff-cli shell-compare [--select-left] <path>',
'Adds Open With on Linux, or a helper app on macOS, so you can compare from the file manager.',
'ui.unixShellCliHint':
'From a terminal: open-diff-cli left right or open-diff-cli shell-compare [--select-left] <path> or open-diff-cli --help',
'ui.registerShellExtension': 'Install Explorer context menu',
'ui.installExplorerContextMenu': 'Install Explorer context menu',
'ui.removeExplorerContextMenu': 'Remove Explorer context menu',
'ui.windowsOnly': 'Windows only',
'ui.shellExtensionHint':
'Adds Compare with Open Diff and Select Left File/Folder for Compare to Explorer.',
'Adds Compare with Open Diff and Select Left for Compare to File Explorer.',
'ui.shellExtensionFlowHint':
'Select two items and choose Compare, or Select Left then Compare on the other side.',
'ui.policyRemoteDisabled': 'Remote profiles are disabled by administrator policy.',
Expand Down Expand Up @@ -1208,7 +1209,8 @@ export const enUS: LanguagePack = {
'Default wrap and first-difference scrolling apply when opening new text sessions.',
'ui.openWithHint': 'Enabled applications appear in Folder Compare Open With actions.',
'ui.applicationName': 'Application name',
'ui.shellOptionsHint': 'Register shell integration so the OS can open paths in OpenDiff.',
'ui.shellOptionsHint':
'Let File Explorer or your file manager open paths in Open Diff. From a terminal, try open-diff-cli --help or open-diff-cli left.txt right.txt.',
'ui.backupRetentionCount': 'Keep numbered backups',
'ui.commandsVisibility': 'Commands',
'ui.commandsVisibilityHint':
Expand Down
10 changes: 6 additions & 4 deletions src/i18n/locales/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -913,8 +913,9 @@ export const esES: LanguagePack = {
'ui.installUnixShellIntegration': 'Instalar integración de shell',
'ui.removeUnixShellIntegration': 'Quitar integración de shell',
'ui.unixShellHint':
'Instala una entrada .desktop Abrir con en Linux, o un ayudante shell-compare y una app Abrir con en macOS. Reutiliza CLI --shell-compare.',
'ui.unixShellCliHint': 'También disponible: open-diff-cli shell-compare [--select-left] <path>',
'Añade Abrir con en Linux, o un ayudante en macOS, para comparar desde el administrador de archivos.',
'ui.unixShellCliHint':
'Desde una terminal: open-diff-cli left right o open-diff-cli shell-compare [--select-left] <path> o open-diff-cli --help',
'ui.registerShellExtension': 'Instalar menú contextual del Explorador',
'ui.installExplorerContextMenu': 'Instalar menú contextual del Explorador',
'ui.removeExplorerContextMenu': 'Quitar menú contextual del Explorador',
Expand All @@ -925,7 +926,7 @@ export const esES: LanguagePack = {
'Windows only. Unregister script generated but not applied.',
'ui.windowsOnly': 'Solo Windows',
'ui.shellExtensionHint':
'Añade Open Diff al Explorador para archivos y carpetas seleccionados.',
'Añade Comparar con Open Diff y Seleccionar izquierda para comparar en el Explorador.',
'ui.policyRemoteDisabled':
'Los perfiles remotos están deshabilitados por la directiva de administrador.',
'ui.policyPasswordsDisabled':
Expand Down Expand Up @@ -1233,7 +1234,8 @@ export const esES: LanguagePack = {
'ui.textEditingHint': 'Default wrap applies when opening new text compare and edit sessions.',
'ui.openWithHint': 'Enabled applications appear in Folder Compare Open With actions.',
'ui.applicationName': 'Application name',
'ui.shellOptionsHint': 'Register shell integration so the OS can open paths in OpenDiff.',
'ui.shellOptionsHint':
'Permite que el Explorador o el administrador de archivos abran rutas en Open Diff. En terminal: open-diff-cli --help o open-diff-cli left.txt right.txt.',
'ui.backupRetentionCount': 'Conservar copias de seguridad numeradas',
'ui.commandsVisibility': 'Comandos',
'ui.commandsVisibilityHint':
Expand Down
10 changes: 6 additions & 4 deletions src/i18n/locales/fr-FR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -913,8 +913,9 @@ export const frFR: LanguagePack = {
'ui.installUnixShellIntegration': 'Installer l’intégration shell',
'ui.removeUnixShellIntegration': 'Supprimer l’intégration shell',
'ui.unixShellHint':
'Installe une entrée .desktop Ouvrir avec sous Linux, ou un assistant shell-compare et une app Ouvrir avec sous macOS. Réutilise CLI --shell-compare.',
'ui.unixShellCliHint': 'Aussi disponible : open-diff-cli shell-compare [--select-left] <path>',
'Ajoute Ouvrir avec sous Linux, ou un assistant sous macOS, pour comparer depuis le gestionnaire de fichiers.',
'ui.unixShellCliHint':
'Depuis un terminal : open-diff-cli left right ou open-diff-cli shell-compare [--select-left] <path> ou open-diff-cli --help',
'ui.registerShellExtension': 'Installer le menu contextuel Explorateur',
'ui.installExplorerContextMenu': 'Installer le menu contextuel Explorateur',
'ui.removeExplorerContextMenu': 'Supprimer le menu contextuel Explorateur',
Expand All @@ -925,7 +926,7 @@ export const frFR: LanguagePack = {
'Windows only. Unregister script generated but not applied.',
'ui.windowsOnly': 'Windows uniquement',
'ui.shellExtensionHint':
'Ajoute Open Diff à l’Explorateur pour les fichiers et dossiers sélectionnés.',
'Ajoute Comparer avec Open Diff et Sélectionner à gauche pour comparer dans l’Explorateur.',
'ui.policyRemoteDisabled':
'Les profils distants sont désactivés par la stratégie d’administration.',
'ui.policyPasswordsDisabled':
Expand Down Expand Up @@ -1248,7 +1249,8 @@ export const frFR: LanguagePack = {
'ui.textEditingHint': 'Default wrap applies when opening new text compare and edit sessions.',
'ui.openWithHint': 'Enabled applications appear in Folder Compare Open With actions.',
'ui.applicationName': 'Application name',
'ui.shellOptionsHint': 'Register shell integration so the OS can open paths in OpenDiff.',
'ui.shellOptionsHint':
'Permet à l’Explorateur ou au gestionnaire de fichiers d’ouvrir des chemins dans Open Diff. En terminal : open-diff-cli --help ou open-diff-cli left.txt right.txt.',
'ui.backupRetentionCount': 'Conserver les sauvegardes numérotées',
'ui.commandsVisibility': 'Commandes',
'ui.commandsVisibilityHint':
Expand Down
Loading
Loading