diff --git a/docs/changelog.rst b/docs/changelog.rst index fe09c097a..9db7b9a61 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -259,6 +259,8 @@ Detailed list of changes - Fix setting :opt:`momentum_scroll` to zero not *fully* disabling momentum scrolling (:iss:`9904`) +- macOS: Fix args passed via ``open --args`` being ignored when :file:`macos-launch-services-cmdline` is present (:iss:`9910`) + 0.46.2 [2026-03-21] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/launcher/cmdline.c b/kitty/launcher/cmdline.c index b584a85fc..eb9294e0e 100644 --- a/kitty/launcher/cmdline.c +++ b/kitty/launcher/cmdline.c @@ -13,6 +13,24 @@ #include #endif +bool +append_arg_to_argv_array(argv_array *a, const char *arg) { + if (a->count + 2 > a->capacity) { + size_t cap = a->capacity * 2; + if (!cap) cap = 256; + void *m = realloc(a->argv, cap * sizeof(a->argv[0])); + if (!m) return false; + a->argv = m; + a->capacity = cap; + a->needs_free = true; + } + // arg points into the process's original argv which persists for the lifetime + // of the process and is never modified through this pointer. + a->argv[a->count++] = (char*)arg; + a->argv[a->count] = 0; + return true; +} + void free_argv_array(argv_array *a) { if (a && a->needs_free) { diff --git a/kitty/launcher/launcher.h b/kitty/launcher/launcher.h index 70570ea1b..403e16946 100644 --- a/kitty/launcher/launcher.h +++ b/kitty/launcher/launcher.h @@ -25,4 +25,5 @@ typedef struct argv_array { void single_instance_main(int argc, char *argv[], const CLIOptions *opts); bool get_argv_from(const char *filename, const char* argv0, argv_array *ans); +bool append_arg_to_argv_array(argv_array *a, const char *arg); void free_argv_array(argv_array *a); diff --git a/kitty/launcher/main.c b/kitty/launcher/main.c index b892215f6..591a3015b 100644 --- a/kitty/launcher/main.c +++ b/kitty/launcher/main.c @@ -539,7 +539,19 @@ main(int argc_, char *argv_[], char* envp[]) { if (launched_by_launch_services && config_dir[0]) { char cbuf[PATH_MAX]; safe_snprintf(cbuf, sizeof(cbuf), "%s/macos-launch-services-cmdline", config_dir); + int orig_argc = argc_; char **orig_argv = argv_; if (!get_argv_from(cbuf, argva.argv[0], &argva)) exit(1); + // If the file was loaded (argva replaced), append any extra args passed via open --args + if (argva.needs_free) { + for (int i = 1; i < orig_argc; i++) { + // Skip Apple-internal process serial number args (-psn_*) + if (strncmp(orig_argv[i], "-psn_", sizeof("-psn_") - 1) == 0) continue; + if (!append_arg_to_argv_array(&argva, orig_argv[i])) { + fprintf(stderr, "Out of memory while processing launch services args\n"); + exit(1); + } + } + } } } #else diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index ca04e4610..cc462d647 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -650,19 +650,26 @@ class TestDataTypes(BaseTest): shutil.rmtree(dot_config) with tempfile.TemporaryDirectory() as tdir: with open(tdir + '/macos-launch-services-cmdline', 'w') as f: - print('kitty +runpy "import sys; print(sys.argv[-1])"', file=f) - print('next-line', file=f) - print() + print('kitty --title from-file', file=f) if is_macos: env = os.environ.copy() env['KITTY_CONFIG_DIRECTORY'] = tdir env['KITTY_LAUNCHED_BY_LAUNCH_SERVICES'] = '1' - cp = subprocess.run([kitty_exe(), '+runpy', 'import json, sys; print(json.dumps(sys.argv))'], env=env, stdout=subprocess.PIPE) - actual = cp.stdout.strip().decode() + # Test 1: file args are loaded when no extra user args are present + cp = subprocess.run([kitty_exe(), '+testing-launcher-code'], env=env, stdout=subprocess.PIPE) + actual = cp.stdout.decode() if cp.returncode != 0: print(actual) - raise AssertionError(f'kitty +runpy failed with return code: {cp.returncode}') - self.ae('next-line', actual) + raise AssertionError(f'kitty +testing-launcher-code failed with return code: {cp.returncode}') + self.assertIn('from-file', actual) + # Test 2: extra args passed via open --args are appended after file args + cp = subprocess.run([kitty_exe(), '+testing-launcher-code', '--title', 'from-args'], env=env, stdout=subprocess.PIPE) + actual = cp.stdout.decode() + if cp.returncode != 0: + print(actual) + raise AssertionError(f'kitty +testing-launcher-code failed with return code: {cp.returncode}') + # from-args overrides from-file because user args are appended after file args + self.assertIn('from-args', actual) os.makedirs(tdir + '/good/kitty') open(tdir + '/good/kitty/kitty.conf', 'w').close() data = os.urandom(32879) diff --git a/kitty_tests/options.py b/kitty_tests/options.py index c72a9cfe1..a75700893 100644 --- a/kitty_tests/options.py +++ b/kitty_tests/options.py @@ -125,14 +125,18 @@ version def launcher(self): + import tempfile + from kitty.constants import is_macos kexe = kitty_exe() cfgdir = None - def get_report(cmdline: str, launch_services= False): + def get_report(cmdline: str, launch_services= False, config_dir: str = ''): nonlocal cfgdir args = list(shlex_split(cmdline)) env = dict(os.environ) if launch_services: env['KITTY_LAUNCHED_BY_LAUNCH_SERVICES'] = '1' + if config_dir: + env['KITTY_CONFIG_DIRECTORY'] = config_dir cp = subprocess.run([kexe, "+testing-launcher-code"] + args, env=env, stdout=subprocess.PIPE) self.assertEqual(cp.returncode, 0) ans = {} @@ -200,6 +204,17 @@ def launcher(self): dt('--detach --session=moose --detached-log=xyz', detached_log='xyz', session='moose') pn('+kitten panel -1 --edge=left', edge='left') + if is_macos: + with tempfile.TemporaryDirectory() as tdir: + with open(tdir + '/macos-launch-services-cmdline', 'w') as f: + f.write('kitty --title from-file\n') + # File args are used when launched by launch services + r, output = get_report('', launch_services=True, config_dir=tdir) + self.assertEqual(r.get('title'), 'from-file', f'Expected title=from-file in:\n{output}') + # User args passed via open --args are appended after file args (and override them) + r, output = get_report('--title from-args', launch_services=True, config_dir=tdir) + self.assertEqual(r.get('title'), 'from-args', f'Expected title=from-args to override from-file in:\n{output}') + def conf_parsing(self): from kitty.config import defaults, load_config