mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-07-03 11:12:30 +08:00
Move parsing of macos-launch-services-cmdline into native code
Avoids expensive re-exec and simplifies various things. Much faster for single instance usage.
This commit is contained in:
@@ -239,6 +239,10 @@ workaround that limitation, |kitty| will read command line options from the file
|
||||
:file:`<kitty config dir>/macos-launch-services-cmdline` when it is launched
|
||||
from the GUI, i.e. by clicking the |kitty| application icon or using
|
||||
``open -a kitty``. Note that this file is *only read* when running via the GUI.
|
||||
The contents of the file are assumed to be the command line to pass to kitty in
|
||||
shell syntax, for example::
|
||||
|
||||
--single-instance --override background=red
|
||||
|
||||
You can, of course, also run |kitty| from a terminal with command line options,
|
||||
using: :file:`/Applications/kitty.app/Contents/MacOS/kitty`.
|
||||
|
||||
@@ -652,6 +652,17 @@ abspath(PyObject *self UNUSED, PyObject *path) {
|
||||
return PyUnicode_FromString(buf);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
read_file(PyObject *self UNUSED, PyObject *path) {
|
||||
if (!PyUnicode_Check(path)) { PyErr_SetString(PyExc_TypeError, "path must a string"); return NULL; }
|
||||
size_t sz;
|
||||
char *result = read_full_file(PyUnicode_AsUTF8(path), &sz);
|
||||
if (!result) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
|
||||
PyObject *ans = PyBytes_FromStringAndSize(result, sz);
|
||||
free(result);
|
||||
return ans;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
py_makedirs(PyObject *self UNUSED, PyObject *args) {
|
||||
int mode = 0755; const char *p;
|
||||
@@ -670,6 +681,7 @@ py_get_config_dir(PyObject *self UNUSED, PyObject *args UNUSED) {
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
METHODB(replace_c0_codes_except_nl_space_tab, METH_O),
|
||||
METHODB(read_file, METH_O),
|
||||
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
|
||||
{"expanduser", (PyCFunction)expanduser, METH_O, ""},
|
||||
{"abspath", (PyCFunction)abspath, METH_O, ""},
|
||||
|
||||
81
kitty/launcher/cmdline.c
Normal file
81
kitty/launcher/cmdline.c
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* cmdline.c
|
||||
* Copyright (C) 2025 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
#include "shlex.h"
|
||||
#include "utils.h"
|
||||
#include "launcher.h"
|
||||
|
||||
|
||||
void
|
||||
free_argv_array(argv_array *a) {
|
||||
if (a && a->needs_free) {
|
||||
free(a->buf); free(a->argv);
|
||||
*a = (argv_array){0};
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
add_to_argv(argv_array *a, const char* arg, size_t sz) {
|
||||
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->argv[a->count] = 0;
|
||||
a->capacity = cap;
|
||||
a->needs_free = true;
|
||||
}
|
||||
memcpy(a->buf + a->pos, arg, sz);
|
||||
a->argv[a->count++] = a->buf + a->pos;
|
||||
a->argv[a->count] = 0;
|
||||
a->pos += sz;
|
||||
a->buf[a->pos++] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
get_argv_from(const char *filename, const char *argv0, argv_array *final_ans) {
|
||||
(void)get_config_dir;
|
||||
if (!filename || !filename[0]) return true;
|
||||
size_t src_sz;
|
||||
char* src = read_full_file(filename, &src_sz);
|
||||
if (!src) {
|
||||
fprintf(stderr, "Failed to read from %s ", filename); perror("with error");
|
||||
return false;
|
||||
}
|
||||
ShlexState s = {0};
|
||||
argv_array ans = {0};
|
||||
bool ok = false;
|
||||
ans.buf = malloc(src_sz + strlen(argv0) + 64);
|
||||
if (!ans.buf) { errno = ENOMEM; goto end; }
|
||||
ans.needs_free = true;
|
||||
if (!add_to_argv(&ans, argv0, strlen(argv0))) goto end;
|
||||
if (!alloc_shlex_state(&s, src, src_sz, false)) { errno = ENOMEM; goto end; }
|
||||
bool keep_going = true;
|
||||
while (keep_going) {
|
||||
ssize_t q = next_word(&s);
|
||||
switch(q) {
|
||||
case -1: fprintf(stderr, "Failed to parse %s with error: %s\n", filename, s.err); goto end;
|
||||
case -2: keep_going = false; break;
|
||||
default:
|
||||
if (ans.count == 1 && strcmp(s.buf, "kitty") == 0) continue;
|
||||
if (!add_to_argv(&ans, s.buf, q)) { goto end; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
ok = true;
|
||||
end:
|
||||
free(src); dealloc_shlex_state(&s);
|
||||
if (ok) *final_ans = ans;
|
||||
else {
|
||||
free_argv_array(&ans);
|
||||
fprintf(stderr, "Failed to read from %s ", filename); perror("with error");
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct CLIOptions {
|
||||
const char *session, *instance_group, *detached_log;
|
||||
@@ -16,5 +17,12 @@ typedef struct CLIOptions {
|
||||
} CLIOptions;
|
||||
|
||||
|
||||
void
|
||||
single_instance_main(int argc, char *argv[], const CLIOptions *opts);
|
||||
typedef struct argv_array {
|
||||
char **argv, *buf; size_t capacity, count, pos;
|
||||
bool needs_free;
|
||||
} 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);
|
||||
void free_argv_array(argv_array *a);
|
||||
|
||||
@@ -92,67 +92,13 @@ set_kitty_run_data(RunData *run_data, bool from_source, wchar_t *extensions_dir)
|
||||
#ifdef FOR_BUNDLE
|
||||
#include <bypy-freeze.h>
|
||||
|
||||
static bool
|
||||
canonicalize_path(const char *srcpath, char *dstpath, size_t sz) {
|
||||
// remove . and .. path segments
|
||||
bool ok = false;
|
||||
size_t plen = strlen(srcpath) + 1, chk;
|
||||
RAII_ALLOC(char, wtmp, malloc(plen));
|
||||
RAII_ALLOC(char*, tokv, malloc(sizeof(char*) * plen));
|
||||
if (!wtmp || !tokv) goto end;
|
||||
char *s, *tok, *sav;
|
||||
bool relpath = *srcpath != '/';
|
||||
|
||||
// use a buffer as strtok modifies its input
|
||||
memcpy(wtmp, srcpath, plen);
|
||||
|
||||
tok = strtok_r(wtmp, "/", &sav);
|
||||
int ti = 0;
|
||||
while (tok != NULL) {
|
||||
if (strcmp(tok, "..") == 0) {
|
||||
if (ti > 0) ti--;
|
||||
} else if (strcmp(tok, ".") != 0) {
|
||||
tokv[ti++] = tok;
|
||||
}
|
||||
tok = strtok_r(NULL, "/", &sav);
|
||||
}
|
||||
|
||||
chk = 0;
|
||||
s = dstpath;
|
||||
for (int i = 0; i < ti; i++) {
|
||||
size_t token_sz = strlen(tokv[i]);
|
||||
|
||||
if (i > 0 || !relpath) {
|
||||
if (++chk >= sz) goto end;
|
||||
*s++ = '/';
|
||||
}
|
||||
|
||||
chk += token_sz;
|
||||
if (chk >= sz) goto end;
|
||||
|
||||
memcpy(s, tokv[i], token_sz);
|
||||
s += token_sz;
|
||||
}
|
||||
|
||||
if (s == dstpath) {
|
||||
if (++chk >= sz) goto end;
|
||||
*s++ = relpath ? '.' : '/';
|
||||
}
|
||||
*s = '\0';
|
||||
ok = true;
|
||||
|
||||
end:
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool
|
||||
static void
|
||||
canonicalize_path_wide(const char *srcpath, wchar_t *dest, size_t sz) {
|
||||
char buf[sz + 1];
|
||||
bool ret = canonicalize_path(srcpath, buf, sz);
|
||||
lexical_absolute_path(srcpath, buf, sz);
|
||||
buf[sz] = 0;
|
||||
mbstowcs(dest, buf, sz - 1);
|
||||
dest[sz-1] = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
@@ -164,18 +110,12 @@ run_embedded(RunData *run_data) {
|
||||
#else
|
||||
const char *python_relpath = "../" KITTY_LIB_DIR_NAME;
|
||||
#endif
|
||||
int num = safe_snprintf(extensions_dir_full, PATH_MAX, "%s/%s/kitty-extensions", run_data->exe_dir, python_relpath);
|
||||
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to extensions_dir: %s/%s\n", run_data->exe_dir, python_relpath); return 1; }
|
||||
wchar_t extensions_dir[num+2];
|
||||
if (!canonicalize_path_wide(extensions_dir_full, extensions_dir, num+1)) {
|
||||
fprintf(stderr, "Failed to canonicalize the path: %s\n", extensions_dir_full); return 1; }
|
||||
|
||||
num = snprintf(python_home_full, PATH_MAX, "%s/%s/python%s", run_data->exe_dir, python_relpath, PYVER);
|
||||
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to python home: %s/%s\n", run_data->exe_dir, python_relpath); return 1; }
|
||||
wchar_t python_home[num+2];
|
||||
if (!canonicalize_path_wide(python_home_full, python_home, num+1)) {
|
||||
fprintf(stderr, "Failed to canonicalize the path: %s\n", python_home_full); return 1; }
|
||||
|
||||
safe_snprintf(extensions_dir_full, PATH_MAX, "%s/%s/kitty-extensions", run_data->exe_dir, python_relpath);
|
||||
wchar_t extensions_dir[PATH_MAX];
|
||||
canonicalize_path_wide(extensions_dir_full, extensions_dir, PATH_MAX);
|
||||
safe_snprintf(python_home_full, PATH_MAX, "%s/%s/python%s", run_data->exe_dir, python_relpath, PYVER);
|
||||
wchar_t python_home[PATH_MAX];
|
||||
canonicalize_path_wide(python_home_full, python_home, PATH_MAX);
|
||||
bypy_initialize_interpreter(
|
||||
L"kitty", python_home, L"kitty_main", extensions_dir, run_data->argc, run_data->argv);
|
||||
if (!set_kitty_run_data(run_data, false, extensions_dir)) return 1;
|
||||
@@ -498,29 +438,36 @@ delegate_to_kitten_if_possible(int argc, char *argv[], char* exe_dir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[], char* envp[]) {
|
||||
if (argc < 1 || !argv) { fprintf(stderr, "Invalid argc/argv\n"); return 1; }
|
||||
int main(int argc_, char *argv_[], char* envp[]) {
|
||||
if (argc_ < 1 || !argv_) { fprintf(stderr, "Invalid argc/argv\n"); return 1; }
|
||||
if (!ensure_working_stdio()) return 1;
|
||||
char exe[PATH_MAX+1] = {0};
|
||||
char exe_dir_buf[PATH_MAX+1] = {0};
|
||||
RAII_ALLOC(const char, lc_ctype, NULL);
|
||||
bool launched_by_launch_services = false;
|
||||
const char *config_dir = NULL;
|
||||
argv_array argva = {.argv = argv_, .count = argc_};
|
||||
#ifdef __APPLE__
|
||||
lc_ctype = getenv("LC_CTYPE");
|
||||
if (lc_ctype) lc_ctype = strdup(lc_ctype);
|
||||
char abuf[PATH_MAX+1];
|
||||
if (getenv("KITTY_LAUNCHED_BY_LAUNCH_SERVICES")) {
|
||||
launched_by_launch_services = true;
|
||||
unsetenv("KITTY_LAUNCHED_BY_LAUNCH_SERVICES");
|
||||
char buf[PATH_MAX+1];
|
||||
if (!get_config_dir(buf,sizeof(buf))) buf[0] = 0;
|
||||
config_dir = buf;
|
||||
if (!get_config_dir(abuf, sizeof(abuf))) abuf[0] = 0;
|
||||
config_dir = abuf;
|
||||
if (launched_by_launch_services && config_dir[0]) {
|
||||
char cbuf[PATH_MAX];
|
||||
safe_snprintf(cbuf, sizeof(cbuf), "%s/macos-launch-services-cmdline", config_dir);
|
||||
if (!get_argv_from(cbuf, argva.argv[0], &argva)) exit(1);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
(void)read_full_file;
|
||||
if (!read_exe_path(exe, sizeof(exe))) return 1;
|
||||
strncpy(exe_dir_buf, exe, sizeof(exe_dir_buf));
|
||||
char *exe_dir = dirname(exe_dir_buf);
|
||||
if (!delegate_to_kitten_if_possible(argc, argv, exe_dir)) handle_fast_commandline(argc, argv, NULL);
|
||||
if (!delegate_to_kitten_if_possible(argva.count, argva.argv, exe_dir)) handle_fast_commandline(argva.count, argva.argv, NULL);
|
||||
int ret=0;
|
||||
char lib[PATH_MAX+1] = {0};
|
||||
if (KITTY_LIB_PATH[0] == '/') {
|
||||
@@ -529,10 +476,11 @@ int main(int argc, char *argv[], char* envp[]) {
|
||||
safe_snprintf(lib, PATH_MAX, "%s/%s", exe_dir, KITTY_LIB_PATH);
|
||||
}
|
||||
RunData run_data = {
|
||||
.exe = exe, .exe_dir = exe_dir, .lib_dir = lib, .argc = argc, .argv = argv, .lc_ctype = lc_ctype,
|
||||
.exe = exe, .exe_dir = exe_dir, .lib_dir = lib, .argc = argva.count, .argv = argva.argv, .lc_ctype = lc_ctype,
|
||||
.launched_by_launch_services=launched_by_launch_services, .config_dir = config_dir,
|
||||
};
|
||||
ret = run_embedded(&run_data);
|
||||
free_argv_array(&argva);
|
||||
single_instance_main(-1, NULL, NULL);
|
||||
Py_FinalizeEx();
|
||||
return ret;
|
||||
|
||||
@@ -86,6 +86,7 @@ write_unich(ShlexState *self, unsigned long ch) {
|
||||
static size_t
|
||||
get_word(ShlexState *self) {
|
||||
size_t ans = self->buf_pos; self->buf_pos = 0;
|
||||
self->buf[ans] = 0;
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
@@ -195,3 +195,40 @@ get_config_dir(char *output, size_t outputsz) {
|
||||
#undef expand
|
||||
#undef check_and_ret
|
||||
}
|
||||
|
||||
|
||||
static ssize_t
|
||||
safe_read_stream(void* ptr, size_t size, FILE* stream) {
|
||||
errno = 0;
|
||||
ssize_t total = 0, bytes_to_read = size;
|
||||
while (total < bytes_to_read) {
|
||||
size_t n = fread((char*)ptr + total, 1, bytes_to_read - total, stream);
|
||||
if (n > 0) total += n;
|
||||
else {
|
||||
if (!ferror(stream)) break; // eof
|
||||
if (errno != EINTR) return -1;
|
||||
clearerr(stream);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
static char*
|
||||
read_full_file(const char* filename, size_t *sz) {
|
||||
FILE* file = fopen(filename, "rb");
|
||||
if (!file) return NULL;
|
||||
fseek(file, 0, SEEK_END);
|
||||
unsigned long file_size = ftell(file);
|
||||
rewind(file);
|
||||
char* buffer = (char*)malloc(file_size + 1); // +1 for the null terminator
|
||||
if (!buffer) {
|
||||
errno = ENOMEM;
|
||||
fclose(file);
|
||||
return NULL;
|
||||
}
|
||||
ssize_t q = safe_read_stream(buffer, file_size, file);
|
||||
fclose(file);
|
||||
if (q < 0) { free(buffer); buffer = NULL; if (sz) *sz = 0; }
|
||||
else { if (sz) { *sz = q; } buffer[q] = 0; }
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ from .constants import (
|
||||
appname,
|
||||
beam_cursor_data_file,
|
||||
clear_handled_signals,
|
||||
config_dir,
|
||||
glfw_path,
|
||||
is_macos,
|
||||
is_wayland,
|
||||
@@ -63,7 +62,6 @@ from .utils import (
|
||||
log_error,
|
||||
parse_os_window_state,
|
||||
safe_mtime,
|
||||
shlex_split,
|
||||
startup_notification_handler,
|
||||
)
|
||||
|
||||
@@ -337,30 +335,6 @@ def setup_profiling() -> Generator[None, None, None]:
|
||||
print('To view the graphical call data, use: kcachegrind', cg)
|
||||
|
||||
|
||||
def macos_cmdline(argv_args: list[str]) -> list[str]:
|
||||
try:
|
||||
with open(os.path.join(config_dir, 'macos-launch-services-cmdline')) as f:
|
||||
raw = f.read()
|
||||
except FileNotFoundError:
|
||||
return argv_args
|
||||
raw = raw.strip()
|
||||
ans = list(shlex_split(raw))
|
||||
if ans and ans[0] == 'kitty':
|
||||
del ans[0]
|
||||
if '-1' in ans or '--single-instance' in ans:
|
||||
if 'KITTY_SI_DATA' in os.environ:
|
||||
# C code will already have setup single instance
|
||||
log_error(
|
||||
'--single-instance supplied in both command line arguments and macos-launch-services-cmdline,'
|
||||
' ignoring any --instance-group in macos-launch-services-cmdline')
|
||||
else:
|
||||
# Re-exec with new argv so that the C code that handles single instance
|
||||
# can pick up the modified argv
|
||||
os.environ['KITTY_LAUNCHED_BY_LAUNCH_SERVICES'] = '2' # so that use_os_log is set in the re-execed process
|
||||
os.execl(kitty_exe(), 'kitty', *(ans + argv_args))
|
||||
return ans + argv_args
|
||||
|
||||
|
||||
def expand_listen_on(listen_on: str, from_config_file: bool) -> str:
|
||||
if from_config_file and listen_on == 'none':
|
||||
return ''
|
||||
@@ -483,7 +457,6 @@ def _main() -> None:
|
||||
args = sys.argv[1:]
|
||||
if is_macos and launched_by_launch_services:
|
||||
os.chdir(os.path.expanduser('~'))
|
||||
args = macos_cmdline(args)
|
||||
set_use_os_log(True)
|
||||
try:
|
||||
cwd_ok = os.path.isdir(os.getcwd())
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from kitty.constants import read_kitty_resource
|
||||
from kitty.constants import is_macos, kitty_exe, read_kitty_resource
|
||||
from kitty.fast_data_types import (
|
||||
Color,
|
||||
HistoryBuf,
|
||||
@@ -19,6 +20,7 @@ from kitty.fast_data_types import (
|
||||
get_config_dir,
|
||||
makedirs,
|
||||
parse_input_from_terminal,
|
||||
read_file,
|
||||
replace_c0_codes_except_nl_space_tab,
|
||||
split_into_graphemes,
|
||||
strip_csi,
|
||||
@@ -492,9 +494,22 @@ class TestDataTypes(BaseTest):
|
||||
if os.path.exists(dot_config):
|
||||
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()
|
||||
if is_macos:
|
||||
env = os.environ.copy()
|
||||
env['KITTY_CONFIG_DIRECTORY'] = tdir
|
||||
env['KITTY_LAUNCHED_BY_LAUNCH_SERVICES'] = '1'
|
||||
actual = subprocess.check_output([kitty_exe(), '+runpy', 'import json, sys; print(json.dumps(sys.argv))'], env=env).strip().decode()
|
||||
self.ae('next-line', actual)
|
||||
os.makedirs(tdir + '/good/kitty')
|
||||
open(tdir + '/good/kitty/kitty.conf', 'w').close()
|
||||
open(tdir + '/f', 'w').close()
|
||||
data = os.urandom(32879)
|
||||
with open(tdir + '/f', 'wb') as f:
|
||||
f.write(data)
|
||||
self.ae(data, read_file(f.name))
|
||||
for x in (
|
||||
(f'KITTY_CONFIG_DIRECTORY={tdir}', f'{tdir}'),
|
||||
(f'XDG_CONFIG_HOME={tdir}/good', f'{tdir}/good/kitty'),
|
||||
|
||||
2
setup.py
2
setup.py
@@ -1345,7 +1345,7 @@ def build_launcher(args: Options, launcher_dir: str = '.', bundle_type: str = 's
|
||||
objects = []
|
||||
cppflags.append('-DKITTY_CLI_BOOL_OPTIONS=" ' + ' '.join(kitty_cli_boolean_options()) + ' "')
|
||||
cppflags.append('-DKITTY_VERSION="' + '.'.join(map(str, version)) + '"')
|
||||
for src in ('kitty/launcher/main.c', 'kitty/launcher/single-instance.c'):
|
||||
for src in ('kitty/launcher/main.c', 'kitty/launcher/single-instance.c', 'kitty/launcher/cmdline.c'):
|
||||
obj = os.path.join(build_dir, src.replace('/', '-').replace('.c', '.o'))
|
||||
objects.append(obj)
|
||||
cmd = env.cc + cppflags + cflags + ['-c', src, '-o', obj]
|
||||
|
||||
Reference in New Issue
Block a user