Files
kitty/kitty/main.py
Kovid Goyal 9135387cfa Get rid of the various different launchers
Now there is only one launcher. Which means it can be used to start
kitty with profiling and ASAN in the natural way. The recommended
way to run kitty from source is now:

./kitty/launcher/kitty

The launcher also automatically re-execs to resolve symlinks on macOS.
2019-06-24 17:21:30 +05:30

283 lines
9.6 KiB
Python

#!/usr/bin/env python3
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import locale
import os
import shutil
import sys
from contextlib import contextmanager, suppress
from .borders import load_borders_program
from .boss import Boss
from .child import set_default_env
from .cli import create_opts, parse_args
from .config import cached_values_for, initial_window_size_func
from .constants import (
appname, beam_cursor_data_file, config_dir, glfw_path, is_macos,
is_wayland, kitty_exe, logo_data_file
)
from .fast_data_types import (
GLFW_IBEAM_CURSOR, GLFW_MOD_SUPER, create_os_window, free_font_data,
glfw_init, glfw_terminate, load_png_data, set_custom_cursor,
set_default_window_icon, set_options
)
from .fonts.box_drawing import set_scale
from .fonts.render import set_font_family
from .utils import (
detach, log_error, single_instance, startup_notification_handler,
unix_socket_paths
)
from .window import load_shader_programs
def set_custom_ibeam_cursor():
with open(beam_cursor_data_file, 'rb') as f:
data = f.read()
rgba_data, width, height = load_png_data(data)
c2x = os.path.splitext(beam_cursor_data_file)
with open(c2x[0] + '@2x' + c2x[1], 'rb') as f:
data = f.read()
rgba_data2, width2, height2 = load_png_data(data)
images = (rgba_data, width, height), (rgba_data2, width2, height2)
try:
set_custom_cursor(GLFW_IBEAM_CURSOR, images, 4, 8)
except Exception as e:
log_error('Failed to set custom beam cursor with error: {}'.format(e))
def talk_to_instance(args):
import json
import socket
data = {'cmd': 'new_instance', 'args': tuple(sys.argv),
'startup_id': os.environ.get('DESKTOP_STARTUP_ID'),
'cwd': os.getcwd()}
notify_socket = None
if args.wait_for_single_instance_window_close:
address = '\0{}-os-window-close-notify-{}-{}'.format(appname, os.getpid(), os.geteuid())
notify_socket = socket.socket(family=socket.AF_UNIX)
try:
notify_socket.bind(address)
except FileNotFoundError:
for address in unix_socket_paths(address[1:], ext='.sock'):
notify_socket.bind(address)
break
data['notify_on_os_window_death'] = address
notify_socket.listen()
data = json.dumps(data, ensure_ascii=False).encode('utf-8')
single_instance.socket.sendall(data)
with suppress(EnvironmentError):
single_instance.socket.shutdown(socket.SHUT_RDWR)
single_instance.socket.close()
if args.wait_for_single_instance_window_close:
conn = notify_socket.accept()[0]
conn.recv(1)
with suppress(EnvironmentError):
conn.shutdown(socket.SHUT_RDWR)
conn.close()
def load_all_shaders(semi_transparent=0):
load_shader_programs(semi_transparent)
load_borders_program()
def init_glfw(opts, debug_keyboard=False):
glfw_module = 'cocoa' if is_macos else ('wayland' if is_wayland(opts) else 'x11')
if not glfw_init(glfw_path(glfw_module), debug_keyboard):
raise SystemExit('GLFW initialization failed')
return glfw_module
def prefer_cmd_shortcuts(x):
return x[0] == GLFW_MOD_SUPER
def get_new_os_window_trigger(opts):
new_os_window_trigger = None
if is_macos:
new_os_window_shortcuts = []
for k, v in opts.keymap.items():
if v.func == 'new_os_window':
new_os_window_shortcuts.append(k)
if new_os_window_shortcuts:
from .fast_data_types import cocoa_set_new_window_trigger
new_os_window_shortcuts.sort(key=prefer_cmd_shortcuts, reverse=True)
for candidate in new_os_window_shortcuts:
if cocoa_set_new_window_trigger(candidate[0], candidate[2]):
new_os_window_trigger = candidate
break
return new_os_window_trigger
def _run_app(opts, args, bad_lines=()):
new_os_window_trigger = get_new_os_window_trigger(opts)
if is_macos and opts.macos_custom_beam_cursor:
set_custom_ibeam_cursor()
if not is_wayland() and not is_macos: # no window icons on wayland
with open(logo_data_file, 'rb') as f:
set_default_window_icon(f.read(), 256, 256)
load_shader_programs.use_selection_fg = opts.selection_foreground is not None
with cached_values_for(run_app.cached_values_name) as cached_values:
with startup_notification_handler(extra_callback=run_app.first_window_callback) as pre_show_callback:
window_id = create_os_window(
run_app.initial_window_size_func(opts, cached_values),
pre_show_callback,
appname, args.name or args.cls or appname,
args.cls or appname, load_all_shaders)
boss = Boss(window_id, opts, args, cached_values, new_os_window_trigger)
boss.start()
if bad_lines:
boss.show_bad_config_lines(bad_lines)
try:
boss.child_monitor.main_loop()
finally:
boss.destroy()
def run_app(opts, args, bad_lines=()):
set_scale(opts.box_drawing_scale)
set_options(opts, is_wayland(), args.debug_gl, args.debug_font_fallback)
set_font_family(opts, debug_font_matching=args.debug_font_fallback)
try:
_run_app(opts, args, bad_lines)
finally:
free_font_data() # must free font data before glfw/freetype/fontconfig/opengl etc are finalized
run_app.cached_values_name = 'main'
run_app.first_window_callback = lambda window_handle: None
run_app.initial_window_size_func = initial_window_size_func
def ensure_macos_locale():
# Ensure the LANG env var is set. See
# https://github.com/kovidgoyal/kitty/issues/90
from .fast_data_types import cocoa_get_lang
if 'LANG' not in os.environ:
lang = cocoa_get_lang()
if lang is not None:
os.environ['LANG'] = lang + '.UTF-8'
@contextmanager
def setup_profiling(args):
try:
from .fast_data_types import start_profiler, stop_profiler
except ImportError:
start_profiler = stop_profiler = None
if start_profiler is not None:
start_profiler('/tmp/kitty-profile.log')
yield
if stop_profiler is not None:
import subprocess
stop_profiler()
exe = kitty_exe()
cg = '/tmp/kitty-profile.callgrind'
print('Post processing profile data for', exe, '...')
subprocess.call(['pprof', '--callgrind', exe, '/tmp/kitty-profile.log'], stdout=open(cg, 'wb'))
try:
subprocess.Popen(['kcachegrind', cg])
except FileNotFoundError:
subprocess.call(['pprof', '--text', exe, '/tmp/kitty-profile.log'])
print('To view the graphical call data, use: kcachegrind', cg)
def macos_cmdline(argv_args):
try:
with open(os.path.join(config_dir, 'macos-launch-services-cmdline')) as f:
raw = f.read()
except FileNotFoundError:
return argv_args
import shlex
raw = raw.strip()
ans = shlex.split(raw)
if ans and ans[0] == 'kitty':
del ans[0]
return ans
def setup_environment(opts, args):
extra_env = opts.env.copy()
if opts.editor != '.':
os.environ['EDITOR'] = opts.editor
if args.listen_on:
os.environ['KITTY_LISTEN_ON'] = args.listen_on
set_default_env(extra_env)
def _main():
with suppress(AttributeError): # python compiled without threading
sys.setswitchinterval(1000.0) # we have only a single python thread
if is_macos:
ensure_macos_locale()
try:
locale.setlocale(locale.LC_ALL, '')
except Exception:
if not is_macos:
raise
log_error('Failed to set locale with LANG:', os.environ.get('LANG'))
os.environ.pop('LANG', None)
try:
locale.setlocale(locale.LC_ALL, '')
except Exception:
log_error('Failed to set locale with no LANG, ignoring')
# Ensure the correct kitty is in PATH
rpath = sys._xoptions.get('bundle_exe_dir')
if rpath:
modify_path = is_macos or getattr(sys, 'frozen', False) or sys._xoptions.get('kitty_from_source') == '1'
if modify_path or not shutil.which('kitty'):
existing_paths = list(filter(None, os.environ.get('PATH', '').split(os.pathsep)))
existing_paths.insert(0, rpath)
os.environ['PATH'] = os.pathsep.join(existing_paths)
args = sys.argv[1:]
if is_macos and os.environ.pop('KITTY_LAUNCHED_BY_LAUNCH_SERVICES', None) == '1':
os.chdir(os.path.expanduser('~'))
args = macos_cmdline(args)
try:
cwd_ok = os.path.isdir(os.getcwd())
except Exception:
cwd_ok = False
if not cwd_ok:
os.chdir(os.path.expanduser('~'))
args, rest = parse_args(args=args)
args.args = rest
if args.debug_config:
create_opts(args, debug_config=True)
return
if getattr(args, 'detach', False):
detach()
if args.replay_commands:
from kitty.client import main
main(args.replay_commands)
return
if args.single_instance:
is_first = single_instance(args.instance_group)
if not is_first:
talk_to_instance(args)
return
bad_lines = []
opts = create_opts(args, accumulate_bad_lines=bad_lines)
init_glfw(opts, args.debug_keyboard)
setup_environment(opts, args)
try:
with setup_profiling(args):
# Avoid needing to launch threads to reap zombies
run_app(opts, args, bad_lines)
finally:
glfw_terminate()
def main():
try:
_main()
except Exception:
import traceback
tb = traceback.format_exc()
log_error(tb)
raise SystemExit(1)