1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> 4 5import locale 6import os 7import shutil 8import sys 9from contextlib import contextmanager, suppress 10from typing import Dict, Generator, List, Optional, Sequence, Tuple 11 12from .borders import load_borders_program 13from .boss import Boss 14from .child import set_default_env 15from .cli import create_opts, parse_args 16from .cli_stub import CLIOptions 17from .conf.utils import BadLine 18from .config import cached_values_for 19from .constants import ( 20 appname, beam_cursor_data_file, config_dir, glfw_path, is_macos, 21 is_wayland, kitty_exe, logo_png_file, running_in_kitty 22) 23from .fast_data_types import ( 24 GLFW_IBEAM_CURSOR, GLFW_MOD_ALT, GLFW_MOD_SHIFT, create_os_window, 25 free_font_data, glfw_init, glfw_terminate, load_png_data, 26 set_custom_cursor, set_default_window_icon, set_options 27) 28from .fonts.box_drawing import set_scale 29from .fonts.render import set_font_family 30from .options.types import Options 31from .os_window_size import initial_window_size_func 32from .session import get_os_window_sizing_data 33from .types import SingleKey 34from .utils import ( 35 detach, expandvars, log_error, single_instance, 36 startup_notification_handler, unix_socket_paths 37) 38from .window import load_shader_programs 39 40 41def set_custom_ibeam_cursor() -> None: 42 with open(beam_cursor_data_file, 'rb') as f: 43 data = f.read() 44 rgba_data, width, height = load_png_data(data) 45 c2x = os.path.splitext(beam_cursor_data_file) 46 with open(c2x[0] + '@2x' + c2x[1], 'rb') as f: 47 data = f.read() 48 rgba_data2, width2, height2 = load_png_data(data) 49 images = (rgba_data, width, height), (rgba_data2, width2, height2) 50 try: 51 set_custom_cursor(GLFW_IBEAM_CURSOR, images, 4, 8) 52 except Exception as e: 53 log_error('Failed to set custom beam cursor with error: {}'.format(e)) 54 55 56def talk_to_instance(args: CLIOptions) -> None: 57 import json 58 import socket 59 data = {'cmd': 'new_instance', 'args': tuple(sys.argv), 60 'startup_id': os.environ.get('DESKTOP_STARTUP_ID'), 61 'cwd': os.getcwd()} 62 notify_socket = None 63 if args.wait_for_single_instance_window_close: 64 address = '\0{}-os-window-close-notify-{}-{}'.format(appname, os.getpid(), os.geteuid()) 65 notify_socket = socket.socket(family=socket.AF_UNIX) 66 try: 67 notify_socket.bind(address) 68 except FileNotFoundError: 69 for address in unix_socket_paths(address[1:], ext='.sock'): 70 notify_socket.bind(address) 71 break 72 data['notify_on_os_window_death'] = address 73 notify_socket.listen() 74 75 sdata = json.dumps(data, ensure_ascii=False).encode('utf-8') 76 assert single_instance.socket is not None 77 single_instance.socket.sendall(sdata) 78 with suppress(OSError): 79 single_instance.socket.shutdown(socket.SHUT_RDWR) 80 single_instance.socket.close() 81 82 if args.wait_for_single_instance_window_close: 83 assert notify_socket is not None 84 conn = notify_socket.accept()[0] 85 conn.recv(1) 86 with suppress(OSError): 87 conn.shutdown(socket.SHUT_RDWR) 88 conn.close() 89 90 91def load_all_shaders(semi_transparent: bool = False) -> None: 92 load_shader_programs(semi_transparent) 93 load_borders_program() 94 95 96def init_glfw_module(glfw_module: str, debug_keyboard: bool = False, debug_rendering: bool = False) -> None: 97 if not glfw_init(glfw_path(glfw_module), debug_keyboard, debug_rendering): 98 raise SystemExit('GLFW initialization failed') 99 100 101def init_glfw(opts: Options, debug_keyboard: bool = False, debug_rendering: bool = False) -> str: 102 glfw_module = 'cocoa' if is_macos else ('wayland' if is_wayland(opts) else 'x11') 103 init_glfw_module(glfw_module, debug_keyboard, debug_rendering) 104 return glfw_module 105 106 107def get_macos_shortcut_for(opts: Options, function: str = 'new_os_window', args: Tuple = (), lookup_name: str = '') -> Optional[SingleKey]: 108 ans = None 109 candidates = [] 110 for k, v in opts.keymap.items(): 111 if v.func == function and v.args == args: 112 candidates.append(k) 113 if candidates: 114 from .fast_data_types import cocoa_set_global_shortcut 115 alt_mods = GLFW_MOD_ALT, GLFW_MOD_ALT | GLFW_MOD_SHIFT 116 # Reverse list so that later defined keyboard shortcuts take priority over earlier defined ones 117 for candidate in reversed(candidates): 118 if candidate.mods in alt_mods: 119 # Option based shortcuts dont work in the global menubar, 120 # presumably because Apple reserves them for IME, see 121 # https://github.com/kovidgoyal/kitty/issues/3515 122 continue 123 if cocoa_set_global_shortcut(lookup_name or function, candidate[0], candidate[2]): 124 ans = candidate 125 break 126 return ans 127 128 129def set_x11_window_icon() -> None: 130 # max icon size on X11 64bits is 128x128 131 path, ext = os.path.splitext(logo_png_file) 132 set_default_window_icon(path + '-128' + ext) 133 134 135def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None: 136 global_shortcuts: Dict[str, SingleKey] = {} 137 if is_macos: 138 for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab', 139 'next_tab', 'new_tab', 'new_window', 'close_window'): 140 val = get_macos_shortcut_for(opts, ac) 141 if val is not None: 142 global_shortcuts[ac] = val 143 val = get_macos_shortcut_for(opts, 'clear_terminal', args=('reset', True), lookup_name='reset_terminal') 144 if val is not None: 145 global_shortcuts['reset_terminal'] = val 146 val = get_macos_shortcut_for(opts, 'load_config_file', args=(), lookup_name='reload_config') 147 if val is not None: 148 global_shortcuts['reload_config'] = val 149 if is_macos and opts.macos_custom_beam_cursor: 150 set_custom_ibeam_cursor() 151 if not is_wayland() and not is_macos: # no window icons on wayland 152 set_x11_window_icon() 153 load_shader_programs.use_selection_fg = opts.selection_foreground is not None 154 with cached_values_for(run_app.cached_values_name) as cached_values: 155 with startup_notification_handler(extra_callback=run_app.first_window_callback) as pre_show_callback: 156 window_id = create_os_window( 157 run_app.initial_window_size_func(get_os_window_sizing_data(opts), cached_values), 158 pre_show_callback, 159 args.title or appname, args.name or args.cls or appname, 160 args.cls or appname, load_all_shaders, disallow_override_title=bool(args.title)) 161 boss = Boss(opts, args, cached_values, global_shortcuts) 162 boss.start(window_id) 163 if bad_lines: 164 boss.show_bad_config_lines(bad_lines) 165 try: 166 boss.child_monitor.main_loop() 167 finally: 168 boss.destroy() 169 170 171class AppRunner: 172 173 def __init__(self) -> None: 174 self.cached_values_name = 'main' 175 self.first_window_callback = lambda window_handle: None 176 self.initial_window_size_func = initial_window_size_func 177 178 def __call__(self, opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None: 179 set_scale(opts.box_drawing_scale) 180 set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback) 181 try: 182 set_font_family(opts, debug_font_matching=args.debug_font_fallback) 183 _run_app(opts, args, bad_lines) 184 finally: 185 set_options(None) 186 free_font_data() # must free font data before glfw/freetype/fontconfig/opengl etc are finalized 187 if is_macos: 188 from kitty.fast_data_types import cocoa_set_notification_activated_callback 189 cocoa_set_notification_activated_callback(None) 190 191 192run_app = AppRunner() 193 194 195def ensure_macos_locale() -> None: 196 # Ensure the LANG env var is set. See 197 # https://github.com/kovidgoyal/kitty/issues/90 198 from .fast_data_types import cocoa_get_lang, locale_is_valid 199 if 'LANG' not in os.environ: 200 lang = cocoa_get_lang() 201 if lang is not None: 202 if not locale_is_valid(lang): 203 if lang.startswith('en_'): 204 lang = 'en_US' 205 else: 206 log_error(f'Could not set LANG Cocoa returns language as: {lang}') 207 os.environ['LANG'] = lang + '.UTF-8' 208 209 210@contextmanager 211def setup_profiling(args: CLIOptions) -> Generator[None, None, None]: 212 try: 213 from .fast_data_types import start_profiler, stop_profiler 214 do_profile = True 215 except ImportError: 216 do_profile = False 217 if do_profile: 218 start_profiler('/tmp/kitty-profile.log') 219 yield 220 if do_profile: 221 import subprocess 222 stop_profiler() 223 exe = kitty_exe() 224 cg = '/tmp/kitty-profile.callgrind' 225 print('Post processing profile data for', exe, '...') 226 with open(cg, 'wb') as f: 227 subprocess.call(['pprof', '--callgrind', exe, '/tmp/kitty-profile.log'], stdout=f) 228 try: 229 subprocess.Popen(['kcachegrind', cg]) 230 except FileNotFoundError: 231 subprocess.call(['pprof', '--text', exe, '/tmp/kitty-profile.log']) 232 print('To view the graphical call data, use: kcachegrind', cg) 233 234 235def macos_cmdline(argv_args: List[str]) -> List[str]: 236 try: 237 with open(os.path.join(config_dir, 'macos-launch-services-cmdline')) as f: 238 raw = f.read() 239 except FileNotFoundError: 240 return argv_args 241 import shlex 242 raw = raw.strip() 243 ans = shlex.split(raw) 244 if ans and ans[0] == 'kitty': 245 del ans[0] 246 return ans 247 248 249def expand_listen_on(listen_on: str, from_config_file: bool) -> str: 250 listen_on = expandvars(listen_on) 251 if '{kitty_pid}' not in listen_on and from_config_file: 252 listen_on += '-{kitty_pid}' 253 listen_on = listen_on.replace('{kitty_pid}', str(os.getpid())) 254 if listen_on.startswith('unix:'): 255 path = listen_on[len('unix:'):] 256 if not path.startswith('@'): 257 if path.startswith('~'): 258 listen_on = f'unix:{os.path.expanduser(path)}' 259 elif not os.path.isabs(path): 260 import tempfile 261 listen_on = f'unix:{os.path.join(tempfile.gettempdir(), path)}' 262 return listen_on 263 264 265def setup_environment(opts: Options, cli_opts: CLIOptions) -> None: 266 from_config_file = False 267 if not cli_opts.listen_on and opts.listen_on.startswith('unix:'): 268 cli_opts.listen_on = opts.listen_on 269 from_config_file = True 270 if cli_opts.listen_on and opts.allow_remote_control != 'n': 271 cli_opts.listen_on = expand_listen_on(cli_opts.listen_on, from_config_file) 272 os.environ['KITTY_LISTEN_ON'] = cli_opts.listen_on 273 set_default_env(opts.env.copy()) 274 275 276def set_locale() -> None: 277 if is_macos: 278 ensure_macos_locale() 279 try: 280 locale.setlocale(locale.LC_ALL, '') 281 except Exception: 282 log_error('Failed to set locale with LANG:', os.environ.get('LANG')) 283 os.environ.pop('LANG', None) 284 try: 285 locale.setlocale(locale.LC_ALL, '') 286 except Exception: 287 log_error('Failed to set locale with no LANG') 288 289 290def _main() -> None: 291 running_in_kitty(True) 292 with suppress(AttributeError): # python compiled without threading 293 sys.setswitchinterval(1000.0) # we have only a single python thread 294 295 try: 296 set_locale() 297 except Exception: 298 log_error('Failed to set locale, ignoring') 299 300 # Ensure the correct kitty is in PATH 301 rpath = sys._xoptions.get('bundle_exe_dir') 302 if rpath: 303 modify_path = is_macos or getattr(sys, 'frozen', False) or sys._xoptions.get('kitty_from_source') == '1' 304 if modify_path or not shutil.which('kitty'): 305 existing_paths = list(filter(None, os.environ.get('PATH', '').split(os.pathsep))) 306 existing_paths.insert(0, rpath) 307 os.environ['PATH'] = os.pathsep.join(existing_paths) 308 309 args = sys.argv[1:] 310 if is_macos and os.environ.pop('KITTY_LAUNCHED_BY_LAUNCH_SERVICES', None) == '1': 311 os.chdir(os.path.expanduser('~')) 312 args = macos_cmdline(args) 313 try: 314 cwd_ok = os.path.isdir(os.getcwd()) 315 except Exception: 316 cwd_ok = False 317 if not cwd_ok: 318 os.chdir(os.path.expanduser('~')) 319 cli_opts, rest = parse_args(args=args, result_class=CLIOptions) 320 cli_opts.args = rest 321 if cli_opts.detach: 322 if cli_opts.session == '-': 323 from .session import PreReadSession 324 cli_opts.session = PreReadSession(sys.stdin.read()) 325 detach() 326 if cli_opts.replay_commands: 327 from kitty.client import main as client_main 328 client_main(cli_opts.replay_commands) 329 return 330 if cli_opts.single_instance: 331 is_first = single_instance(cli_opts.instance_group) 332 if not is_first: 333 talk_to_instance(cli_opts) 334 return 335 bad_lines: List[BadLine] = [] 336 opts = create_opts(cli_opts, accumulate_bad_lines=bad_lines) 337 init_glfw(opts, cli_opts.debug_keyboard, cli_opts.debug_rendering) 338 setup_environment(opts, cli_opts) 339 try: 340 with setup_profiling(cli_opts): 341 # Avoid needing to launch threads to reap zombies 342 run_app(opts, cli_opts, bad_lines) 343 finally: 344 glfw_terminate() 345 346 347def main() -> None: 348 try: 349 _main() 350 except Exception: 351 import traceback 352 tb = traceback.format_exc() 353 log_error(tb) 354 raise SystemExit(1) 355