summaryrefslogtreecommitdiff
path: root/kitty/main.py
blob: be691b5f480e7a0fc1f072baf6315c1452c6c106 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>

import locale
import os
import sys
from contextlib import contextmanager

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)
    try:
        single_instance.socket.shutdown(socket.SHUT_RDWR)
    except EnvironmentError:
        pass
    single_instance.socket.close()

    if args.wait_for_single_instance_window_close:
        conn = notify_socket.accept()[0]
        conn.recv(1)
        try:
            conn.shutdown(socket.SHUT_RDWR)
        except EnvironmentError:
            pass
        conn.close()


def load_all_shaders(semi_transparent=0):
    load_shader_programs(semi_transparent, load_all_shaders.cursor_text_color)
    load_borders_program()


load_all_shaders.cursor_text_color = None


def init_glfw(debug_keyboard=False):
    glfw_module = 'cocoa' if is_macos else ('wayland' if is_wayland 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):
    new_os_window_trigger = get_new_os_window_trigger(opts)
    if is_macos and opts.macos_custom_beam_cursor:
        set_custom_ibeam_cursor()
    load_all_shaders.cursor_text_color = opts.cursor_text_color
    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)
    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()
        try:
            boss.child_monitor.main_loop()
        finally:
            boss.destroy()


def run_app(opts, args):
    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)
    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_osx_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 = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'kitty-profile')
        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():
    try:
        sys.setswitchinterval(1000.0)  # we have only a single python thread
    except AttributeError:
        pass  # python compiled without threading
    if is_macos:
        ensure_osx_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 kitty is in PATH
    rpath = os.path.dirname(kitty_exe())
    items = frozenset(os.environ['PATH'].split(os.pathsep))
    if rpath and rpath not in items:
        os.environ['PATH'] += os.pathsep + rpath

    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:
        init_glfw(args.debug_keyboard)  # needed for parsing native keysyms
        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
    init_glfw(args.debug_keyboard)  # needed for parsing native keysyms
    opts = create_opts(args)
    setup_environment(opts, args)
    try:
        with setup_profiling(args):
            # Avoid needing to launch threads to reap zombies
            run_app(opts, args)
    finally:
        glfw_terminate()


def main():
    try:
        _main()
    except Exception:
        import traceback
        tb = traceback.format_exc()
        log_error(tb)
        raise SystemExit(1)