mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-14 12:37:48 +02:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d15380f9d | ||
|
|
3119088a54 | ||
|
|
fcc77c264f | ||
|
|
d38d72bd5e | ||
|
|
c36815a380 | ||
|
|
6be57e4316 | ||
|
|
f591ca151d | ||
|
|
9c1acce02a | ||
|
|
858e05186c | ||
|
|
f104562533 | ||
|
|
a3398a44f8 | ||
|
|
92fb47ae3c | ||
|
|
01c182c410 | ||
|
|
6fded182b3 | ||
|
|
a8082f7a3c | ||
|
|
75fdd86637 | ||
|
|
e42d7efe85 | ||
|
|
13b574486c | ||
|
|
c94844b220 | ||
|
|
31d7dc43b0 | ||
|
|
f46fc096c8 | ||
|
|
e31eda7735 | ||
|
|
5b05b1235e | ||
|
|
da6f85dbaa | ||
|
|
9d9ad91f37 | ||
|
|
3b20936959 | ||
|
|
33f278b477 | ||
|
|
255dd2845e | ||
|
|
d8b0edce43 | ||
|
|
95c6279bdd | ||
|
|
cc4d4eeaca | ||
|
|
abc9b1fc48 | ||
|
|
95f5e9293e | ||
|
|
82523b14df | ||
|
|
6f689f3221 | ||
|
|
e687d6db05 | ||
|
|
13c37cf694 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -28,3 +28,4 @@ terminfo/x/* linguist-generated=true
|
||||
*.py text diff=python
|
||||
*.m text diff=objc
|
||||
*.go text diff=go
|
||||
*.mod filter=goreplace
|
||||
|
||||
@@ -106,6 +106,13 @@ consumption to do the same tasks.
|
||||
Detailed list of changes
|
||||
-------------------------------------
|
||||
|
||||
0.42.2 [future]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Fix :opt:`remember_window_position` not working because of a stupid typo (:iss:`8646`)
|
||||
|
||||
- A new :option:`kitty --grab-keyboard` that can be used to grab the keyboard so that global shortcuts are sent to kitty instead
|
||||
|
||||
0.42.1 [2025-05-17]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ Key Value
|
||||
``c`` ``c=1`` if the terminal supports close events, otherwise the ``c``
|
||||
must be omitted.
|
||||
|
||||
``o`` Comma separated list of occassions from the ``o`` key that the
|
||||
``o`` Comma separated list of occasions from the ``o`` key that the
|
||||
terminal implements. If no occasions are supported, the value
|
||||
``o=always`` must be sent in the query response.
|
||||
|
||||
@@ -405,7 +405,7 @@ Key Value
|
||||
|
||||
``s`` Comma separated list of sound names from the table of standard sound names above.
|
||||
Terminals will report the list of standard sound names they support.
|
||||
Terminals *should* support atleast ``system`` and ``silent``.
|
||||
Terminals *should* support at least ``system`` and ``silent``.
|
||||
|
||||
``u`` Comma separated list of urgency values that the terminal implements.
|
||||
If urgency is not supported, the ``u`` key must be absent from the
|
||||
@@ -450,10 +450,10 @@ Key Value Default Description
|
||||
encoded UTF-8
|
||||
application name
|
||||
|
||||
``g`` :ref:`identifier` ``unset`` Identifier for icon data. Make these globally unqiue,
|
||||
``g`` :ref:`identifier` ``unset`` Identifier for icon data. Make these globally unique,
|
||||
like an UUID.
|
||||
|
||||
``i`` :ref:`identifier` ``unset`` Identifier for the notification. Make these globally unqiue,
|
||||
``i`` :ref:`identifier` ``unset`` Identifier for the notification. Make these globally unique,
|
||||
like an UUID, so that terminal multiplexers can
|
||||
direct responses to the correct window. Note that for backwards
|
||||
compatibility reasons i=0 is special and should not be used.
|
||||
|
||||
@@ -354,7 +354,7 @@ are the :kbd:`Enter`, :kbd:`Tab` and :kbd:`Backspace` keys which still generate
|
||||
bytes as in legacy mode this is to allow the user to type and execute commands
|
||||
in the shell such as ``reset`` after a program that sets this mode crashes
|
||||
without clearing it. Note that the Lock modifiers are not reported for text
|
||||
producing keys, to keep them useable in legacy programs. To get lock modifiers
|
||||
producing keys, to keep them usable in legacy programs. To get lock modifiers
|
||||
for all keys use the :ref:`report_all_keys` enhancement.
|
||||
|
||||
.. _report_events:
|
||||
|
||||
@@ -118,7 +118,7 @@ that is, they are used to set the variable value for some font characteristic.
|
||||
<https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string>`__
|
||||
|
||||
``system``
|
||||
This can be used to pass an arbitrary string, usuall a family or full name
|
||||
This can be used to pass an arbitrary string, usually a family or full name
|
||||
to the OS font selection APIs. Should not be used in conjunction with any
|
||||
other keys. Is the same as specifying just the font name without any keys.
|
||||
|
||||
|
||||
@@ -114,11 +114,12 @@ shell.
|
||||
|
||||
The Linux dock panel was::
|
||||
|
||||
kitten panel kitty +launch my-panel.py
|
||||
wm bar
|
||||
|
||||
This creates the panel window and runs the ``my-panel.py`` script inside it
|
||||
using the Python interpreter that comes bundled with kitty. Unfortunately the
|
||||
actual script is not public, but there are :ref:`public projects implementing
|
||||
This is a custom program I wrote for my personal use. It uses kitty's kitten
|
||||
infrastructure to implement the bar in a `few hundred lines of code
|
||||
<https://github.com/kovidgoyal/wm/blob/master/bar/main.go>`__.
|
||||
This was designed for my personal use only, but, there are :ref:`public projects implementing
|
||||
general purpose panels using kitty <panel_projects>`.
|
||||
|
||||
|
||||
@@ -161,8 +162,8 @@ Compatibility with various platforms
|
||||
🟠 **niri**
|
||||
Breaks when hiding (unmapping) layer shell windows. This means the quick
|
||||
access terminal is non-functional, but background and dock panels work.
|
||||
More technically, keyboard focus gets stuck in the hidden window and when trying
|
||||
to remap the hidden window niri never sends configure events for the remapped surface.
|
||||
More technically, when trying to remap the hidden window niri never sends
|
||||
configure events for the remapped surface.
|
||||
|
||||
🟠 **labwc**
|
||||
Breaks when hiding (unmapping) layer shell windows. This means the quick
|
||||
|
||||
@@ -310,7 +310,7 @@ below::
|
||||
|
||||
# Beep on unknown keys
|
||||
map --new-mode XXX --on-unknown beep ...
|
||||
# Ingore unknown keys silently
|
||||
# Ignore unknown keys silently
|
||||
map --new-mode XXX --on-unknown ignore ...
|
||||
# Beep and exit the keyboard mode on unknown key
|
||||
map --new-mode XXX --on-unknown end ...
|
||||
|
||||
@@ -101,7 +101,7 @@ ASCII only text.
|
||||
|
||||
foot, iterm2 and Terminal.app are left out as they do not run under X11.
|
||||
Alacritty+tmux is included just to show the effect of putting a terminal
|
||||
multiplexer into the mix (halving throughput) and because alacritty isnt
|
||||
multiplexer into the mix (halving throughput) and because alacritty isn't
|
||||
remotely comparable to any of the other terminals feature wise without tmux.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -255,17 +255,17 @@ Detecting if the terminal supports this protocol
|
||||
-----------------------------------------------------
|
||||
|
||||
To detect support for this protocol use the `CPR (Cursor Position Report)
|
||||
<https://vt100.net/docs/vt510-rm/CPR.html>`__ escape code. Send a ``CPR``
|
||||
followed by ``\e]_text_size_code;w=2; \a`` which will draw a space character in
|
||||
two cells, followed by another ``CPR``. Then send ``\e]_text_size_code;s=2; \a``
|
||||
which will draw a space in a ``2 by 2`` block of cells, followed by another
|
||||
``CPR``.
|
||||
<https://vt100.net/docs/vt510-rm/CPR.html>`__ escape code. Send a ``CR``
|
||||
(carriage return) followed by ``CPR`` followed by ``\e]_text_size_code;w=2; \a``
|
||||
which will draw a space character in two cells, followed by another ``CPR``.
|
||||
Then send ``\e]_text_size_code;s=2; \a`` which will draw a space in a ``2 by 2``
|
||||
block of cells, followed by another ``CPR``.
|
||||
|
||||
Then wait for the three responses from the terminal to the three CPR queries.
|
||||
If the cursor position in the three responses is the same, the terminal does
|
||||
not support this protocol at all, if the second response has a different cursor
|
||||
position then the width part is supported and if the third response has yet
|
||||
another position, the scale part is supported.
|
||||
not support this protocol at all, if the second response has the cursor
|
||||
moved by two cells, then the width part is supported and if the third response has the
|
||||
cursor moved by another two cells, then the scale part is supported.
|
||||
|
||||
|
||||
Interaction with other terminal controls
|
||||
|
||||
@@ -828,7 +828,7 @@ int _glfwPlatformInit(bool *supports_window_occlusion)
|
||||
{
|
||||
debug_key("---------------- key down -------------------\n");
|
||||
debug_key("%s\n", [[event description] UTF8String]);
|
||||
if (!_glfw.ignoreOSKeyboardProcessing) {
|
||||
if (!_glfw.ignoreOSKeyboardProcessing && !_glfw.keyboard_grabbed) {
|
||||
// first check if there is a global menu bar shortcut
|
||||
if ([[NSApp mainMenu] performKeyEquivalent:event]) {
|
||||
debug_key("keyDown triggered global menu bar action ignoring\n");
|
||||
@@ -1144,3 +1144,4 @@ void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval,
|
||||
}
|
||||
|
||||
void _glfwPlatformInputColorScheme(GLFWColorScheme appearance UNUSED) { }
|
||||
bool _glfwPlatformGrabKeyboard(bool grab UNUSED) { return true; /* directly uses _glfw.keyboard_grabbed */ }
|
||||
|
||||
1
glfw/glfw3.h
vendored
1
glfw/glfw3.h
vendored
@@ -4247,6 +4247,7 @@ GLFWAPI void glfwPostEmptyEvent(void);
|
||||
|
||||
GLFWAPI bool glfwGetIgnoreOSKeyboardProcessing(void);
|
||||
GLFWAPI void glfwSetIgnoreOSKeyboardProcessing(bool enabled);
|
||||
GLFWAPI bool glfwGrabKeyboard(int grab);
|
||||
|
||||
/*! @brief Returns the value of an input option for the specified window.
|
||||
*
|
||||
|
||||
7
glfw/input.c
vendored
7
glfw/input.c
vendored
@@ -684,6 +684,13 @@ GLFWAPI void glfwSetIgnoreOSKeyboardProcessing(bool enabled) {
|
||||
_glfw.ignoreOSKeyboardProcessing = enabled;
|
||||
}
|
||||
|
||||
GLFWAPI bool glfwGrabKeyboard(int grab) {
|
||||
if (grab == 0 || grab == 1) {
|
||||
if (_glfwPlatformGrabKeyboard(grab)) _glfw.keyboard_grabbed = grab;
|
||||
}
|
||||
return _glfw.keyboard_grabbed;
|
||||
}
|
||||
|
||||
GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode)
|
||||
{
|
||||
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||
|
||||
3
glfw/internal.h
vendored
3
glfw/internal.h
vendored
@@ -616,7 +616,7 @@ struct _GLFWlibrary
|
||||
_GLFWtls contextSlot;
|
||||
_GLFWmutex errorLock;
|
||||
|
||||
bool ignoreOSKeyboardProcessing;
|
||||
bool ignoreOSKeyboardProcessing, keyboard_grabbed;
|
||||
|
||||
struct {
|
||||
bool available;
|
||||
@@ -884,6 +884,7 @@ void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval,
|
||||
void _glfwPlatformRemoveTimer(unsigned long long timer_id);
|
||||
int _glfwPlatformSetWindowBlur(_GLFWwindow* handle, int value);
|
||||
MonitorGeometry _glfwPlatformGetMonitorGeometry(_GLFWmonitor* monitor);
|
||||
bool _glfwPlatformGrabKeyboard(bool grab);
|
||||
|
||||
char* _glfw_strdup(const char* source);
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"staging/fractional-scale/fractional-scale-v1.xml",
|
||||
"staging/single-pixel-buffer/single-pixel-buffer-v1.xml",
|
||||
"unstable/idle-inhibit/idle-inhibit-unstable-v1.xml",
|
||||
"unstable/keyboard-shortcuts-inhibit/keyboard-shortcuts-inhibit-unstable-v1.xml",
|
||||
"staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml",
|
||||
"staging/xdg-system-bell/xdg-system-bell-v1.xml",
|
||||
"staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml",
|
||||
|
||||
7
glfw/wl_init.c
vendored
7
glfw/wl_init.c
vendored
@@ -600,6 +600,9 @@ static void registryHandleGlobal(void* data UNUSED,
|
||||
else if (is(zwp_idle_inhibit_manager_v1)) {
|
||||
_glfw.wl.idle_inhibit_manager = wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1);
|
||||
}
|
||||
else if (is(zwp_keyboard_shortcuts_inhibit_manager_v1)) {
|
||||
_glfw.wl.keyboard_shortcuts_inhibit_manager = wl_registry_bind(registry, name, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface, 1);
|
||||
}
|
||||
else if (is(xdg_toplevel_icon_manager_v1)) {
|
||||
_glfw.wl.xdg_toplevel_icon_manager_v1 = wl_registry_bind(registry, name, &xdg_toplevel_icon_manager_v1_interface, 1);
|
||||
}
|
||||
@@ -718,7 +721,7 @@ get_compositor_missing_capabilities(void) {
|
||||
C(cursor_shape, wp_cursor_shape_manager_v1); C(layer_shell, zwlr_layer_shell_v1);
|
||||
C(single_pixel_buffer, wp_single_pixel_buffer_manager_v1); C(preferred_scale, has_preferred_buffer_scale);
|
||||
C(idle_inhibit, idle_inhibit_manager); C(icon, xdg_toplevel_icon_manager_v1); C(bell, xdg_system_bell_v1);
|
||||
C(window-tag, xdg_toplevel_tag_manager_v1);
|
||||
C(window-tag, xdg_toplevel_tag_manager_v1); C(keyboard_shortcuts_inhibit, keyboard_shortcuts_inhibit_manager);
|
||||
if (_glfw.wl.xdg_wm_base_version < 6) p += snprintf(p, sizeof(buf) - (p - buf), "%s ", "window-state-suspended");
|
||||
if (_glfw.wl.xdg_wm_base_version < 5) p += snprintf(p, sizeof(buf) - (p - buf), "%s ", "window-capabilities");
|
||||
#undef C
|
||||
@@ -913,6 +916,8 @@ void _glfwPlatformTerminate(void)
|
||||
zwlr_layer_shell_v1_destroy(_glfw.wl.zwlr_layer_shell_v1);
|
||||
if (_glfw.wl.idle_inhibit_manager)
|
||||
zwp_idle_inhibit_manager_v1_destroy(_glfw.wl.idle_inhibit_manager);
|
||||
if (_glfw.wl.keyboard_shortcuts_inhibit_manager)
|
||||
zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(_glfw.wl.keyboard_shortcuts_inhibit_manager);
|
||||
|
||||
if (_glfw.wl.registry)
|
||||
wl_registry_destroy(_glfw.wl.registry);
|
||||
|
||||
3
glfw/wl_platform.h
vendored
3
glfw/wl_platform.h
vendored
@@ -66,6 +66,7 @@ typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR
|
||||
#include "wayland-wlr-layer-shell-unstable-v1-client-protocol.h"
|
||||
#include "wayland-single-pixel-buffer-v1-client-protocol.h"
|
||||
#include "wayland-idle-inhibit-unstable-v1-client-protocol.h"
|
||||
#include "wayland-keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
|
||||
#include "wayland-xdg-toplevel-icon-v1-client-protocol.h"
|
||||
#include "wayland-xdg-system-bell-v1-client-protocol.h"
|
||||
#include "wayland-xdg-toplevel-tag-v1-client-protocol.h"
|
||||
@@ -290,6 +291,7 @@ typedef struct _GLFWwindowWayland
|
||||
WaylandWindowState toplevel_states;
|
||||
uint32_t decoration_mode;
|
||||
} current, pending;
|
||||
struct zwp_keyboard_shortcuts_inhibitor_v1 *keyboard_shortcuts_inhibitor;
|
||||
} _GLFWwindowWayland;
|
||||
|
||||
typedef enum _GLFWWaylandOfferType
|
||||
@@ -350,6 +352,7 @@ typedef struct _GLFWlibraryWayland
|
||||
struct zwlr_layer_shell_v1* zwlr_layer_shell_v1; uint32_t zwlr_layer_shell_v1_version;
|
||||
struct wp_single_pixel_buffer_manager_v1 *wp_single_pixel_buffer_manager_v1;
|
||||
struct zwp_idle_inhibit_manager_v1* idle_inhibit_manager;
|
||||
struct zwp_keyboard_shortcuts_inhibit_manager_v1 *keyboard_shortcuts_inhibit_manager;
|
||||
|
||||
int compositorVersion;
|
||||
int seatVersion;
|
||||
|
||||
25
glfw/wl_window.c
vendored
25
glfw/wl_window.c
vendored
@@ -46,6 +46,18 @@
|
||||
static bool
|
||||
is_layer_shell(_GLFWwindow *window) { return window->wl.layer_shell.config.type != GLFW_LAYER_SHELL_NONE; }
|
||||
|
||||
static void
|
||||
inhibit_shortcuts_for(_GLFWwindow *window, bool inhibit) {
|
||||
if (inhibit) {
|
||||
if (window->wl.keyboard_shortcuts_inhibitor) return;
|
||||
window->wl.keyboard_shortcuts_inhibitor = zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(_glfw.wl.keyboard_shortcuts_inhibit_manager, window->wl.surface, _glfw.wl.seat);
|
||||
} else {
|
||||
if (!window->wl.keyboard_shortcuts_inhibitor) return;
|
||||
zwp_keyboard_shortcuts_inhibitor_v1_destroy(window->wl.keyboard_shortcuts_inhibitor);
|
||||
window->wl.keyboard_shortcuts_inhibitor = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, const char *token) {
|
||||
for (size_t i = 0; i < _glfw.wl.activation_requests.sz; i++) {
|
||||
@@ -616,6 +628,7 @@ static bool createSurface(_GLFWwindow* window,
|
||||
update_regions(window);
|
||||
|
||||
wl_surface_set_buffer_scale(window->wl.surface, scale);
|
||||
if (_glfw.keyboard_grabbed) inhibit_shortcuts_for(window, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1469,6 +1482,8 @@ void _glfwPlatformDestroyWindow(_GLFWwindow* window)
|
||||
if (window->id == _glfw.wl.keyRepeatInfo.keyboardFocusId) {
|
||||
_glfw.wl.keyRepeatInfo.keyboardFocusId = 0;
|
||||
}
|
||||
if (window->wl.keyboard_shortcuts_inhibitor)
|
||||
zwp_keyboard_shortcuts_inhibitor_v1_destroy(window->wl.keyboard_shortcuts_inhibitor);
|
||||
|
||||
if (window->wl.temp_buffer_used_during_window_creation)
|
||||
wl_buffer_destroy(window->wl.temp_buffer_used_during_window_creation);
|
||||
@@ -2820,6 +2835,16 @@ _glfwPlatformSetWindowBlur(_GLFWwindow *window, int blur_radius) {
|
||||
return has_blur ? 1 : 0;
|
||||
}
|
||||
|
||||
bool
|
||||
_glfwPlatformGrabKeyboard(bool grab) {
|
||||
if (!_glfw.wl.keyboard_shortcuts_inhibit_manager) {
|
||||
_glfwInputError(GLFW_PLATFORM_ERROR, "The Wayland compositor does not implement inhibit-keyboard-shortcuts, cannot grab keyboard");
|
||||
return false;
|
||||
}
|
||||
for (_GLFWwindow* window = _glfw.windowListHead; window; window = window->next) inhibit_shortcuts_for(window, grab);
|
||||
return true;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW native API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
11
glfw/x11_window.c
vendored
11
glfw/x11_window.c
vendored
@@ -3384,6 +3384,17 @@ _glfwPlatformSetWindowBlur(_GLFWwindow *window, int blur_radius) {
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
_glfwPlatformGrabKeyboard(bool grab) {
|
||||
int result;
|
||||
if (grab) {
|
||||
result = XGrabKeyboard(_glfw.x11.display, _glfw.x11.root, True, GrabModeAsync, GrabModeAsync, CurrentTime);
|
||||
} else {
|
||||
result = XUngrabKeyboard(_glfw.x11.display, CurrentTime);
|
||||
}
|
||||
return result == GrabSuccess;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
////// GLFW native API //////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
5
go.mod
5
go.mod
@@ -6,12 +6,13 @@ toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/ALTree/bigfloat v0.2.0
|
||||
github.com/alecthomas/chroma/v2 v2.17.2
|
||||
github.com/alecthomas/chroma/v2 v2.18.0
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/edwvee/exiffix v0.0.0-20240229113213-0dbb146775be
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
|
||||
github.com/kovidgoyal/imaging v1.6.4
|
||||
github.com/seancfoley/ipaddress-go v1.7.1
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
@@ -22,6 +23,8 @@ require (
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
|
||||
//replace github.com/kovidgoyal/dbus => ../dbus
|
||||
|
||||
require (
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
|
||||
10
go.sum
10
go.sum
@@ -2,8 +2,8 @@ github.com/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM
|
||||
github.com/ALTree/bigfloat v0.2.0/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
|
||||
github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
||||
@@ -28,6 +28,12 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250518162316-472d38549895 h1:CQ+iKunklzE89I7C/AtXXltUhokfDNwL4rlst4AFQ9Q=
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250518162316-472d38549895/go.mod h1:RbNG3Q1g6GUy1/WzWVx+S24m7VKyvl57vV+cr2hpt50=
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250518163929-5e5bfb2bd2f2 h1:txptp4rHsZ8LPdmJSbak3fU08C+SmVqKy+FRBbQhl84=
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250518163929-5e5bfb2bd2f2/go.mod h1:RbNG3Q1g6GUy1/WzWVx+S24m7VKyvl57vV+cr2hpt50=
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 h1:rMY/hWfcVzBm6BLX6YLA+gLJEpuXBed/VP6YEkXt8R4=
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1/go.mod h1:RbNG3Q1g6GUy1/WzWVx+S24m7VKyvl57vV+cr2hpt50=
|
||||
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
|
||||
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
|
||||
|
||||
0
kittens/desktop_ui/__init__.py
Normal file
0
kittens/desktop_ui/__init__.py
Normal file
192
kittens/desktop_ui/main.go
Normal file
192
kittens/desktop_ui/main.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package desktop_ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kovidgoyal/dbus"
|
||||
"github.com/kovidgoyal/kitty/tools/cli"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
type Options struct {
|
||||
Color_scheme, AccentColor, Contrast string
|
||||
}
|
||||
|
||||
func run_server(opts *Options) (err error) {
|
||||
portal, err := NewPortal(opts)
|
||||
if err == nil {
|
||||
err = portal.Start()
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c := make(chan string)
|
||||
<-c
|
||||
return
|
||||
}
|
||||
|
||||
func EntryPoint(root *cli.Command) {
|
||||
parent := root.AddSubCommand(&cli.Command{
|
||||
Name: "desktop-ui",
|
||||
ShortDescription: "Implement various desktop components for use with lightweight compositors/window managers on Linux",
|
||||
Run: func(cmd *cli.Command, args []string) (int, error) {
|
||||
cmd.ShowHelp()
|
||||
return 1, nil
|
||||
},
|
||||
})
|
||||
rs := parent.AddSubCommand(&cli.Command{
|
||||
Name: "run-server",
|
||||
ShortDescription: "Start the various servers used to integrate with the Linux desktop",
|
||||
HelpText: "This should be run very early in the startup sequence of your window manager, before any other programs are run.",
|
||||
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
|
||||
opts := Options{}
|
||||
err = cmd.GetOptionValues(&opts)
|
||||
if err == nil {
|
||||
err = run_server(&opts)
|
||||
}
|
||||
return utils.IfElse(err == nil, 0, 1), err
|
||||
},
|
||||
})
|
||||
rs.Add(cli.OptionSpec{
|
||||
Name: `--color-scheme`, Type: "choices", Dest: `Color_scheme`, Choices: "no-preference, light, dark",
|
||||
Completer: cli.NamesCompleter("Choices for color-scheme", "no-preference", "light", "dark"),
|
||||
Help: "The color scheme for your system. This sets the initial value of the color scheme. It can be changed subsequently by using the color-scheme sub-command.",
|
||||
})
|
||||
rs.Add(cli.OptionSpec{
|
||||
Name: `--accent-color`,
|
||||
Help: "The RGB accent color for your system, can be specified as a color name or in hex a decimal format.",
|
||||
Default: "cyan",
|
||||
})
|
||||
rs.Add(cli.OptionSpec{
|
||||
Name: `--contrast`, Type: "choices", Choices: "normal, high",
|
||||
Help: "The preferred contrast level. Choices: normal, high",
|
||||
Default: "normal",
|
||||
})
|
||||
|
||||
parent.AddSubCommand(&cli.Command{
|
||||
Name: "enable-portal",
|
||||
ShortDescription: "This will create or edit the various files needed so that the portal from this kitten is used by xdg-desktop-portal",
|
||||
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
|
||||
err = enable_portal()
|
||||
return utils.IfElse(err == nil, 0, 1), err
|
||||
},
|
||||
})
|
||||
parent.AddSubCommand(&cli.Command{
|
||||
Name: "set-color-scheme",
|
||||
ShortDescription: "Change the color scheme",
|
||||
ArgCompleter: cli.NamesCompleter("Choices for color-scheme", "no-preference", "light", "dark", "toggle"),
|
||||
Usage: " light|dark|no-preference|toggle",
|
||||
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
|
||||
if len(args) != 1 {
|
||||
cmd.ShowHelp()
|
||||
return 1, fmt.Errorf("must specify the new color scheme value")
|
||||
}
|
||||
err = set_color_scheme(args[0])
|
||||
return utils.IfElse(err == nil, 0, 1), err
|
||||
},
|
||||
})
|
||||
parent.AddSubCommand(&cli.Command{
|
||||
Name: "set-accent-color",
|
||||
ShortDescription: "Change the accent color",
|
||||
Usage: " color_as_hex_or_name",
|
||||
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
|
||||
if len(args) != 1 {
|
||||
cmd.ShowHelp()
|
||||
return 1, fmt.Errorf("must specify the new accent color value")
|
||||
}
|
||||
var v dbus.Variant
|
||||
if v, err = to_color(args[0]); err == nil {
|
||||
err = set_variant_setting(PORTAL_APPEARANCE_NAMESPACE, PORTAL_ACCENT_COLOR_KEY, v, false)
|
||||
}
|
||||
return utils.IfElse(err == nil, 0, 1), err
|
||||
},
|
||||
})
|
||||
parent.AddSubCommand(&cli.Command{
|
||||
Name: "set-contrast",
|
||||
ShortDescription: "Change the contrast. Can be high or normal.",
|
||||
Usage: " high|normal",
|
||||
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
|
||||
if len(args) != 1 {
|
||||
cmd.ShowHelp()
|
||||
return 1, fmt.Errorf("must specify the new contrast value")
|
||||
}
|
||||
|
||||
var v dbus.Variant
|
||||
switch args[0] {
|
||||
case "normal":
|
||||
v = dbus.MakeVariant(uint32(0))
|
||||
case "high":
|
||||
v = dbus.MakeVariant(uint32(1))
|
||||
default:
|
||||
return 1, fmt.Errorf("%s is not a valid contrast value", args[0])
|
||||
}
|
||||
err = set_variant_setting(PORTAL_APPEARANCE_NAMESPACE, PORTAL_CONTRAST_KEY, v, false)
|
||||
return utils.IfElse(err == nil, 0, 1), err
|
||||
},
|
||||
})
|
||||
st := parent.AddSubCommand(&cli.Command{
|
||||
Name: "set-setting",
|
||||
ShortDescription: "Change an arbitrary setting",
|
||||
Usage: " key [value]",
|
||||
HelpText: "Set an arbitrary setting. If you want to set the color-scheme use the dedicated command for it. Use this command with care as it does no validation for the type of value. The syntax for specifying values is described at: :link:`the glib docs <https://docs.gtk.org/glib/gvariant-text-format.html>`. Leaving out the value or specifying an empty value, will delete the setting.",
|
||||
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
|
||||
val := ""
|
||||
if len(args) < 1 {
|
||||
cmd.ShowHelp()
|
||||
return 1, fmt.Errorf("must specify the key")
|
||||
}
|
||||
if len(args) > 1 {
|
||||
val = args[1]
|
||||
}
|
||||
opts := SetOptions{}
|
||||
if err = cmd.GetOptionValues(&opts); err == nil {
|
||||
err = set_setting(args[0], val, &opts)
|
||||
}
|
||||
return utils.IfElse(err == nil, 0, 1), err
|
||||
},
|
||||
})
|
||||
st.Add(cli.OptionSpec{
|
||||
Name: "--namespace -n",
|
||||
Help: "The namespace in which to change the setting.",
|
||||
Default: PORTAL_APPEARANCE_NAMESPACE,
|
||||
})
|
||||
st.Add(cli.OptionSpec{
|
||||
Name: "--data-type",
|
||||
Help: "The DBUS data type signature of the value. The default is to guess from the textual representation, see :link:`the glib docs <https://docs.gtk.org/glib/gvariant-text-format.html>` for details.",
|
||||
})
|
||||
|
||||
ss := parent.AddSubCommand(&cli.Command{
|
||||
Name: "show-settings",
|
||||
ShortDescription: "Print the current values of the desktop settings",
|
||||
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
|
||||
if len(args) != 0 {
|
||||
cmd.ShowHelp()
|
||||
return 1, fmt.Errorf("no arguments allowed")
|
||||
}
|
||||
opts := ShowSettingsOptions{}
|
||||
err = cmd.GetOptionValues(&opts)
|
||||
if err == nil {
|
||||
err = show_settings(&opts)
|
||||
}
|
||||
return utils.IfElse(err == nil, 0, 1), err
|
||||
},
|
||||
})
|
||||
ss.Add(cli.OptionSpec{
|
||||
Name: "--as-json",
|
||||
Help: "Show the settings as JSON for machine consumption",
|
||||
Type: "bool-set",
|
||||
})
|
||||
ss.Add(cli.OptionSpec{
|
||||
Name: "--in-namespace",
|
||||
Help: "Show only settings in the specified names. Can be specified multiple times. When unspecified all namespaces are returned.",
|
||||
Type: "list",
|
||||
})
|
||||
ss.Add(cli.OptionSpec{
|
||||
Name: "--allow-other-backends",
|
||||
Help: "Normally, after printing the settings, if the settings did not come from the desktop-ui kitten the command prints an error and exits. This prevents that.",
|
||||
Type: "bool-set",
|
||||
})
|
||||
|
||||
}
|
||||
4
kittens/desktop_ui/main.py
Normal file
4
kittens/desktop_ui/main.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
601
kittens/desktop_ui/portal.go
Normal file
601
kittens/desktop_ui/portal.go
Normal file
@@ -0,0 +1,601 @@
|
||||
package desktop_ui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/kovidgoyal/dbus"
|
||||
"github.com/kovidgoyal/dbus/introspect"
|
||||
"github.com/kovidgoyal/dbus/prop"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/style"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
const PORTAL_APPEARANCE_NAMESPACE = "org.freedesktop.appearance"
|
||||
const PORTAL_COLOR_SCHEME_KEY = "color-scheme"
|
||||
const PORTAL_ACCENT_COLOR_KEY = "accent-color"
|
||||
const PORTAL_CONTRAST_KEY = "contrast"
|
||||
const PORTAL_BUS_NAME = "org.freedesktop.impl.portal.desktop.kitty"
|
||||
const SETTINGS_OBJECT_PATH = "/org/freedesktop/portal/desktop"
|
||||
const SETTINGS_INTERFACE = "org.freedesktop.impl.portal.Settings"
|
||||
const CHANGE_SETTINGS_OBJECT_PATH = "/net/kovidgoyal/kitty/portal"
|
||||
const CHANGE_SETTINGS_INTERFACE = "net.kovidgoyal.kitty.settings"
|
||||
const DESKTOP_PORTAL_NAME = "org.freedesktop.portal.Desktop"
|
||||
|
||||
// Special portal setting used to check if we are being called by xdg-desktop-portal
|
||||
const SETTINGS_CANARY_NAMESPACE = "net.kovidgoyal.kitty"
|
||||
const SETTINGS_CANARY_KEY = "status"
|
||||
|
||||
type ColorScheme uint32
|
||||
|
||||
const (
|
||||
NO_PREFERENCE ColorScheme = iota
|
||||
DARK
|
||||
LIGHT
|
||||
)
|
||||
|
||||
type SettingsMap map[string]map[string]dbus.Variant
|
||||
|
||||
type Portal struct {
|
||||
bus *dbus.Conn
|
||||
settings SettingsMap
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func to_color(spec string) (v dbus.Variant, err error) {
|
||||
if col, err := style.ParseColor(spec); err == nil {
|
||||
return dbus.MakeVariant([]float64{float64(col.Red) / 255., float64(col.Green) / 255., float64(col.Blue) / 255.}), nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NewPortal(opts *Options) (p *Portal, err error) {
|
||||
ans := Portal{}
|
||||
ans.settings = SettingsMap{
|
||||
SETTINGS_CANARY_NAMESPACE: map[string]dbus.Variant{
|
||||
SETTINGS_CANARY_KEY: dbus.MakeVariant("running"),
|
||||
},
|
||||
}
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE] = map[string]dbus.Variant{}
|
||||
switch opts.Color_scheme {
|
||||
case "dark":
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(DARK))
|
||||
case "light":
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(LIGHT))
|
||||
default:
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_COLOR_SCHEME_KEY] = dbus.MakeVariant(uint32(NO_PREFERENCE))
|
||||
}
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_ACCENT_COLOR_KEY], err = to_color(opts.AccentColor)
|
||||
var contrast uint32
|
||||
if opts.Contrast == "high" {
|
||||
contrast = 1
|
||||
}
|
||||
ans.settings[PORTAL_APPEARANCE_NAMESPACE][PORTAL_CONTRAST_KEY] = dbus.MakeVariant(contrast)
|
||||
return &ans, nil
|
||||
}
|
||||
|
||||
type PropSpec map[string]*prop.Prop
|
||||
type SignalSpec map[string][]struct {
|
||||
Name, Type string
|
||||
}
|
||||
type MethodSpec map[string][]struct {
|
||||
Name, Type string
|
||||
Out bool
|
||||
}
|
||||
|
||||
func ExportInterface(conn *dbus.Conn, object any, interface_name, object_path string, method_spec MethodSpec, prop_spec PropSpec, signal_spec SignalSpec) (err error) {
|
||||
op := dbus.ObjectPath(object_path)
|
||||
method_map := make(map[string]string, len(method_spec))
|
||||
methods := []introspect.Method{}
|
||||
if len(method_spec) > 0 {
|
||||
for method_name, args := range method_spec {
|
||||
method_map[method_name] = method_name
|
||||
meth_args := make([]introspect.Arg, len(args))
|
||||
for i, a := range args {
|
||||
meth_args[i] = introspect.Arg{
|
||||
Name: a.Name,
|
||||
Type: a.Type,
|
||||
Direction: utils.IfElse(a.Out, "out", "in"),
|
||||
}
|
||||
}
|
||||
methods = append(methods, introspect.Method{
|
||||
Name: method_name,
|
||||
Args: meth_args,
|
||||
})
|
||||
}
|
||||
}
|
||||
if err = conn.ExportWithMap(object, method_map, op, interface_name); err != nil {
|
||||
return fmt.Errorf("failed to export interface: %s at object path: %s with error: %w", interface_name, object_path, err)
|
||||
}
|
||||
var properties []introspect.Property
|
||||
p := prop.Map{interface_name: prop_spec}
|
||||
if len(prop_spec) > 0 {
|
||||
if props, err := prop.Export(conn, op, p); err != nil {
|
||||
return fmt.Errorf("failed to export properties with error: %w", err)
|
||||
} else {
|
||||
properties = props.Introspection(interface_name)
|
||||
}
|
||||
}
|
||||
var signals []introspect.Signal
|
||||
if len(signal_spec) > 0 {
|
||||
for signal_name, args := range signal_spec {
|
||||
sig_args := make([]introspect.Arg, len(args))
|
||||
for i, a := range args {
|
||||
sig_args[i] = introspect.Arg{
|
||||
Name: a.Name,
|
||||
Type: a.Type,
|
||||
Direction: "out",
|
||||
}
|
||||
}
|
||||
signals = append(signals, introspect.Signal{
|
||||
Name: signal_name,
|
||||
Args: sig_args,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
interface_data := introspect.Interface{
|
||||
Name: interface_name,
|
||||
Methods: methods,
|
||||
Properties: properties,
|
||||
Signals: signals,
|
||||
}
|
||||
interfaces := []introspect.Interface{
|
||||
introspect.IntrospectData, interface_data,
|
||||
}
|
||||
if len(properties) > 0 {
|
||||
interfaces = append(interfaces, prop.IntrospectData)
|
||||
}
|
||||
n := &introspect.Node{Name: object_path, Interfaces: interfaces}
|
||||
if err = conn.Export(introspect.NewIntrospectable(n), op, introspect.IntrospectData.Name); err != nil {
|
||||
return fmt.Errorf("failed to export introspected methods with error: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Portal) Start() (err error) {
|
||||
if self.bus, err = dbus.SessionBus(); err != nil {
|
||||
return fmt.Errorf("could not connect to session D-Bus: %s", err)
|
||||
}
|
||||
reply, err := self.bus.RequestName(PORTAL_BUS_NAME, dbus.NameFlagDoNotQueue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register dbus name: %v", err)
|
||||
}
|
||||
if reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
return fmt.Errorf("can't register D-Bus name: name already taken")
|
||||
}
|
||||
props := PropSpec{
|
||||
"version": {Value: uint32(1), Writable: false, Emit: prop.EmitFalse},
|
||||
}
|
||||
signals := SignalSpec{
|
||||
"SettingChanged": {{"namespace", "s"}, {"key", "s"}, {"value", "v"}},
|
||||
}
|
||||
methods := MethodSpec{
|
||||
"Read": {{"namespace", "s", false}, {"key", "s", false}, {"value", "v", true}},
|
||||
"ReadAll": {{"namespaces", "as", false}, {"value", "a{sa{sv}}", true}},
|
||||
}
|
||||
if err = ExportInterface(self.bus, self, SETTINGS_INTERFACE, SETTINGS_OBJECT_PATH, methods, props, signals); err != nil {
|
||||
return
|
||||
}
|
||||
methods = MethodSpec{
|
||||
"ChangeSetting": {{"namespace", "s", false}, {"key", "s", false}, {"value", "v", false}},
|
||||
"RemoveSetting": {{"namespace", "s", false}, {"key", "s", false}},
|
||||
}
|
||||
props["version"].Value = uint32(1)
|
||||
if err = ExportInterface(self.bus, self, CHANGE_SETTINGS_INTERFACE, CHANGE_SETTINGS_OBJECT_PATH, methods, props, nil); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseValueWithSignature(value, value_type_signature string) (v dbus.Variant, err error) {
|
||||
var s dbus.Signature
|
||||
if value_type_signature != "" {
|
||||
if value_type_signature[0] == '@' {
|
||||
value_type_signature = value_type_signature[1:]
|
||||
}
|
||||
s, err = dbus.ParseSignature(value_type_signature)
|
||||
if err != nil {
|
||||
return dbus.Variant{}, fmt.Errorf("%s is not a valid type signature: %w", value_type_signature, err)
|
||||
}
|
||||
}
|
||||
v, err = dbus.ParseVariant(value, s)
|
||||
if err != nil {
|
||||
if value_type_signature == "" {
|
||||
return dbus.Variant{}, fmt.Errorf("could not guess the data type of: %s with error: %w", value, err)
|
||||
}
|
||||
return dbus.Variant{}, fmt.Errorf("%s is not a valid value for signature: %#v with error: %w", value, value_type_signature, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func ParseValue(value string) (dbus.Variant, error) {
|
||||
return ParseValueWithSignature(value, "")
|
||||
}
|
||||
|
||||
type ShowSettingsOptions struct {
|
||||
AsJson bool
|
||||
AllowOtherBackends bool
|
||||
InNamespace []string
|
||||
}
|
||||
|
||||
func fetch_settings(conn *dbus.Conn, namespaces ...string) (ans ReadAllType, err error) {
|
||||
path := "/" + strings.ToLower(strings.ReplaceAll(DESKTOP_PORTAL_NAME, ".", "/"))
|
||||
obj := conn.Object(DESKTOP_PORTAL_NAME, dbus.ObjectPath(path))
|
||||
interface_name := strings.ReplaceAll(DESKTOP_PORTAL_NAME, "Desktop", "Settings")
|
||||
if len(namespaces) == 0 {
|
||||
namespaces = append(namespaces, "")
|
||||
}
|
||||
call := obj.Call(interface_name+".ReadAll", dbus.FlagNoAutoStart, namespaces)
|
||||
if err = call.Store(&ans); err != nil {
|
||||
return nil, fmt.Errorf("Failed to read response from ReadAll with error: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func show_settings(opts *ShowSettingsOptions) (err error) {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to system bus with error: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
var response ReadAllType
|
||||
response, err = fetch_settings(conn, opts.InNamespace...)
|
||||
if opts.AsJson {
|
||||
unwrapped := make(map[string]map[string]any, len(response))
|
||||
for ns, m := range response {
|
||||
w := make(map[string]any, len(m))
|
||||
for k, a := range m {
|
||||
w[k] = a.Value()
|
||||
}
|
||||
unwrapped[ns] = w
|
||||
}
|
||||
j, err := json.MarshalIndent(unwrapped, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to format the response as JSON: %w", err)
|
||||
}
|
||||
fmt.Println(string(j))
|
||||
} else {
|
||||
for ns, m := range response {
|
||||
fmt.Println(ns + ":")
|
||||
for key, v := range m {
|
||||
fmt.Printf("\t%s: %s\n", key, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !opts.AllowOtherBackends {
|
||||
is_running_self := false
|
||||
if m, found := response[SETTINGS_CANARY_NAMESPACE]; found {
|
||||
_, is_running_self = m[SETTINGS_CANARY_KEY]
|
||||
}
|
||||
if !is_running_self {
|
||||
err = fmt.Errorf("the settings did not come from the desktop-ui kitten. Some other portal backend is providing the service.")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var DataDirs = sync.OnceValue(func() (ans []string) {
|
||||
d := os.Getenv("XDG_DATA_DIRS")
|
||||
if d == "" {
|
||||
d = "/usr/local/share/:/usr/share/"
|
||||
}
|
||||
all := []string{os.Getenv("XDG_DATA_HOME")}
|
||||
all = append(all, strings.Split(d, ":")...)
|
||||
seen := map[string]bool{}
|
||||
for _, x := range all {
|
||||
if !seen[x] {
|
||||
seen[x] = true
|
||||
ans = append(ans, x)
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
func IsDir(x string) bool {
|
||||
s, err := os.Stat(x)
|
||||
return err == nil && s.IsDir()
|
||||
}
|
||||
|
||||
var WritableDataDirs = sync.OnceValue(func() (ans []string) {
|
||||
for _, x := range DataDirs() {
|
||||
if err := os.MkdirAll(x, 0o755); err == nil && unix.Access(x, unix.W_OK) == nil {
|
||||
ans = append(ans, x)
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
var AllPortalInterfaces = sync.OnceValue(func() (ans []string) {
|
||||
return []string{SETTINGS_INTERFACE}
|
||||
})
|
||||
|
||||
// enable-portal {{{
|
||||
func patch_portals_conf(text []byte) []byte {
|
||||
lines := []string{}
|
||||
in_preferred := false
|
||||
for _, line := range utils.Splitlines(utils.UnsafeBytesToString(text)) {
|
||||
sl := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(sl, "[") {
|
||||
in_preferred = sl == "[preferred]"
|
||||
lines = append(lines, line)
|
||||
for _, iface := range AllPortalInterfaces() {
|
||||
lines = append(lines, iface+"=kitty")
|
||||
}
|
||||
} else if in_preferred {
|
||||
remove := false
|
||||
for _, iface := range AllPortalInterfaces() {
|
||||
if strings.HasPrefix(sl, iface) {
|
||||
remove = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !remove {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
return utils.UnsafeStringToBytes(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func enable_portal() (err error) {
|
||||
if len(WritableDataDirs()) == 0 {
|
||||
return fmt.Errorf("Could not find any writable data directories. Make sure XDG_DATA_DIRS is set and contains at least one directory for which you have write permission")
|
||||
}
|
||||
portals_dir := ""
|
||||
for _, x := range WritableDataDirs() {
|
||||
q := filepath.Join(x, "xdg-desktop-portal", "portals")
|
||||
if unix.Access(q, unix.W_OK) == nil && IsDir(q) {
|
||||
portals_dir = q
|
||||
break
|
||||
}
|
||||
}
|
||||
if portals_dir == "" {
|
||||
for _, x := range WritableDataDirs() {
|
||||
q := filepath.Join(x, "xdg-desktop-portal", "portals")
|
||||
if err := os.MkdirAll(q, 0o755); err == nil {
|
||||
portals_dir = q
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if portals_dir == "" {
|
||||
return fmt.Errorf("Could not find any writable portals directories. Make sure XDG_DATA_HOME is set and point to a directory for which you have write permission.")
|
||||
}
|
||||
portals_defn := filepath.Join(portals_dir, "kitty.portal")
|
||||
if err = os.WriteFile(portals_defn, utils.UnsafeStringToBytes(fmt.Sprintf(
|
||||
`[portal]
|
||||
DBusName=%s
|
||||
Interfaces=%s;
|
||||
`, PORTAL_BUS_NAME, strings.Join(AllPortalInterfaces(), ";"))), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Wrote kitty portal definition to:", portals_defn)
|
||||
dbus_service_dir := ""
|
||||
for _, x := range WritableDataDirs() {
|
||||
q := filepath.Join(x, "dbus-1", "services")
|
||||
if err := os.MkdirAll(q, 0o755); err == nil {
|
||||
dbus_service_dir = q
|
||||
break
|
||||
}
|
||||
}
|
||||
if dbus_service_dir == "" {
|
||||
return fmt.Errorf("Could not find any writable portals directories. Make sure XDG_DATA_HOME is set and point to a directory for which you have write permission.")
|
||||
}
|
||||
dbus_service_defn := filepath.Join(dbus_service_dir, PORTAL_BUS_NAME+".desktop")
|
||||
if err = os.WriteFile(dbus_service_defn, utils.UnsafeStringToBytes(fmt.Sprintf(
|
||||
`[D-BUS Service]
|
||||
Name=%s
|
||||
Exec=kitten run-server
|
||||
`, PORTAL_BUS_NAME)), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Wrote kitty DBUS activation service file to:", dbus_service_defn)
|
||||
|
||||
d := os.Getenv("XDG_CURRENT_DESKTOP")
|
||||
cf := os.Getenv("XDG_CONFIG_HOME")
|
||||
if cf == "" {
|
||||
cf = utils.Expanduser("~/.config")
|
||||
}
|
||||
cf = filepath.Join(cf, "xdg-desktop-portal")
|
||||
if err = os.MkdirAll(cf, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create %s to store the portals.conf file with error: %w", cf, err)
|
||||
}
|
||||
patched_file := ""
|
||||
desktops := utils.Filter(strings.Split(d, ":"), func(x string) bool { return x != "" })
|
||||
desktops = append(desktops, "")
|
||||
for _, x := range strings.Split(d, ":") {
|
||||
q := filepath.Join(cf, utils.IfElse(x == "", "portals.conf", fmt.Sprintf("%s-portals.conf", strings.ToLower(x))))
|
||||
if text, err := os.ReadFile(q); err == nil {
|
||||
text := patch_portals_conf(text)
|
||||
if err = os.WriteFile(q, text, 0o644); err == nil {
|
||||
patched_file = q
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if patched_file == "" {
|
||||
x := desktops[0]
|
||||
q := filepath.Join(cf, utils.IfElse(x == "", "portals.conf", fmt.Sprintf("%s-portals.conf", strings.ToLower(x))))
|
||||
text := patch_portals_conf([]byte{})
|
||||
if err = os.WriteFile(q, text, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
patched_file = q
|
||||
}
|
||||
fmt.Printf("Patched %s to use the kitty portals\n", patched_file)
|
||||
return
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
||||
type SetOptions struct {
|
||||
Namespace, DataType string
|
||||
}
|
||||
|
||||
func set_variant_setting(namespace, key string, v dbus.Variant, remove_setting bool) (err error) {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to system bus with error: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
method := "ChangeSetting"
|
||||
var vals = []any{namespace, key}
|
||||
if remove_setting {
|
||||
method = "RemoveSetting"
|
||||
} else {
|
||||
vals = append(vals, v)
|
||||
}
|
||||
obj := conn.Object(PORTAL_BUS_NAME, dbus.ObjectPath(CHANGE_SETTINGS_OBJECT_PATH))
|
||||
call := obj.Call(CHANGE_SETTINGS_INTERFACE+"."+method, dbus.FlagNoAutoStart, vals...)
|
||||
if err = call.Store(); err != nil {
|
||||
return fmt.Errorf("failed to call %s with error: %w", method, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func set_setting(key, value string, opts *SetOptions) (err error) {
|
||||
remove_setting := false
|
||||
var v dbus.Variant
|
||||
if value == "" {
|
||||
remove_setting = true
|
||||
} else {
|
||||
if v, err = ParseValueWithSignature(value, opts.DataType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return set_variant_setting(opts.Namespace, key, v, remove_setting)
|
||||
}
|
||||
|
||||
func set_color_scheme(which string) (err error) {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to system bus with error: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
val := NO_PREFERENCE
|
||||
var res ReadAllType
|
||||
if res, err = fetch_settings(conn, PORTAL_APPEARANCE_NAMESPACE); err != nil {
|
||||
return fmt.Errorf("failed to read existing color scheme setting with error: %w", err)
|
||||
}
|
||||
if m, found := res[PORTAL_APPEARANCE_NAMESPACE]; found {
|
||||
if v, found := m[PORTAL_COLOR_SCHEME_KEY]; found {
|
||||
v.Store(&val)
|
||||
}
|
||||
}
|
||||
nval := val
|
||||
switch which {
|
||||
case "toggle":
|
||||
switch val {
|
||||
case LIGHT:
|
||||
nval = DARK
|
||||
case DARK:
|
||||
nval = LIGHT
|
||||
}
|
||||
case "no-preference":
|
||||
nval = NO_PREFERENCE
|
||||
case "light":
|
||||
nval = LIGHT
|
||||
case "dark":
|
||||
nval = DARK
|
||||
default:
|
||||
return fmt.Errorf("%s is not a valid value of the color-scheme", which)
|
||||
}
|
||||
if val == nval {
|
||||
return
|
||||
}
|
||||
obj := conn.Object(PORTAL_BUS_NAME, dbus.ObjectPath(CHANGE_SETTINGS_OBJECT_PATH))
|
||||
call := obj.Call(CHANGE_SETTINGS_INTERFACE+".ChangeSetting", dbus.FlagNoAutoStart, PORTAL_APPEARANCE_NAMESPACE, PORTAL_COLOR_SCHEME_KEY, dbus.MakeVariant(nval))
|
||||
if err = call.Store(); err != nil {
|
||||
return fmt.Errorf("failed to call ChangeSetting with error: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Portal) ChangeSetting(namespace, key string, value dbus.Variant) *dbus.Error {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if self.settings[namespace] == nil {
|
||||
self.settings[namespace] = map[string]dbus.Variant{}
|
||||
}
|
||||
self.settings[namespace][key] = value
|
||||
|
||||
if e := self.bus.Emit(
|
||||
SETTINGS_OBJECT_PATH,
|
||||
SETTINGS_INTERFACE+".SettingChanged",
|
||||
namespace,
|
||||
key,
|
||||
value,
|
||||
); e != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't emit signal: %s", e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Portal) RemoveSetting(namespace, key string) *dbus.Error {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
existed := false
|
||||
if m := self.settings[namespace]; m != nil {
|
||||
_, existed = m[key]
|
||||
}
|
||||
if !existed {
|
||||
return nil
|
||||
}
|
||||
delete(self.settings[namespace], key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Portal) Read(namespace, key string) (dbus.Variant, *dbus.Error) {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if m, found := self.settings[namespace]; found {
|
||||
if v, found := m[key]; found {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return dbus.Variant{}, dbus.NewError("org.freedesktop.portal.Error.NotFound", []any{fmt.Sprintf("the setting %s in the namespace %s is not supported", key, namespace)})
|
||||
}
|
||||
|
||||
type ReadAllType map[string]map[string]dbus.Variant
|
||||
|
||||
func (self *Portal) ReadAll(namespaces []string) (ReadAllType, *dbus.Error) {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
var matched_namespaces = SettingsMap{}
|
||||
if len(namespaces) == 0 {
|
||||
matched_namespaces = self.settings
|
||||
} else {
|
||||
for _, namespace := range namespaces {
|
||||
if namespace == "" {
|
||||
matched_namespaces = self.settings
|
||||
break
|
||||
} else {
|
||||
if strings.HasSuffix(namespace, ".*") {
|
||||
namespace = namespace[:len(namespace)-1]
|
||||
for candidate := range self.settings {
|
||||
if strings.HasPrefix(candidate, namespace) {
|
||||
matched_namespaces[candidate] = map[string]dbus.Variant{}
|
||||
}
|
||||
}
|
||||
} else if _, found := self.settings[namespace]; found {
|
||||
matched_namespaces[namespace] = map[string]dbus.Variant{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
values := map[string]map[string]dbus.Variant{}
|
||||
for namespace := range matched_namespaces {
|
||||
values[namespace] = make(map[string]dbus.Variant, len(self.settings[namespace]))
|
||||
maps.Copy(values[namespace], self.settings[namespace])
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
@@ -161,6 +161,8 @@ def actual_main(sys_args: list[str]) -> None:
|
||||
sys.argv.extend(('--name', args.name))
|
||||
if args.start_as_hidden:
|
||||
sys.argv.append('--start-as=hidden')
|
||||
if args.grab_keyboard:
|
||||
sys.argv.append('--grab-keyboard')
|
||||
for override in args.override:
|
||||
sys.argv.extend(('--override', override))
|
||||
sys.argv.append('--override=linux_display_server=auto')
|
||||
|
||||
@@ -74,6 +74,9 @@ func main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) {
|
||||
if conf.Start_as_hidden {
|
||||
argv = append(argv, `--start-as-hidden`)
|
||||
}
|
||||
if conf.Grab_keyboard {
|
||||
argv = append(argv, `--grab-keyboard`)
|
||||
}
|
||||
if conf.Hide_on_focus_loss {
|
||||
argv = append(argv, `--hide-on-focus-loss`)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import sys
|
||||
|
||||
from kitty.conf.types import Definition
|
||||
from kitty.constants import appname
|
||||
from kitty.simple_cli_definitions import CONFIG_HELP
|
||||
from kitty.simple_cli_definitions import CONFIG_HELP, grab_keyboard_docs
|
||||
|
||||
help_text = 'A quick access terminal window that you can bring up instantly with a keypress or a command.'
|
||||
|
||||
@@ -45,6 +45,8 @@ Hide the window when it loses keyboard focus automatically. Using this option
|
||||
will force :opt:`focus_policy` to :code:`on-demand`.
|
||||
''')
|
||||
|
||||
opt('grab_keyboard', 'no', option_type='to_bool', long_text=grab_keyboard_docs)
|
||||
|
||||
opt('margin_left', '0', option_type='int',
|
||||
long_text='Set the left margin for the window, in pixels. Has no effect for windows on the right edge of the screen.')
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ from .fast_data_types import (
|
||||
get_os_window_size,
|
||||
glfw_get_monitor_workarea,
|
||||
global_font_size,
|
||||
grab_keyboard,
|
||||
is_layer_shell_supported,
|
||||
last_focused_os_window_id,
|
||||
mark_os_window_for_close,
|
||||
@@ -122,6 +123,7 @@ from .options.utils import MINIMUM_FONT_SIZE, KeyboardMode, KeyDefinition
|
||||
from .os_window_size import initial_window_size_func
|
||||
from .session import Session, create_sessions, get_os_window_sizing_data
|
||||
from .shaders import load_shader_programs
|
||||
from .simple_cli_definitions import grab_keyboard_docs
|
||||
from .tabs import SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager
|
||||
from .types import _T, AsyncResponse, LayerShellConfig, SingleInstanceData, WindowSystemMouseEvent, ac
|
||||
from .typing_compat import PopenType, TypedDict
|
||||
@@ -3202,3 +3204,11 @@ class Boss:
|
||||
if wid == exception:
|
||||
continue
|
||||
window.screen.clear_selection()
|
||||
|
||||
@ac('misc', grab_keyboard_docs)
|
||||
def grab_keyboard(self) -> None:
|
||||
grab_keyboard(True)
|
||||
|
||||
@ac('misc', 'Ungrab the keyboard if it was previously grabbed')
|
||||
def ungrab_keyboard(self) -> None:
|
||||
grab_keyboard(False)
|
||||
|
||||
@@ -625,7 +625,7 @@ py_run_atexit_cleanup_functions(PyObject *self UNUSED, PyObject *args UNUSED) {
|
||||
|
||||
static PyObject*
|
||||
py_char_props_for(PyObject *self UNUSED, PyObject *ch) {
|
||||
if (!PyUnicode_Check(ch) || PyUnicode_GET_LENGTH(ch) != 1) { PyErr_SetString(PyExc_TypeError, "must suply a single character"); return NULL; }
|
||||
if (!PyUnicode_Check(ch) || PyUnicode_GET_LENGTH(ch) != 1) { PyErr_SetString(PyExc_TypeError, "must supply a single character"); return NULL; }
|
||||
char_type c = PyUnicode_READ_CHAR(ch, 0);
|
||||
CharProps cp = char_props_for(c);
|
||||
#define B(x) #x, cp.x ? Py_True : Py_False
|
||||
|
||||
@@ -1748,6 +1748,7 @@ def buffer_keys_in_window(os_window_id: int, tab_id: int, window_id: int, enable
|
||||
def sprite_idx_to_pos(idx: int, xnum: int, ynum: int) -> tuple[int, int, int]: ...
|
||||
def render_box_char(ch: int, width: int, height: int, scale: float = 1.0, dpi_x: float = 96.0, dpi_y: float = 96.0) -> bytes: ...
|
||||
def run_at_exit_cleanup_functions() -> None: ...
|
||||
def grab_keyboard(grab: bool | None) -> bool: ...
|
||||
DecorationTypes = Literal[
|
||||
'curl', 'dashed', 'dotted', 'double', 'straight', 'strikethrough', 'beam_cursor', 'underline_cursor', 'hollow_cursor', 'missing']
|
||||
def render_decoration(
|
||||
|
||||
@@ -431,7 +431,7 @@ specialize_font_descriptor(PyObject *base_descriptor, double font_sz_in_pts, dou
|
||||
FcPatternDestroy(pat); pat = NULL;
|
||||
if (!ans) return NULL;
|
||||
// fontconfig returns a completely random font if the base descriptor
|
||||
// points to a font that fontconfig hasnt indexed, for example the builting
|
||||
// points to a font that fontconfig hasnt indexed, for example the built-in
|
||||
// NERD font
|
||||
PyObject *new_path = PyDict_GetItemString(ans, "path");
|
||||
if (!new_path || PyObject_RichCompareBool(p, new_path, Py_EQ) != 1) { Py_CLEAR(ans); ans = PyDict_Copy(base_descriptor); if (!ans) return NULL; }
|
||||
|
||||
3
kitty/glfw-wrapper.c
generated
3
kitty/glfw-wrapper.c
generated
@@ -293,6 +293,9 @@ load_glfw(const char* path) {
|
||||
*(void **) (&glfwSetIgnoreOSKeyboardProcessing_impl) = dlsym(handle, "glfwSetIgnoreOSKeyboardProcessing");
|
||||
if (glfwSetIgnoreOSKeyboardProcessing_impl == NULL) fail("Failed to load glfw function glfwSetIgnoreOSKeyboardProcessing with error: %s", dlerror());
|
||||
|
||||
*(void **) (&glfwGrabKeyboard_impl) = dlsym(handle, "glfwGrabKeyboard");
|
||||
if (glfwGrabKeyboard_impl == NULL) fail("Failed to load glfw function glfwGrabKeyboard with error: %s", dlerror());
|
||||
|
||||
*(void **) (&glfwGetInputMode_impl) = dlsym(handle, "glfwGetInputMode");
|
||||
if (glfwGetInputMode_impl == NULL) fail("Failed to load glfw function glfwGetInputMode with error: %s", dlerror());
|
||||
|
||||
|
||||
5
kitty/glfw-wrapper.h
generated
5
kitty/glfw-wrapper.h
generated
@@ -794,6 +794,7 @@ typedef enum {
|
||||
|
||||
#define GLFW_WAYLAND_APP_ID 0x00025001
|
||||
#define GLFW_WAYLAND_BGCOLOR 0x00025002
|
||||
#define GLFW_WAYLAND_WINDOW_TAG 0x00025003
|
||||
/*! @} */
|
||||
|
||||
#define GLFW_NO_API 0
|
||||
@@ -2089,6 +2090,10 @@ typedef void (*glfwSetIgnoreOSKeyboardProcessing_func)(bool);
|
||||
GFW_EXTERN glfwSetIgnoreOSKeyboardProcessing_func glfwSetIgnoreOSKeyboardProcessing_impl;
|
||||
#define glfwSetIgnoreOSKeyboardProcessing glfwSetIgnoreOSKeyboardProcessing_impl
|
||||
|
||||
typedef bool (*glfwGrabKeyboard_func)(int);
|
||||
GFW_EXTERN glfwGrabKeyboard_func glfwGrabKeyboard_impl;
|
||||
#define glfwGrabKeyboard glfwGrabKeyboard_impl
|
||||
|
||||
typedef int (*glfwGetInputMode_func)(GLFWwindow*, int);
|
||||
GFW_EXTERN glfwGetInputMode_func glfwGetInputMode_impl;
|
||||
#define glfwGetInputMode glfwGetInputMode_impl
|
||||
|
||||
@@ -2612,6 +2612,10 @@ set_layer_shell_config(PyObject *self UNUSED, PyObject *args) {
|
||||
return Py_NewRef(set_layer_shell_config_for(window, &lsc) ? Py_True : Py_False);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
grab_keyboard(PyObject *self UNUSED, PyObject *action) {
|
||||
return Py_NewRef(glfwGrabKeyboard(action == Py_None ? 2 : PyObject_IsTrue(action)) ? Py_True : Py_False);
|
||||
}
|
||||
|
||||
// Boilerplate {{{
|
||||
|
||||
@@ -2621,6 +2625,7 @@ static PyMethodDef module_methods[] = {
|
||||
METHODB(toggle_os_window_visibility, METH_VARARGS),
|
||||
METHODB(layer_shell_config_for_os_window, METH_O),
|
||||
METHODB(set_layer_shell_config, METH_VARARGS),
|
||||
METHODB(grab_keyboard, METH_O),
|
||||
METHODB(pointer_name_to_css_name, METH_O),
|
||||
{"create_os_window", (PyCFunction)(void (*) (void))(create_os_window), METH_VARARGS | METH_KEYWORDS, NULL},
|
||||
METHODB(set_default_window_icon, METH_VARARGS),
|
||||
|
||||
@@ -305,7 +305,7 @@ on_key_input(const GLFWkeyevent *ev) {
|
||||
}
|
||||
GLFWkeyevent *k = w->buffered_keys.key_data;
|
||||
k[w->buffered_keys.count++] = *ev;
|
||||
debug("bufferring key until child is ready\n");
|
||||
debug("buffering key until child is ready\n");
|
||||
} else send_key_to_child(w->id, screen, ev);
|
||||
#undef dispatch_key_event
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ from .fast_data_types import (
|
||||
SingleKey,
|
||||
get_boss,
|
||||
get_options,
|
||||
grab_keyboard,
|
||||
is_modifier_key,
|
||||
ring_bell,
|
||||
set_ignore_os_keyboard_processing,
|
||||
@@ -157,11 +158,15 @@ class Mappings:
|
||||
is_root_mode = not self.keyboard_mode_stack
|
||||
mode = self.keyboard_modes[''] if is_root_mode else self.keyboard_mode_stack[-1]
|
||||
key_action = get_shortcut(mode.keymap, ev)
|
||||
if key_action is None and self.global_shortcuts_map and (global_key_action := get_shortcut(self.global_shortcuts_map, ev)) is not None:
|
||||
if grab_keyboard(None):
|
||||
# the shortcuts in the global menubar will have been bypassed so trigger them here
|
||||
key_action = global_key_action
|
||||
else:
|
||||
return True
|
||||
if key_action is None:
|
||||
if is_modifier_key(ev.key):
|
||||
return False
|
||||
if self.global_shortcuts_map and get_shortcut(self.global_shortcuts_map, ev):
|
||||
return True
|
||||
if not is_root_mode:
|
||||
if mode.sequence_keys is not None:
|
||||
self.pop_keyboard_mode()
|
||||
|
||||
@@ -215,7 +215,7 @@ of bias depends on the current layout.
|
||||
|
||||
* Splits layout: The bias is interpreted as a percentage between 0 and 100.
|
||||
When splitting a window into two, the new window will take up the specified fraction
|
||||
of the space alloted to the original window and the original window will take up
|
||||
of the space allotted to the original window and the original window will take up
|
||||
the remainder of the space.
|
||||
|
||||
* Vertical/horizontal layout: The bias is interpreted as adding/subtracting from the
|
||||
|
||||
@@ -42,6 +42,7 @@ from .fast_data_types import (
|
||||
glfw_get_monitor_workarea,
|
||||
glfw_init,
|
||||
glfw_terminate,
|
||||
grab_keyboard,
|
||||
is_layer_shell_supported,
|
||||
load_png_data,
|
||||
mask_kitty_signals_process_wide,
|
||||
@@ -254,12 +255,14 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = (),
|
||||
global_shortcuts = {}
|
||||
set_window_icon()
|
||||
if _is_panel_kitten and not is_layer_shell_supported():
|
||||
raise SystemExit('Cannot create panels as the window manager/compositor does not support the neccessary protocols')
|
||||
raise SystemExit('Cannot create panels as the window manager/compositor does not support the necessary protocols')
|
||||
pos_x, pos_y = None, None
|
||||
if args.grab_keyboard:
|
||||
grab_keyboard(True)
|
||||
with cached_values_for(run_app.cached_values_name) as cached_values:
|
||||
if not _is_panel_kitten and not is_wayland():
|
||||
if opts.remember_window_position:
|
||||
cached_workarea = cached_values.get('monitor-workarea', ())
|
||||
cached_workarea = tuple(tuple(x) for x in cached_values.get('monitor-workarea', ()))
|
||||
if cached_workarea and glfw_get_monitor_workarea() == tuple(cached_workarea):
|
||||
pos_x, pos_y = cached_values.get('window-pos', (None, None))
|
||||
if args.position:
|
||||
|
||||
@@ -1040,7 +1040,9 @@ opt('linux_bell_theme', '__custom', ctype='!bell_theme',
|
||||
long_text='''
|
||||
The XDG Sound Theme kitty will use to play the bell sound.
|
||||
On Wayland, when the compositor supports it, it is asked to play the system default
|
||||
bell sound, and this setting has no effect. Otherwise, defaults to the custom theme name specified in the
|
||||
bell sound, and this setting has no effect. Note that Hyprland claims to support this
|
||||
protocol, but :link:`does not actually play a sound <https://github.com/hyprwm/Hyprland/discussions/10428>`.
|
||||
This setting defaults to the custom theme name specified in the
|
||||
:link:`XDG Sound theme specification <https://specifications.freedesktop.org/sound-theme-spec/latest/sound_lookup.html>,
|
||||
falling back to the default freedesktop theme if it does not exist.
|
||||
To change your sound theme desktop wide, create :file:`~/.local/share/sounds/__custom/index.theme` with the contents:
|
||||
|
||||
@@ -1031,7 +1031,7 @@ def underline_exclusion(x: str) -> tuple[float, Literal['', 'px', 'pt']]:
|
||||
try:
|
||||
val = float(x[:-2])
|
||||
except Exception:
|
||||
raise ValueError(f'Invalid underline_exclusion with non numberic value: {x}')
|
||||
raise ValueError(f'Invalid underline_exclusion with non numeric value: {x}')
|
||||
return val, unit
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class ScrollWindow(RemoteCommand):
|
||||
'Scroll the specified windows, if no window is specified, scroll the window this command is run inside.'
|
||||
' :italic:`SCROLL_AMOUNT` can be either the keywords :code:`start` or :code:`end` or an'
|
||||
' argument of the form :italic:`<number>[unit][+-]`. :code:`unit` can be :code:`l` for lines, :code:`p` for pages,'
|
||||
' :code:`u` for unscroll and :code:`r` for scroll to prompt. If unspecifed, :code:`l` is the default.'
|
||||
' :code:`u` for unscroll and :code:`r` for scroll to prompt. If unspecified, :code:`l` is the default.'
|
||||
' For example, :code:`30` will scroll down 30 lines, :code:`2p-`'
|
||||
' will scroll up 2 pages and :code:`0.5p` will scroll down half page.'
|
||||
' :code:`3u` will *unscroll* by 3 lines, which means that 3 lines will move from the'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
# This module must be runnable by a vanilla python interperter
|
||||
# This module must be runnable by a vanilla python interpreter
|
||||
# as it is used to generate C code when building kitty
|
||||
|
||||
import re
|
||||
@@ -323,6 +323,14 @@ def generate_c_parsers() -> Iterator[str]:
|
||||
|
||||
|
||||
# kitty CLI spec {{{
|
||||
grab_keyboard_docs = """\
|
||||
Grab the keyboard. This means global shortcuts defined in the OS will be passed to kitty instead. Useful if
|
||||
you want to create an OS modal window. How well this
|
||||
works depends on the OS/window manager/desktop environment. On Wayland it works only if the compositor implements
|
||||
the :link:`inhibit-keyboard-shortcuts protocol <https://wayland.app/protocols/keyboard-shortcuts-inhibit-unstable-v1>`.
|
||||
On macOS Apple doesn't allow applications to grab the keyboard without special permissions, so it doesn't work.
|
||||
"""
|
||||
|
||||
listen_on_defn = f'''\
|
||||
--listen-on
|
||||
completion=type:special group:complete_kitty_listen_on
|
||||
@@ -484,6 +492,11 @@ See also :opt:`remember_window_position` to have kitty automatically try
|
||||
to restore the previous window position.
|
||||
|
||||
|
||||
--grab-keyboard
|
||||
type=bool-set
|
||||
{grab_keyboard_docs}
|
||||
|
||||
|
||||
# Debugging options
|
||||
|
||||
--version -v
|
||||
@@ -539,8 +552,9 @@ type=bool-set
|
||||
'''
|
||||
setattr(kitty_options_spec, 'ans', OPTIONS.format(
|
||||
appname=appname, conf_name=appname, listen_on_defn=listen_on_defn,
|
||||
config_help=CONFIG_HELP.format(appname=appname, conf_name=appname),
|
||||
))
|
||||
grab_keyboard_docs=grab_keyboard_docs,
|
||||
config_help=CONFIG_HELP.format(appname=appname, conf_name=appname
|
||||
)))
|
||||
ans: str = getattr(kitty_options_spec, 'ans')
|
||||
return ans
|
||||
# }}}
|
||||
@@ -675,6 +689,11 @@ to :code:`on-demand`. Note that on Wayland, depending on the compositor, this ca
|
||||
becoming visible.
|
||||
|
||||
|
||||
--grab-keyboard
|
||||
type=bool-set
|
||||
{grab_keyboard_docs}
|
||||
|
||||
|
||||
--exclusive-zone
|
||||
type=int
|
||||
default={exclusive_zone}
|
||||
@@ -740,7 +759,7 @@ Path to a log file to store STDOUT/STDERR when using :option:`--detach`
|
||||
--debug-rendering
|
||||
type=bool-set
|
||||
For internal debugging use.
|
||||
'''.format(appname=appname, listen_on_defn=listen_on_defn, **d)
|
||||
'''.format(appname=appname, listen_on_defn=listen_on_defn, grab_keyboard_docs=grab_keyboard_docs, **d)
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/kovidgoyal/kitty/kittens/ask"
|
||||
"github.com/kovidgoyal/kitty/kittens/choose_fonts"
|
||||
"github.com/kovidgoyal/kitty/kittens/clipboard"
|
||||
"github.com/kovidgoyal/kitty/kittens/desktop_ui"
|
||||
"github.com/kovidgoyal/kitty/kittens/diff"
|
||||
"github.com/kovidgoyal/kitty/kittens/hints"
|
||||
"github.com/kovidgoyal/kitty/kittens/hyperlinked_grep"
|
||||
@@ -63,6 +64,8 @@ func KittyToolEntryPoints(root *cli.Command) {
|
||||
unicode_input.EntryPoint(root)
|
||||
// show_key
|
||||
show_key.EntryPoint(root)
|
||||
// desktop_ui
|
||||
desktop_ui.EntryPoint(root)
|
||||
// mouse_demo
|
||||
root.AddSubCommand(&cli.Command{
|
||||
Name: "mouse-demo",
|
||||
|
||||
Reference in New Issue
Block a user