mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-27 18:51:41 +02:00
Implement support for buttons on notifications in Linux
This commit is contained in:
2
glfw/glfw3.h
vendored
2
glfw/glfw3.h
vendored
@@ -1316,7 +1316,7 @@ typedef struct GLFWLayerShellConfig {
|
|||||||
} GLFWLayerShellConfig;
|
} GLFWLayerShellConfig;
|
||||||
|
|
||||||
typedef struct GLFWDBUSNotificationData {
|
typedef struct GLFWDBUSNotificationData {
|
||||||
const char *app_name, *icon, *summary, *body, *action_name;
|
const char *app_name, *icon, *summary, *body, **actions; size_t num_actions;
|
||||||
int32_t timeout; uint8_t urgency; uint32_t replaces;
|
int32_t timeout; uint8_t urgency; uint32_t replaces;
|
||||||
} GLFWDBUSNotificationData;
|
} GLFWDBUSNotificationData;
|
||||||
|
|
||||||
|
|||||||
8
glfw/linux_notify.c
vendored
8
glfw/linux_notify.c
vendored
@@ -124,10 +124,10 @@ glfw_dbus_send_user_notification(const GLFWDBUSNotificationData *n, GLFWDBusnoti
|
|||||||
APPEND(args, DBUS_TYPE_STRING, n->summary)
|
APPEND(args, DBUS_TYPE_STRING, n->summary)
|
||||||
APPEND(args, DBUS_TYPE_STRING, n->body)
|
APPEND(args, DBUS_TYPE_STRING, n->body)
|
||||||
check_call(dbus_message_iter_open_container, &args, DBUS_TYPE_ARRAY, "s", &array);
|
check_call(dbus_message_iter_open_container, &args, DBUS_TYPE_ARRAY, "s", &array);
|
||||||
if (n->action_name) {
|
if (n->actions) {
|
||||||
static const char* default_action = "default";
|
for (size_t i = 0; i < n->num_actions; i++) {
|
||||||
APPEND(array, DBUS_TYPE_STRING, default_action);
|
APPEND(array, DBUS_TYPE_STRING, n->actions[i]);
|
||||||
APPEND(array, DBUS_TYPE_STRING, n->action_name);
|
}
|
||||||
}
|
}
|
||||||
check_call(dbus_message_iter_close_container, &args, &array);
|
check_call(dbus_message_iter_close_container, &args, &array);
|
||||||
check_call(dbus_message_iter_open_container, &args, DBUS_TYPE_ARRAY, "{sv}", &array);
|
check_call(dbus_message_iter_open_container, &args, DBUS_TYPE_ARRAY, "{sv}", &array);
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ func (p *parsed_data) generate_chunks(callback func(string)) {
|
|||||||
if len(p.image_data) > 0 {
|
if len(p.image_data) > 0 {
|
||||||
add_payload("icon", utils.UnsafeBytesToString(p.image_data))
|
add_payload("icon", utils.UnsafeBytesToString(p.image_data))
|
||||||
}
|
}
|
||||||
|
if len(p.opts.Button) > 0 {
|
||||||
|
add_payload("buttons", strings.Join(p.opts.Button, "\u2028"))
|
||||||
|
}
|
||||||
write_chunk(";")
|
write_chunk(";")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +113,7 @@ func (p *parsed_data) run_loop() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
activated := ""
|
activated := -1
|
||||||
prefix := ESC_CODE_PREFIX + "i=" + p.identifier
|
prefix := ESC_CODE_PREFIX + "i=" + p.identifier
|
||||||
|
|
||||||
poll_for_close := func() {
|
poll_for_close := func() {
|
||||||
@@ -156,7 +159,9 @@ func (p *parsed_data) run_loop() (err error) {
|
|||||||
lp.Quit(0)
|
lp.Quit(0)
|
||||||
}
|
}
|
||||||
case "":
|
case "":
|
||||||
activated = utils.IfElse(payload == "", "activated", payload)
|
if activated, err = strconv.Atoi(utils.IfElse(payload == "", "0", payload)); err != nil {
|
||||||
|
return fmt.Errorf("Got invalid activation response from terminal: %#v", payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,7 +197,7 @@ func (p *parsed_data) run_loop() (err error) {
|
|||||||
lp.KillIfSignalled()
|
lp.KillIfSignalled()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if activated != "" && err == nil {
|
if activated > -1 && err == nil {
|
||||||
fmt.Println(activated)
|
fmt.Println(activated)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ default=kitten-notify
|
|||||||
The application name for the notification.
|
The application name for the notification.
|
||||||
|
|
||||||
|
|
||||||
|
--button -b
|
||||||
|
type=list
|
||||||
|
Add a button with the specified text to the notification. Can be specified multiple times for multiple buttons.
|
||||||
|
If --wait-for-completion is used then the kitten will print th ebutton number to STDOUT if the user clicks a button.
|
||||||
|
1 for the first button, 2 for the second button and so on.
|
||||||
|
|
||||||
|
|
||||||
--urgency -u
|
--urgency -u
|
||||||
default=normal
|
default=normal
|
||||||
choices=normal,low,critical
|
choices=normal,low,critical
|
||||||
@@ -62,7 +69,7 @@ your own identifier via the --identifier option.
|
|||||||
--wait-till-closed -w
|
--wait-till-closed -w
|
||||||
type=bool-set
|
type=bool-set
|
||||||
Wait until the notification is closed. If the user activates the notification,
|
Wait until the notification is closed. If the user activates the notification,
|
||||||
"activated" is printed to STDOUT before quitting. Press the Esc or Ctrl+C keys
|
"0" is printed to STDOUT before quitting. Press the Esc or Ctrl+C keys
|
||||||
to close the notification manually.
|
to close the notification manually.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -550,7 +550,7 @@ def dbus_send_notification(
|
|||||||
app_icon: str,
|
app_icon: str,
|
||||||
title: str,
|
title: str,
|
||||||
body: str,
|
body: str,
|
||||||
action_text: str = '',
|
actions: dict[str, str],
|
||||||
timeout: int = -1,
|
timeout: int = -1,
|
||||||
urgency: int = 1,
|
urgency: int = 1,
|
||||||
replaces: int = 0,
|
replaces: int = 0,
|
||||||
|
|||||||
2
kitty/glfw-wrapper.h
generated
2
kitty/glfw-wrapper.h
generated
@@ -1054,7 +1054,7 @@ typedef struct GLFWLayerShellConfig {
|
|||||||
} GLFWLayerShellConfig;
|
} GLFWLayerShellConfig;
|
||||||
|
|
||||||
typedef struct GLFWDBUSNotificationData {
|
typedef struct GLFWDBUSNotificationData {
|
||||||
const char *app_name, *icon, *summary, *body, *action_name;
|
const char *app_name, *icon, *summary, *body, **actions; size_t num_actions;
|
||||||
int32_t timeout; uint8_t urgency; uint32_t replaces;
|
int32_t timeout; uint8_t urgency; uint32_t replaces;
|
||||||
} GLFWDBUSNotificationData;
|
} GLFWDBUSNotificationData;
|
||||||
|
|
||||||
|
|||||||
20
kitty/glfw.c
20
kitty/glfw.c
@@ -2077,10 +2077,11 @@ dbus_notification_created_callback(unsigned long long notification_id, uint32_t
|
|||||||
static PyObject*
|
static PyObject*
|
||||||
dbus_send_notification(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
|
dbus_send_notification(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
|
||||||
int timeout = -1, urgency = 1; unsigned int replaces = 0;
|
int timeout = -1, urgency = 1; unsigned int replaces = 0;
|
||||||
GLFWDBUSNotificationData d = {.action_name=""};
|
GLFWDBUSNotificationData d = {0};
|
||||||
static const char* kwlist[] = {"app_name", "app_icon", "title", "body", "action_text", "timeout", "urgency", "replaces", NULL};
|
static const char* kwlist[] = {"app_name", "app_icon", "title", "body", "actions", "timeout", "urgency", "replaces", NULL};
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kw, "ssss|siiI", (char**)kwlist,
|
PyObject *actions = NULL;
|
||||||
&d.app_name, &d.icon, &d.summary, &d.body, &d.action_name, &timeout, &urgency, &replaces)) return NULL;
|
if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssO!|iiI", (char**)kwlist,
|
||||||
|
&d.app_name, &d.icon, &d.summary, &d.body, &PyDict_Type, &actions, &timeout, &urgency, &replaces)) return NULL;
|
||||||
if (!glfwDBusUserNotify) {
|
if (!glfwDBusUserNotify) {
|
||||||
PyErr_SetString(PyExc_RuntimeError, "Failed to load glfwDBusUserNotify, did you call glfw_init?");
|
PyErr_SetString(PyExc_RuntimeError, "Failed to load glfwDBusUserNotify, did you call glfw_init?");
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -2088,6 +2089,17 @@ dbus_send_notification(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
|
|||||||
d.timeout = timeout;
|
d.timeout = timeout;
|
||||||
d.urgency = urgency & 3;
|
d.urgency = urgency & 3;
|
||||||
d.replaces = replaces;
|
d.replaces = replaces;
|
||||||
|
RAII_ALLOC(const char*, aclist, calloc(2*PyDict_Size(actions), sizeof(d.actions[0])));
|
||||||
|
if (!aclist) { return PyErr_NoMemory(); }
|
||||||
|
PyObject *key, *value; Py_ssize_t pos = 0;
|
||||||
|
d.num_actions = 0;
|
||||||
|
while (PyDict_Next(actions, &pos, &key, &value)) {
|
||||||
|
if (!PyUnicode_Check(key) || !PyUnicode_Check(value)) { PyErr_SetString(PyExc_TypeError, "actions must be strings"); return NULL; }
|
||||||
|
if (PyUnicode_GET_LENGTH(key) == 0 || PyUnicode_GET_LENGTH(value) == 0) { PyErr_SetString(PyExc_TypeError, "actions must be non-empty strings"); return NULL; }
|
||||||
|
aclist[d.num_actions] = PyUnicode_AsUTF8(key); if (!aclist[d.num_actions++]) return NULL;
|
||||||
|
aclist[d.num_actions] = PyUnicode_AsUTF8(value); if (!aclist[d.num_actions++]) return NULL;
|
||||||
|
}
|
||||||
|
d.actions = aclist;
|
||||||
unsigned long long notification_id = glfwDBusUserNotify(&d, dbus_notification_created_callback, NULL);
|
unsigned long long notification_id = glfwDBusUserNotify(&d, dbus_notification_created_callback, NULL);
|
||||||
return PyLong_FromUnsignedLongLong(notification_id);
|
return PyLong_FromUnsignedLongLong(notification_id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,10 +131,11 @@ class PayloadType(Enum):
|
|||||||
close = 'close'
|
close = 'close'
|
||||||
icon = 'icon'
|
icon = 'icon'
|
||||||
alive = 'alive'
|
alive = 'alive'
|
||||||
|
buttons = 'buttons'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_text(self) -> bool:
|
def is_text(self) -> bool:
|
||||||
return self in (PayloadType.title, PayloadType.body)
|
return self in (PayloadType.title, PayloadType.body, PayloadType.buttons)
|
||||||
|
|
||||||
|
|
||||||
class OnlyWhen(Enum):
|
class OnlyWhen(Enum):
|
||||||
@@ -198,9 +199,9 @@ class EncodedDataStore:
|
|||||||
return self.data_store.finalise()
|
return self.data_store.finalise()
|
||||||
|
|
||||||
|
|
||||||
def limit_size(x: str) -> str:
|
def limit_size(x: str, limit: int = 1024) -> str:
|
||||||
if len(x) > 1024:
|
if len(x) > limit:
|
||||||
x = x[:1024]
|
x = x[:limit]
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
@@ -217,6 +218,7 @@ class NotificationCommand:
|
|||||||
application_name: str = ''
|
application_name: str = ''
|
||||||
notification_types: tuple[str, ...] = ()
|
notification_types: tuple[str, ...] = ()
|
||||||
timeout: int = -2
|
timeout: int = -2
|
||||||
|
buttons: tuple[str, ...] = ()
|
||||||
|
|
||||||
# event callbacks
|
# event callbacks
|
||||||
on_activation: Optional[Callable[['NotificationCommand'], None]] = None
|
on_activation: Optional[Callable[['NotificationCommand'], None]] = None
|
||||||
@@ -343,6 +345,8 @@ class NotificationCommand:
|
|||||||
self.application_name = prev.application_name
|
self.application_name = prev.application_name
|
||||||
if prev.notification_types:
|
if prev.notification_types:
|
||||||
self.notification_types = prev.notification_types + self.notification_types
|
self.notification_types = prev.notification_types + self.notification_types
|
||||||
|
if prev.buttons:
|
||||||
|
self.buttons += prev.buttons
|
||||||
if self.timeout < -1:
|
if self.timeout < -1:
|
||||||
self.timeout = prev.timeout
|
self.timeout = prev.timeout
|
||||||
self.icon_path = prev.icon_path
|
self.icon_path = prev.icon_path
|
||||||
@@ -386,6 +390,9 @@ class NotificationCommand:
|
|||||||
icd = self.icon_data_cache_ref()
|
icd = self.icon_data_cache_ref()
|
||||||
if icd:
|
if icd:
|
||||||
self.icon_path = icd.add_icon(self.icon_data_key, data)
|
self.icon_path = icd.add_icon(self.icon_data_key, data)
|
||||||
|
elif self.current_payload_type is PayloadType.buttons:
|
||||||
|
self.buttons += tuple(limit_size(x, 256) for x in text.split('\u2028') if x)
|
||||||
|
self.buttons = self.buttons[:8]
|
||||||
|
|
||||||
def finalise(self) -> None:
|
def finalise(self) -> None:
|
||||||
if self.current_payload_buffer:
|
if self.current_payload_buffer:
|
||||||
@@ -559,7 +566,7 @@ class MacOSIntegration(DesktopIntegration):
|
|||||||
from .fast_data_types import cocoa_live_delivered_notifications
|
from .fast_data_types import cocoa_live_delivered_notifications
|
||||||
cocoa_live_delivered_notifications() # so that we purge dead notifications
|
cocoa_live_delivered_notifications() # so that we purge dead notifications
|
||||||
elif event == "activated":
|
elif event == "activated":
|
||||||
self.notification_manager.notification_activated(desktop_notification_id)
|
self.notification_manager.notification_activated(desktop_notification_id, 0)
|
||||||
elif event == "creation_failed":
|
elif event == "creation_failed":
|
||||||
self.notification_manager.notification_closed(desktop_notification_id)
|
self.notification_manager.notification_closed(desktop_notification_id)
|
||||||
|
|
||||||
@@ -618,7 +625,8 @@ class FreeDesktopIntegration(DesktopIntegration):
|
|||||||
if event_type == 'activation_token':
|
if event_type == 'activation_token':
|
||||||
self.notification_manager.notification_activation_token_received(desktop_notification_id, str(extra))
|
self.notification_manager.notification_activation_token_received(desktop_notification_id, str(extra))
|
||||||
elif event_type == 'activated':
|
elif event_type == 'activated':
|
||||||
self.notification_manager.notification_activated(desktop_notification_id)
|
button = 0 if extra == 'default' else int(extra)
|
||||||
|
self.notification_manager.notification_activated(desktop_notification_id, button)
|
||||||
elif event_type == 'closed':
|
elif event_type == 'closed':
|
||||||
self.notification_manager.notification_closed(desktop_notification_id)
|
self.notification_manager.notification_closed(desktop_notification_id)
|
||||||
|
|
||||||
@@ -646,9 +654,12 @@ class FreeDesktopIntegration(DesktopIntegration):
|
|||||||
replaces_dbus_id = 0
|
replaces_dbus_id = 0
|
||||||
if existing_desktop_notification_id:
|
if existing_desktop_notification_id:
|
||||||
replaces_dbus_id = self.get_dbus_notification_id(existing_desktop_notification_id, 'notify') or 0
|
replaces_dbus_id = self.get_dbus_notification_id(existing_desktop_notification_id, 'notify') or 0
|
||||||
|
actions = {'default': ' '} # dbus requires string to not be empty
|
||||||
|
for i, b in enumerate(nc.buttons):
|
||||||
|
actions[str(i+1)] = b
|
||||||
desktop_notification_id = dbus_send_notification(
|
desktop_notification_id = dbus_send_notification(
|
||||||
app_name=nc.application_name or 'kitty', app_icon=app_icon, title=nc.title, body=body, timeout=nc.timeout,
|
app_name=nc.application_name or 'kitty', app_icon=app_icon, title=nc.title, body=body, actions=actions,
|
||||||
urgency=nc.urgency.value, replaces=replaces_dbus_id)
|
timeout=nc.timeout, urgency=nc.urgency.value, replaces=replaces_dbus_id)
|
||||||
if debug_desktop_integration:
|
if debug_desktop_integration:
|
||||||
log_error(f'Requested creation of notification with {desktop_notification_id=}')
|
log_error(f'Requested creation of notification with {desktop_notification_id=}')
|
||||||
if existing_desktop_notification_id and replaces_dbus_id:
|
if existing_desktop_notification_id and replaces_dbus_id:
|
||||||
@@ -761,7 +772,7 @@ class NotificationManager:
|
|||||||
if n := self.in_progress_notification_commands.get(desktop_notification_id):
|
if n := self.in_progress_notification_commands.get(desktop_notification_id):
|
||||||
n.activation_token = token
|
n.activation_token = token
|
||||||
|
|
||||||
def notification_activated(self, desktop_notification_id: int) -> None:
|
def notification_activated(self, desktop_notification_id: int, which: int) -> None:
|
||||||
if n := self.in_progress_notification_commands.get(desktop_notification_id):
|
if n := self.in_progress_notification_commands.get(desktop_notification_id):
|
||||||
if not n.close_response_requested:
|
if not n.close_response_requested:
|
||||||
self.purge_notification(n)
|
self.purge_notification(n)
|
||||||
@@ -769,7 +780,7 @@ class NotificationManager:
|
|||||||
self.channel.focus(n.channel_id, n.activation_token)
|
self.channel.focus(n.channel_id, n.activation_token)
|
||||||
if n.report_requested:
|
if n.report_requested:
|
||||||
ident = n.identifier or '0'
|
ident = n.identifier or '0'
|
||||||
self.channel.send(n.channel_id, f'99;i={ident};')
|
self.channel.send(n.channel_id, f'99;i={ident};{which or ''}')
|
||||||
if n.on_activation:
|
if n.on_activation:
|
||||||
try:
|
try:
|
||||||
n.on_activation(n)
|
n.on_activation(n)
|
||||||
|
|||||||
Reference in New Issue
Block a user