Compare commits

..

37 Commits

Author SHA1 Message Date
Kovid Goyal
2d15380f9d ... 2025-05-19 20:14:27 +05:30
Kovid Goyal
3119088a54 Allow setting contrast and accent-color as well 2025-05-19 19:39:32 +05:30
Kovid Goyal
fcc77c264f Endpoint to set arbitrary settings 2025-05-19 19:07:37 +05:30
Kovid Goyal
d38d72bd5e Command to change color-scheme 2025-05-19 13:42:24 +05:30
Kovid Goyal
c36815a380 Command to auto-create config files needed to get xdg-desktop-portal to use the kitty portal 2025-05-19 13:10:32 +05:30
Kovid Goyal
6be57e4316 Couple more options 2025-05-19 10:21:53 +05:30
Kovid Goyal
f591ca151d Code to show desktop settings 2025-05-19 10:12:00 +05:30
Kovid Goyal
9c1acce02a Build against a local copy of dbus 2025-05-19 08:52:18 +05:30
Kovid Goyal
858e05186c Run goreplace filter on commit 2025-05-19 08:52:18 +05:30
Kovid Goyal
f104562533 Add API to change settings values over DBUS 2025-05-19 08:52:18 +05:30
Kovid Goyal
a3398a44f8 Allow arbitrary number of settings 2025-05-19 08:52:18 +05:30
Kovid Goyal
92fb47ae3c bump dbus version 2025-05-19 08:52:18 +05:30
Kovid Goyal
01c182c410 Only add properties interface if there are properties defined 2025-05-19 08:52:18 +05:30
Kovid Goyal
6fded182b3 Add SettingChanged to introspect data 2025-05-19 08:52:18 +05:30
Kovid Goyal
a8082f7a3c ... 2025-05-19 08:52:18 +05:30
Kovid Goyal
75fdd86637 Get the DBUS export actually working 2025-05-19 08:52:18 +05:30
Kovid Goyal
e42d7efe85 Get it building on the various BSDs bane of my existence 2025-05-19 08:52:18 +05:30
Kovid Goyal
13b574486c ... 2025-05-19 08:52:18 +05:30
Kovid Goyal
c94844b220 Start work on Linux desktop portal kitten 2025-05-19 08:52:18 +05:30
Kovid Goyal
31d7dc43b0 Merge branch 'dependabot/go_modules/all-go-deps-66fe8ab3e4' of https://github.com/kovidgoyal/kitty 2025-05-19 08:52:01 +05:30
dependabot[bot]
f46fc096c8 Bump github.com/alecthomas/chroma/v2 in the all-go-deps group
Bumps the all-go-deps group with 1 update: [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma).


Updates `github.com/alecthomas/chroma/v2` from 2.17.2 to 2.18.0
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Changelog](https://github.com/alecthomas/chroma/blob/master/.goreleaser.yml)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.17.2...v2.18.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-go-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 03:14:49 +00:00
Kovid Goyal
e31eda7735 Add a note that xdg-system-bell is terminally broken on Hyprland 2025-05-19 07:03:56 +05:30
Kovid Goyal
5b05b1235e Fix #8648 2025-05-19 06:22:45 +05:30
Kovid Goyal
da6f85dbaa Merge branch 'update-wayland-compat-matrix' of https://github.com/alex-huff/kitty 2025-05-19 06:17:21 +05:30
alex-huff
9d9ad91f37 docs: panel: update niri compatibility information 2025-05-18 18:18:43 -05:00
Stefan A. Haubenthal
3b20936959 Fixed typos 2025-05-18 19:52:12 +05:30
Stefan A. Haubenthal
33f278b477 Fixed typos 2025-05-18 15:16:56 +02:00
Kovid Goyal
255dd2845e Implement grab keyboard for x11 2025-05-18 14:49:07 +05:30
Kovid Goyal
d8b0edce43 Add a note that grabbing keyboard doesnt work on macOS 2025-05-18 14:34:10 +05:30
Kovid Goyal
95c6279bdd Implement --grab-keyboard
Currently works on Wayland.
2025-05-18 14:30:33 +05:30
Kovid Goyal
cc4d4eeaca Fix shortcuts from global menubar not working when keyboard grabbed on macOS 2025-05-18 14:23:38 +05:30
Kovid Goyal
abc9b1fc48 ... 2025-05-18 12:06:10 +05:30
Kovid Goyal
95f5e9293e DRYer 2025-05-18 11:47:22 +05:30
Kovid Goyal
82523b14df Wayland: Dont loose keyboard grab when new OS window created 2025-05-18 11:43:41 +05:30
Kovid Goyal
6f689f3221 Work on keyboard grabbing functionality 2025-05-18 11:37:11 +05:30
Kovid Goyal
e687d6db05 Link to my bar's code since it is now public 2025-05-18 09:56:20 +05:30
Kovid Goyal
13c37cf694 Fix remember_window_position not working because of a stupid typo
Fixes #8646
2025-05-18 07:53:30 +05:30
43 changed files with 977 additions and 44 deletions

1
.gitattributes vendored
View File

@@ -28,3 +28,4 @@ terminfo/x/* linguist-generated=true
*.py text diff=python
*.m text diff=objc
*.go text diff=go
*.mod filter=goreplace

View File

@@ -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]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -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.

View File

@@ -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:

View File

@@ -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.

View File

@@ -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

View File

@@ -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 ...

View File

@@ -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::

View File

@@ -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

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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);

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

192
kittens/desktop_ui/main.go Normal file
View 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",
})
}

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>

View 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
}

View File

@@ -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')

View File

@@ -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`)
}

View File

@@ -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.')

View File

@@ -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)

View File

@@ -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

View File

@@ -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(

View File

@@ -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
View File

@@ -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
View File

@@ -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

View File

@@ -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),

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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'

View File

@@ -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)
# }}}

View File

@@ -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",