mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 08:26:56 +00:00
Implement support for buttons on notifications in Linux
This commit is contained in:
parent
ad36c481af
commit
aa16918dd4
8 changed files with 60 additions and 25 deletions
2
glfw/glfw3.h
vendored
2
glfw/glfw3.h
vendored
|
|
@ -1316,7 +1316,7 @@ typedef struct GLFWLayerShellConfig {
|
|||
} GLFWLayerShellConfig;
|
||||
|
||||
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;
|
||||
} 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->body)
|
||||
check_call(dbus_message_iter_open_container, &args, DBUS_TYPE_ARRAY, "s", &array);
|
||||
if (n->action_name) {
|
||||
static const char* default_action = "default";
|
||||
APPEND(array, DBUS_TYPE_STRING, default_action);
|
||||
APPEND(array, DBUS_TYPE_STRING, n->action_name);
|
||||
if (n->actions) {
|
||||
for (size_t i = 0; i < n->num_actions; i++) {
|
||||
APPEND(array, DBUS_TYPE_STRING, n->actions[i]);
|
||||
}
|
||||
}
|
||||
check_call(dbus_message_iter_close_container, &args, &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 {
|
||||
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(";")
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +113,7 @@ func (p *parsed_data) run_loop() (err error) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activated := ""
|
||||
activated := -1
|
||||
prefix := ESC_CODE_PREFIX + "i=" + p.identifier
|
||||
|
||||
poll_for_close := func() {
|
||||
|
|
@ -156,7 +159,9 @@ func (p *parsed_data) run_loop() (err error) {
|
|||
lp.Quit(0)
|
||||
}
|
||||
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()
|
||||
return
|
||||
}
|
||||
if activated != "" && err == nil {
|
||||
if activated > -1 && err == nil {
|
||||
fmt.Println(activated)
|
||||
}
|
||||
return
|
||||
|
|
|
|||
|
|
@ -26,6 +26,13 @@ default=kitten-notify
|
|||
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
|
||||
default=normal
|
||||
choices=normal,low,critical
|
||||
|
|
@ -62,7 +69,7 @@ your own identifier via the --identifier option.
|
|||
--wait-till-closed -w
|
||||
type=bool-set
|
||||
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.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -550,7 +550,7 @@ def dbus_send_notification(
|
|||
app_icon: str,
|
||||
title: str,
|
||||
body: str,
|
||||
action_text: str = '',
|
||||
actions: dict[str, str],
|
||||
timeout: int = -1,
|
||||
urgency: int = 1,
|
||||
replaces: int = 0,
|
||||
|
|
|
|||
2
kitty/glfw-wrapper.h
generated
2
kitty/glfw-wrapper.h
generated
|
|
@ -1054,7 +1054,7 @@ typedef struct GLFWLayerShellConfig {
|
|||
} GLFWLayerShellConfig;
|
||||
|
||||
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;
|
||||
} 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*
|
||||
dbus_send_notification(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
|
||||
int timeout = -1, urgency = 1; unsigned int replaces = 0;
|
||||
GLFWDBUSNotificationData d = {.action_name=""};
|
||||
static const char* kwlist[] = {"app_name", "app_icon", "title", "body", "action_text", "timeout", "urgency", "replaces", NULL};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kw, "ssss|siiI", (char**)kwlist,
|
||||
&d.app_name, &d.icon, &d.summary, &d.body, &d.action_name, &timeout, &urgency, &replaces)) return NULL;
|
||||
GLFWDBUSNotificationData d = {0};
|
||||
static const char* kwlist[] = {"app_name", "app_icon", "title", "body", "actions", "timeout", "urgency", "replaces", NULL};
|
||||
PyObject *actions = 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) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to load glfwDBusUserNotify, did you call glfw_init?");
|
||||
return NULL;
|
||||
|
|
@ -2088,6 +2089,17 @@ dbus_send_notification(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
|
|||
d.timeout = timeout;
|
||||
d.urgency = urgency & 3;
|
||||
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);
|
||||
return PyLong_FromUnsignedLongLong(notification_id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,10 +131,11 @@ class PayloadType(Enum):
|
|||
close = 'close'
|
||||
icon = 'icon'
|
||||
alive = 'alive'
|
||||
buttons = 'buttons'
|
||||
|
||||
@property
|
||||
def is_text(self) -> bool:
|
||||
return self in (PayloadType.title, PayloadType.body)
|
||||
return self in (PayloadType.title, PayloadType.body, PayloadType.buttons)
|
||||
|
||||
|
||||
class OnlyWhen(Enum):
|
||||
|
|
@ -198,9 +199,9 @@ class EncodedDataStore:
|
|||
return self.data_store.finalise()
|
||||
|
||||
|
||||
def limit_size(x: str) -> str:
|
||||
if len(x) > 1024:
|
||||
x = x[:1024]
|
||||
def limit_size(x: str, limit: int = 1024) -> str:
|
||||
if len(x) > limit:
|
||||
x = x[:limit]
|
||||
return x
|
||||
|
||||
|
||||
|
|
@ -217,6 +218,7 @@ class NotificationCommand:
|
|||
application_name: str = ''
|
||||
notification_types: tuple[str, ...] = ()
|
||||
timeout: int = -2
|
||||
buttons: tuple[str, ...] = ()
|
||||
|
||||
# event callbacks
|
||||
on_activation: Optional[Callable[['NotificationCommand'], None]] = None
|
||||
|
|
@ -343,6 +345,8 @@ class NotificationCommand:
|
|||
self.application_name = prev.application_name
|
||||
if prev.notification_types:
|
||||
self.notification_types = prev.notification_types + self.notification_types
|
||||
if prev.buttons:
|
||||
self.buttons += prev.buttons
|
||||
if self.timeout < -1:
|
||||
self.timeout = prev.timeout
|
||||
self.icon_path = prev.icon_path
|
||||
|
|
@ -386,6 +390,9 @@ class NotificationCommand:
|
|||
icd = self.icon_data_cache_ref()
|
||||
if icd:
|
||||
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:
|
||||
if self.current_payload_buffer:
|
||||
|
|
@ -559,7 +566,7 @@ class MacOSIntegration(DesktopIntegration):
|
|||
from .fast_data_types import cocoa_live_delivered_notifications
|
||||
cocoa_live_delivered_notifications() # so that we purge dead notifications
|
||||
elif event == "activated":
|
||||
self.notification_manager.notification_activated(desktop_notification_id)
|
||||
self.notification_manager.notification_activated(desktop_notification_id, 0)
|
||||
elif event == "creation_failed":
|
||||
self.notification_manager.notification_closed(desktop_notification_id)
|
||||
|
||||
|
|
@ -618,7 +625,8 @@ class FreeDesktopIntegration(DesktopIntegration):
|
|||
if event_type == 'activation_token':
|
||||
self.notification_manager.notification_activation_token_received(desktop_notification_id, str(extra))
|
||||
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':
|
||||
self.notification_manager.notification_closed(desktop_notification_id)
|
||||
|
||||
|
|
@ -646,9 +654,12 @@ class FreeDesktopIntegration(DesktopIntegration):
|
|||
replaces_dbus_id = 0
|
||||
if existing_desktop_notification_id:
|
||||
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(
|
||||
app_name=nc.application_name or 'kitty', app_icon=app_icon, title=nc.title, body=body, timeout=nc.timeout,
|
||||
urgency=nc.urgency.value, replaces=replaces_dbus_id)
|
||||
app_name=nc.application_name or 'kitty', app_icon=app_icon, title=nc.title, body=body, actions=actions,
|
||||
timeout=nc.timeout, urgency=nc.urgency.value, replaces=replaces_dbus_id)
|
||||
if debug_desktop_integration:
|
||||
log_error(f'Requested creation of notification with {desktop_notification_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):
|
||||
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 not n.close_response_requested:
|
||||
self.purge_notification(n)
|
||||
|
|
@ -769,7 +780,7 @@ class NotificationManager:
|
|||
self.channel.focus(n.channel_id, n.activation_token)
|
||||
if n.report_requested:
|
||||
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:
|
||||
try:
|
||||
n.on_activation(n)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue