mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-22 00:08:04 +02:00
The first OS window on Wayland is created at scale 1 because the compositor only sends the fractional scale after the surface exists. The initial size in cells is therefore computed with scale-1 cell metrics and the window is mapped a few cells too small. The existing post-map recompute calls glfwSetWindowSize(), but on Wayland that loses to the compositor's authoritative configure, which re-asserts the size it recorded at map time. So a window that receives focus (the normal interactive case) stays too small; a background window that never gets an activated configure happens to keep the resized value, which is why it can look fixed. Correct the size while the window is still unmapped instead. GLFW gains a hook (glfwWaylandSetInitialWindowSizeCallback) that it invokes when the fractional scale is applied during creation -- mutter, kwin and niri send the scale before the first configure -- letting the embedder recompute the cell-based logical size for the real scale before the window is mapped. The compositor then records the correct size and its activation configure agrees, so there is nothing to race. The existing post-map recompute is kept as a fallback for compositors that only send the scale after the first configure (sway, Hyprland). Also add glfwCocoaSetWindowLevel to the glfw.py wrapper generator list: it was present in the committed glfw-wrapper but missing from the generator, so regenerating the wrapper (required to add the hook above) would otherwise drop it and break the macOS build.
420 lines
16 KiB
Python
Executable File
420 lines
16 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from enum import Enum
|
|
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Sequence, Tuple
|
|
|
|
_plat = sys.platform.lower()
|
|
is_linux = 'linux' in _plat
|
|
is_openbsd = 'openbsd' in _plat
|
|
base = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def null_func() -> None:
|
|
return None
|
|
|
|
|
|
class CompileKey(NamedTuple):
|
|
src: str
|
|
dest: str
|
|
|
|
|
|
class Command(NamedTuple):
|
|
desc: str
|
|
cmd: Sequence[str]
|
|
is_newer_func: Callable[[], bool]
|
|
on_success: Callable[[], None] = null_func
|
|
key: Optional[CompileKey] = None
|
|
keyfile: Optional[str] = None
|
|
|
|
|
|
class ISA(Enum):
|
|
X86 = 0x03
|
|
AMD64 = 0x3e
|
|
ARM64 = 0xb7
|
|
Other = 0x0
|
|
|
|
|
|
class BinaryArch(NamedTuple):
|
|
bits: int = 64
|
|
isa: ISA = ISA.AMD64
|
|
|
|
|
|
class CompilerType(Enum):
|
|
gcc = 'gcc'
|
|
clang = 'clang'
|
|
unknown = 'unknown'
|
|
|
|
|
|
class Env:
|
|
|
|
cc: List[str] = []
|
|
cppflags: List[str] = []
|
|
cflags: List[str] = []
|
|
ldflags: List[str] = []
|
|
library_paths: Dict[str, List[str]] = {}
|
|
ldpaths: List[str] = []
|
|
ccver: Tuple[int, int]
|
|
vcs_rev: str = ''
|
|
binary_arch: BinaryArch = BinaryArch()
|
|
native_optimizations: bool = False
|
|
primary_version: int = 0
|
|
secondary_version: int = 0
|
|
xt_version: str = ''
|
|
has_copy_file_range: bool = False
|
|
|
|
# glfw stuff
|
|
all_headers: List[str] = []
|
|
sources: List[str] = []
|
|
wayland_packagedir: str = ''
|
|
wayland_scanner: str = ''
|
|
wayland_scanner_code: str = ''
|
|
wayland_protocols: Tuple[str, ...] = ()
|
|
|
|
def __init__(
|
|
self, cc: List[str] = [], cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [],
|
|
library_paths: Dict[str, List[str]] = {}, ldpaths: Optional[List[str]] = None, ccver: Tuple[int, int] = (0, 0),
|
|
vcs_rev: str = '', binary_arch: BinaryArch = BinaryArch(),
|
|
native_optimizations: bool = False,
|
|
):
|
|
self.cc, self.cppflags, self.cflags, self.ldflags, self.library_paths = cc, cppflags, cflags, ldflags, library_paths
|
|
self.ldpaths = ldpaths or []
|
|
self.ccver = ccver
|
|
self.vcs_rev = vcs_rev
|
|
self.binary_arch = binary_arch
|
|
self.native_optimizations = native_optimizations
|
|
self._cc_version_string = ''
|
|
self._compiler_type: Optional[CompilerType] = None
|
|
|
|
@property
|
|
def cc_version_string(self) -> str:
|
|
if not self._cc_version_string:
|
|
self._cc_version_string = subprocess.check_output(self.cc + ['--version']).decode()
|
|
return self._cc_version_string
|
|
|
|
@property
|
|
def compiler_type(self) -> CompilerType:
|
|
if self._compiler_type is None:
|
|
raw = self.cc_version_string
|
|
if 'Free Software Foundation' in raw:
|
|
self._compiler_type = CompilerType.gcc
|
|
elif 'clang' in raw.lower().split():
|
|
self._compiler_type = CompilerType.clang
|
|
else:
|
|
self._compiler_type = CompilerType.unknown
|
|
return self._compiler_type
|
|
|
|
def copy(self) -> 'Env':
|
|
ans = Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), dict(self.library_paths), list(self.ldpaths), self.ccver)
|
|
ans.all_headers = list(self.all_headers)
|
|
ans._cc_version_string = self._cc_version_string
|
|
ans.sources = list(self.sources)
|
|
ans.wayland_packagedir = self.wayland_packagedir
|
|
ans.wayland_scanner = self.wayland_scanner
|
|
ans.wayland_scanner_code = self.wayland_scanner_code
|
|
ans.wayland_protocols = self.wayland_protocols
|
|
ans.vcs_rev = self.vcs_rev
|
|
ans.binary_arch = self.binary_arch
|
|
ans.native_optimizations = self.native_optimizations
|
|
ans.primary_version = self.primary_version
|
|
ans.secondary_version = self.secondary_version
|
|
ans.xt_version = self.xt_version
|
|
ans.has_copy_file_range = self.has_copy_file_range
|
|
return ans
|
|
|
|
|
|
def wayland_protocol_file_name(base: str, ext: str = 'c') -> str:
|
|
base = os.path.basename(base).rpartition('.')[0]
|
|
return f'wayland-{base}-client-protocol.{ext}'
|
|
|
|
|
|
def init_env(
|
|
env: Env,
|
|
pkg_config: Callable[..., List[str]],
|
|
pkg_version: Callable[[str], Tuple[int, int]],
|
|
at_least_version: Callable[..., None],
|
|
test_compile: Callable[..., Any],
|
|
module: str = 'x11'
|
|
) -> Env:
|
|
ans = env.copy()
|
|
ans.cflags.append('-fPIC')
|
|
ans.cppflags.append(f'-D_GLFW_{module.upper()}')
|
|
ans.cppflags.append('-D_GLFW_BUILD_DLL')
|
|
|
|
with open(os.path.join(base, 'source-info.json')) as f:
|
|
sinfo = json.load(f)
|
|
module_sources = list(sinfo[module]['sources'])
|
|
if module in ('x11', 'wayland'):
|
|
remove = 'null_joystick.c' if is_linux else 'linux_joystick.c'
|
|
module_sources.remove(remove)
|
|
|
|
ans.sources = sinfo['common']['sources'] + module_sources
|
|
ans.all_headers = [x for x in os.listdir(base) if x.endswith('.h')]
|
|
|
|
if module in ('x11', 'wayland'):
|
|
ans.cflags.append('-pthread')
|
|
ans.ldpaths.extend('-pthread -lm'.split())
|
|
if not is_openbsd:
|
|
ans.ldpaths.extend('-lrt -ldl'.split())
|
|
major, minor = pkg_version('xkbcommon')
|
|
if (major, minor) < (0, 5):
|
|
raise SystemExit('libxkbcommon >= 0.5 required')
|
|
if major < 1:
|
|
ans.cflags.append('-DXKB_HAS_NO_UTF32')
|
|
|
|
if module == 'x11':
|
|
for dep in 'x11 xrandr xinerama xcursor xkbcommon xkbcommon-x11 x11-xcb dbus-1'.split():
|
|
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
|
|
ans.ldpaths.extend(pkg_config(dep, '--libs'))
|
|
|
|
elif module == 'cocoa':
|
|
ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
|
|
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo QuartzCore UniformTypeIdentifiers'.split():
|
|
ans.ldpaths.extend(('-framework', f_))
|
|
|
|
elif module == 'wayland':
|
|
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
|
|
ans.wayland_packagedir = os.path.abspath(pkg_config('wayland-protocols', '--variable=pkgdatadir')[0])
|
|
ans.wayland_scanner = os.path.abspath(pkg_config('wayland-scanner', '--variable=wayland_scanner')[0])
|
|
scanner_version = tuple(map(int, pkg_config('wayland-scanner', '--modversion')[0].strip().split('.')))
|
|
ans.wayland_scanner_code = 'private-code' if scanner_version >= (1, 14, 91) else 'code'
|
|
ans.wayland_protocols = tuple(sinfo[module]['protocols'])
|
|
for p in ans.wayland_protocols:
|
|
ans.sources.append(wayland_protocol_file_name(p))
|
|
ans.all_headers.append(wayland_protocol_file_name(p, 'h'))
|
|
for dep in 'wayland-client wayland-cursor xkbcommon dbus-1'.split():
|
|
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
|
|
ans.ldpaths.extend(pkg_config(dep, '--libs'))
|
|
has_memfd_create = test_compile(env.cc, '-Werror', src='''#define _GNU_SOURCE
|
|
#include <unistd.h>
|
|
#include <sys/syscall.h>
|
|
int main(void) {
|
|
return syscall(__NR_memfd_create, "test", 0);
|
|
}''')
|
|
if has_memfd_create:
|
|
ans.cppflags.append('-DHAS_MEMFD_CREATE')
|
|
|
|
return ans
|
|
|
|
|
|
def build_wayland_protocols(
|
|
env: Env,
|
|
parallel_run: Callable[[List[Command]], None],
|
|
emphasis: Callable[[str], str],
|
|
newer: Callable[..., bool],
|
|
dest_dir: str
|
|
) -> None:
|
|
items = []
|
|
for protocol in env.wayland_protocols:
|
|
if '/' in protocol:
|
|
src = os.path.join(env.wayland_packagedir, protocol)
|
|
if not os.path.exists(src):
|
|
raise SystemExit(f'The wayland-protocols package on your system is missing the {protocol} protocol definition file')
|
|
else:
|
|
src = os.path.join(os.path.dirname(os.path.abspath(__file__)), protocol)
|
|
if not os.path.exists(src):
|
|
raise SystemExit(f'The local Wayland protocol {protocol} is missing from kitty sources')
|
|
for ext in 'hc':
|
|
dest = wayland_protocol_file_name(src, ext)
|
|
dest = os.path.join(dest_dir, dest)
|
|
if newer(dest, src):
|
|
q = 'client-header' if ext == 'h' else env.wayland_scanner_code
|
|
items.append(Command(
|
|
f'Generating {emphasis(os.path.basename(dest))} ...',
|
|
[env.wayland_scanner, q, src, dest], lambda: True))
|
|
if items:
|
|
parallel_run(items)
|
|
|
|
|
|
class Arg:
|
|
|
|
def __init__(self, decl: str):
|
|
self.type, self.name = decl.rsplit(' ', 1)
|
|
self.type = self.type.strip()
|
|
self.name = self.name.strip()
|
|
while self.name.startswith('*'):
|
|
self.name = self.name[1:]
|
|
self.type = self.type + '*'
|
|
if '[' in self.name:
|
|
self.type += '[' + self.name.partition('[')[-1]
|
|
|
|
def __repr__(self) -> str:
|
|
return f'Arg({self.type}, {self.name})'
|
|
|
|
|
|
class Function:
|
|
|
|
def __init__(self, declaration: str, check_fail: bool = True):
|
|
self.check_fail = check_fail
|
|
m = re.match(
|
|
r'(.+?)\s+(glfw[A-Z][a-zA-Z0-9]+)[(](.+)[)]$', declaration
|
|
)
|
|
if m is None:
|
|
raise SystemExit('Failed to parse ' + repr(declaration))
|
|
self.restype = m.group(1).strip()
|
|
self.name = m.group(2)
|
|
args = m.group(3).strip().split(',')
|
|
args = [x.strip() for x in args]
|
|
self.args = []
|
|
for a in args:
|
|
if a == 'void':
|
|
continue
|
|
self.args.append(Arg(a))
|
|
if not self.args:
|
|
self.args = [Arg('void v')]
|
|
|
|
def declaration(self) -> str:
|
|
return 'typedef {restype} (*{name}_func)({args});\nGFW_EXTERN {name}_func {name}_impl;\n#define {name} {name}_impl'.format(
|
|
restype=self.restype,
|
|
name=self.name,
|
|
args=', '.join(a.type for a in self.args)
|
|
)
|
|
|
|
def load(self) -> str:
|
|
ans = f'*(void **) (&{self.name}_impl) = dlsym(handle, "{self.name}");'
|
|
ans += f'\n if ({self.name}_impl == NULL) '
|
|
if self.check_fail:
|
|
ans += f'fail("Failed to load glfw function {self.name} with error: %s", dlerror());'
|
|
else:
|
|
ans += 'dlerror(); // clear error indicator'
|
|
return ans
|
|
|
|
|
|
def generate_wrappers(glfw_header: str) -> None:
|
|
with open(glfw_header) as f:
|
|
src = f.read()
|
|
functions = []
|
|
first = None
|
|
for m in re.finditer(r'^GLFWAPI\s+(.+[)]);\s*$', src, flags=re.MULTILINE):
|
|
if first is None:
|
|
first = m.start()
|
|
decl = m.group(1)
|
|
if 'VkInstance' in decl:
|
|
continue
|
|
functions.append(Function(decl))
|
|
for line in '''\
|
|
void* glfwGetCocoaWindow(GLFWwindow* window)
|
|
void* glfwGetNSGLContext(GLFWwindow *window)
|
|
uint32_t glfwGetCocoaMonitor(GLFWmonitor* monitor)
|
|
GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow* window, GLFWcocoatextinputfilterfun callback)
|
|
GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback)
|
|
GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *window, GLFWcocoatogglefullscreenfun callback)
|
|
GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback)
|
|
GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback)
|
|
uint32_t glfwGetCocoaKeyEquivalent(uint32_t glfw_key, int glfw_mods, int* cocoa_mods)
|
|
void glfwCocoaRequestRenderFrame(GLFWwindow *w, GLFWcocoarenderframefun callback)
|
|
bool glfwCocoaRecreateGLDrawable(GLFWwindow *w)
|
|
GLFWcocoarenderframefun glfwCocoaSetWindowResizeCallback(GLFWwindow *w, GLFWcocoarenderframefun callback)
|
|
void* glfwGetX11Display(void)
|
|
unsigned long glfwGetX11Window(GLFWwindow* window)
|
|
void glfwSetPrimarySelectionString(GLFWwindow* window, const char* string)
|
|
void glfwCocoaCycleThroughOSWindows(bool backwards)
|
|
void glfwCocoaSetWindowChrome(GLFWwindow* window, unsigned int color, bool use_system_color, unsigned int system_color,\
|
|
int background_blur, unsigned int hide_window_decorations, bool show_text_in_titlebar, int color_space, float background_opacity, bool resizable)
|
|
void glfwCocoaRegisterMIMETypes(GLFWwindow *window, const char **mimes, size_t count)
|
|
void glfwCocoaSetWindowLevel(GLFWwindow *window, const char *level_spec)
|
|
const char* glfwGetPrimarySelectionString(GLFWwindow* window, void)
|
|
int glfwGetNativeKeyForName(const char* key_name, int case_sensitive)
|
|
void glfwRequestWaylandFrameEvent(GLFWwindow *handle, unsigned long long id, GLFWwaylandframecallbackfunc callback)
|
|
void glfwWaylandActivateWindow(GLFWwindow *handle, const char *activation_token)
|
|
const char* glfwWaylandMissingCapabilities(void)
|
|
void glfwWaylandRunWithActivationToken(GLFWwindow *handle, GLFWactivationcallback cb, void *cb_data)
|
|
bool glfwWaylandSetTitlebarColor(GLFWwindow *handle, uint32_t color, bool use_system_color)
|
|
void glfwWaylandSetTitlebarHidden(GLFWwindow *handle, bool hidden)
|
|
void glfwWaylandSetInitialWindowSizeCallback(GLFWwaylandinitialsizefun callback)
|
|
void glfwWaylandRedrawCSDWindowTitle(GLFWwindow *handle)
|
|
bool glfwWaylandIsWindowFullyCreated(GLFWwindow *handle)
|
|
bool glfwWaylandBeep(GLFWwindow *handle)
|
|
pid_t glfwWaylandCompositorPID(void)
|
|
void glfwConfigureMomentumScroller(double friction, double min_velocity, double max_velocity, unsigned timer_interval)
|
|
unsigned long long glfwDBusUserNotify(const GLFWDBUSNotificationData *n, GLFWDBusnotificationcreatedfun callback, void *data)
|
|
void glfwDBusSetUserNotificationHandler(GLFWDBusnotificationactivatedfun handler)
|
|
int glfwSetX11LaunchCommand(GLFWwindow *handle, char **argv, int argc)
|
|
'''.splitlines():
|
|
if line:
|
|
functions.append(Function(line.strip(), check_fail=False))
|
|
|
|
declarations = [f.declaration() for f in functions]
|
|
p = src.find(' * GLFW API tokens')
|
|
p = src.find('*/', p)
|
|
preamble = src[p + 2:first]
|
|
header = '''\
|
|
//
|
|
// THIS FILE IS GENERATED BY glfw.py
|
|
//
|
|
// SAVE YOURSELF SOME TIME, DO NOT MANUALLY EDIT
|
|
//
|
|
|
|
#pragma once
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include "monotonic.h"
|
|
|
|
#ifndef GFW_EXTERN
|
|
#define GFW_EXTERN extern
|
|
#endif
|
|
{}
|
|
|
|
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
|
|
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
|
|
typedef bool (* GLFWhandleurlopen)(const char*);
|
|
typedef void (* GLFWapplicationwillfinishlaunchingfun)(bool);
|
|
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
|
|
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
|
|
typedef void (*GLFWwaylandframecallbackfunc)(unsigned long long id);
|
|
typedef void (*GLFWDBusnotificationcreatedfun)(unsigned long long, uint32_t, void*);
|
|
typedef void (*GLFWDBusnotificationactivatedfun)(uint32_t, int, const char*);
|
|
{}
|
|
|
|
const char* load_glfw(const char* path);
|
|
'''.format(preamble, '\n\n'.join(declarations))
|
|
with open('../kitty/glfw-wrapper.h', 'w') as f:
|
|
f.write(header)
|
|
|
|
code = '''
|
|
// generated by glfw.py DO NOT edit
|
|
|
|
#define GFW_EXTERN
|
|
#include "data-types.h"
|
|
#include "glfw-wrapper.h"
|
|
#include <dlfcn.h>
|
|
|
|
static void* handle = NULL;
|
|
|
|
#define fail(msg, ...) { snprintf(buf, sizeof(buf), msg, __VA_ARGS__); return buf; }
|
|
|
|
const char*
|
|
load_glfw(const char* path) {
|
|
static char buf[2048];
|
|
handle = dlopen(path, RTLD_LAZY);
|
|
if (handle == NULL) fail("Failed to dlopen %s with error: %s", path, dlerror());
|
|
dlerror();
|
|
|
|
LOAD
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
unload_glfw(void) {
|
|
if (handle) { dlclose(handle); handle = NULL; }
|
|
}
|
|
'''.replace('LOAD', '\n\n '.join(f.load() for f in functions))
|
|
with open('../kitty/glfw-wrapper.c', 'w') as f:
|
|
f.write(code)
|
|
|
|
|
|
def main() -> None:
|
|
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
|
generate_wrappers('glfw3.h')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|