Automatically check for new releases and notify when an update is available, via the system notification facilities. Can be controlled by :opt:update_check_interval
Fixes #1342
This commit is contained in:
parent
3aaa69b36c
commit
d3ad15dc51
@ -11,6 +11,10 @@ Changelog
|
|||||||
- A new option :opt:`strip_trailing_spaces` to optionally remove trailing
|
- A new option :opt:`strip_trailing_spaces` to optionally remove trailing
|
||||||
spaces from lines when copying to clipboard.
|
spaces from lines when copying to clipboard.
|
||||||
|
|
||||||
|
- Automatically check for new releases and notify when an update is available,
|
||||||
|
via the system notification facilities. Can be controlled by
|
||||||
|
:opt:`update_check_interval` (:iss:`1342`)
|
||||||
|
|
||||||
- macOS: Fix :kbd:`cmd+period` key not working (:iss:`1318`)
|
- macOS: Fix :kbd:`cmd+period` key not working (:iss:`1318`)
|
||||||
|
|
||||||
- macOS: Add an option :opt:`macos_show_window_title_in_menubar` to not
|
- macOS: Add an option :opt:`macos_show_window_title_in_menubar` to not
|
||||||
|
|||||||
@ -16,7 +16,8 @@ from .conf.utils import to_cmdline
|
|||||||
from .config import initial_window_size_func, prepare_config_file_for_editing
|
from .config import initial_window_size_func, prepare_config_file_for_editing
|
||||||
from .config_data import MINIMUM_FONT_SIZE
|
from .config_data import MINIMUM_FONT_SIZE
|
||||||
from .constants import (
|
from .constants import (
|
||||||
appname, config_dir, kitty_exe, set_boss, supports_primary_selection
|
appname, config_dir, is_macos, kitty_exe, set_boss,
|
||||||
|
supports_primary_selection
|
||||||
)
|
)
|
||||||
from .fast_data_types import (
|
from .fast_data_types import (
|
||||||
ChildMonitor, background_opacity_of, change_background_opacity,
|
ChildMonitor, background_opacity_of, change_background_opacity,
|
||||||
@ -136,6 +137,9 @@ class Boss:
|
|||||||
self.toggle_fullscreen()
|
self.toggle_fullscreen()
|
||||||
else:
|
else:
|
||||||
change_os_window_state(args.start_as)
|
change_os_window_state(args.start_as)
|
||||||
|
if is_macos:
|
||||||
|
from .fast_data_types import cocoa_set_notification_activated_callback
|
||||||
|
cocoa_set_notification_activated_callback(self.notification_activated)
|
||||||
|
|
||||||
def add_os_window(self, startup_session, os_window_id=None, wclass=None, wname=None, opts_for_size=None, startup_id=None):
|
def add_os_window(self, startup_session, os_window_id=None, wclass=None, wname=None, opts_for_size=None, startup_id=None):
|
||||||
if os_window_id is None:
|
if os_window_id is None:
|
||||||
@ -355,6 +359,10 @@ class Boss:
|
|||||||
if not getattr(self, 'io_thread_started', False):
|
if not getattr(self, 'io_thread_started', False):
|
||||||
self.child_monitor.start()
|
self.child_monitor.start()
|
||||||
self.io_thread_started = True
|
self.io_thread_started = True
|
||||||
|
if self.opts.update_check_interval > 0 and not hasattr(self, 'update_check_started'):
|
||||||
|
from .update_check import run_update_check
|
||||||
|
run_update_check(self.opts.update_check_interval * 60 * 60)
|
||||||
|
self.update_check_started = True
|
||||||
|
|
||||||
def activate_tab_at(self, os_window_id, x):
|
def activate_tab_at(self, os_window_id, x):
|
||||||
tm = self.os_window_map.get(os_window_id)
|
tm = self.os_window_map.get(os_window_id)
|
||||||
@ -973,3 +981,18 @@ class Boss:
|
|||||||
os.remove(path)
|
os.remove(path)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def set_update_check_process(self, process):
|
||||||
|
self.update_check_process = process
|
||||||
|
|
||||||
|
def on_monitored_pid_death(self, pid, exit_status):
|
||||||
|
update_check_process = getattr(self, 'update_check_process', None)
|
||||||
|
if update_check_process is not None and pid == update_check_process.pid:
|
||||||
|
self.update_check_process = None
|
||||||
|
from .update_check import process_current_release
|
||||||
|
process_current_release(update_check_process.stdout.read().decode('utf-8'))
|
||||||
|
|
||||||
|
def notification_activated(self, identifier):
|
||||||
|
if identifier == 'new-version':
|
||||||
|
from .update_check import notification_activated
|
||||||
|
notification_activated()
|
||||||
|
|||||||
@ -116,6 +116,14 @@ get_dock_menu(id self UNUSED, SEL _cmd UNUSED, NSApplication *sender UNUSED) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static PyObject *notification_activated_callback = NULL;
|
static PyObject *notification_activated_callback = NULL;
|
||||||
|
static PyObject*
|
||||||
|
set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
|
||||||
|
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
|
||||||
|
notification_activated_callback = callback;
|
||||||
|
Py_INCREF(callback);
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@interface NotificationDelegate : NSObject <NSUserNotificationCenterDelegate>
|
@interface NotificationDelegate : NSObject <NSUserNotificationCenterDelegate>
|
||||||
@end
|
@end
|
||||||
@ -177,46 +185,6 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
|||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
call_timer_callback(PyObject *timer_callback) {
|
|
||||||
PyObject *ret = PyObject_CallObject(timer_callback, NULL);
|
|
||||||
if (ret == NULL) PyErr_Print();
|
|
||||||
else Py_DECREF(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject*
|
|
||||||
cocoa_run_notification_loop(PyObject *self UNUSED, PyObject *args) {
|
|
||||||
PyObject *timer_callback;
|
|
||||||
double timeout;
|
|
||||||
if (!PyArg_ParseTuple(args, "OOd", ¬ification_activated_callback, &timer_callback, &timeout)) return NULL;
|
|
||||||
NSAutoreleasePool *pool = [NSAutoreleasePool new];
|
|
||||||
NSApplication * application = [NSApplication sharedApplication];
|
|
||||||
// prevent icon in dock
|
|
||||||
[application setActivationPolicy:NSApplicationActivationPolicyAccessory];
|
|
||||||
signal(SIGTERM, SIG_IGN);
|
|
||||||
signal(SIGINT, SIG_IGN);
|
|
||||||
dispatch_source_t sigint = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGINT, 0, dispatch_get_main_queue());
|
|
||||||
dispatch_source_set_event_handler(sigint, ^{
|
|
||||||
[application terminate:nil];
|
|
||||||
});
|
|
||||||
dispatch_resume(sigint);
|
|
||||||
dispatch_source_t sigterm = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGINT, 0, dispatch_get_main_queue());
|
|
||||||
dispatch_source_set_event_handler(sigterm, ^{
|
|
||||||
[application terminate:nil];
|
|
||||||
});
|
|
||||||
dispatch_resume(sigterm);
|
|
||||||
// timer will fire after timeout, so fire it once at the start
|
|
||||||
call_timer_callback(timer_callback);
|
|
||||||
[NSTimer scheduledTimerWithTimeInterval:timeout
|
|
||||||
repeats:YES
|
|
||||||
block:^(NSTimer *timer UNUSED) {
|
|
||||||
call_timer_callback(timer_callback);
|
|
||||||
}];
|
|
||||||
[application run];
|
|
||||||
[pool drain];
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
cocoa_create_global_menu(void) {
|
cocoa_create_global_menu(void) {
|
||||||
NSString* app_name = find_app_name();
|
NSString* app_name = find_app_name();
|
||||||
@ -420,13 +388,15 @@ static void
|
|||||||
cleanup() {
|
cleanup() {
|
||||||
if (dockMenu) [dockMenu release];
|
if (dockMenu) [dockMenu release];
|
||||||
dockMenu = nil;
|
dockMenu = nil;
|
||||||
|
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
|
||||||
|
notification_activated_callback = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyMethodDef module_methods[] = {
|
static PyMethodDef module_methods[] = {
|
||||||
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
|
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
|
||||||
{"cocoa_set_new_window_trigger", (PyCFunction)cocoa_set_new_window_trigger, METH_VARARGS, ""},
|
{"cocoa_set_new_window_trigger", (PyCFunction)cocoa_set_new_window_trigger, METH_VARARGS, ""},
|
||||||
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
|
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
|
||||||
{"cocoa_run_notification_loop", (PyCFunction)cocoa_run_notification_loop, METH_VARARGS, ""},
|
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
|
||||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -362,8 +362,6 @@ Use negative numbers to change scroll direction.'''))
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
g('mouse') # {{{
|
g('mouse') # {{{
|
||||||
|
|
||||||
o('url_color', '#0087BD', option_type=to_color, long_text=_('''
|
o('url_color', '#0087BD', option_type=to_color, long_text=_('''
|
||||||
@ -735,6 +733,12 @@ environment variables are expanded recursively, so if you use::
|
|||||||
The value of MYVAR2 will be :code:`a/<path to home directory>/b`.
|
The value of MYVAR2 will be :code:`a/<path to home directory>/b`.
|
||||||
'''))
|
'''))
|
||||||
|
|
||||||
|
o('update_check_interval', 24, option_type=float, long_text=_('''
|
||||||
|
Periodically check if an update to kitty is available. If an update is found
|
||||||
|
a system notification is displayed informing you of the available update.
|
||||||
|
The default is to check every 24 hrs, set to zero to disable.
|
||||||
|
'''))
|
||||||
|
|
||||||
|
|
||||||
def startup_session(x):
|
def startup_session(x):
|
||||||
if x.lower() == 'none':
|
if x.lower() == 'none':
|
||||||
|
|||||||
102
kitty/notify.py
102
kitty/notify.py
@ -2,89 +2,49 @@
|
|||||||
# vim:fileencoding=utf-8
|
# vim:fileencoding=utf-8
|
||||||
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
|
||||||
from urllib.request import urlopen
|
|
||||||
|
|
||||||
from .constants import cache_dir, is_macos, logo_png_file, version
|
|
||||||
from .utils import open_url
|
|
||||||
|
|
||||||
CHANGELOG_URL = 'https://sw.kovidgoyal.net/kitty/changelog.html'
|
|
||||||
RELEASED_VERSION_URL = 'https://sw.kovidgoyal.net/kitty/current-version.txt'
|
|
||||||
CHECK_INTERVAL = 24 * 60 * 60
|
|
||||||
|
|
||||||
|
|
||||||
def version_notification_log():
|
|
||||||
return os.path.join(cache_dir(), 'new-version-notifications.txt')
|
|
||||||
|
|
||||||
|
|
||||||
def notify_new_version(version):
|
|
||||||
notify('kitty update available!', 'kitty version {} released'.format('.'.join(map(str, version))))
|
|
||||||
|
|
||||||
|
|
||||||
def get_released_version():
|
|
||||||
try:
|
|
||||||
raw = urlopen(RELEASED_VERSION_URL).read().decode('utf-8').strip()
|
|
||||||
except Exception:
|
|
||||||
raw = '0.0.0'
|
|
||||||
return tuple(map(int, raw.split('.')))
|
|
||||||
|
|
||||||
|
|
||||||
notified_versions = set()
|
|
||||||
|
|
||||||
|
|
||||||
def save_notification(version):
|
|
||||||
notified_versions.add(version)
|
|
||||||
version = '.'.join(map(str, version))
|
|
||||||
with open(version_notification_log(), 'a') as f:
|
|
||||||
print(version, file=f)
|
|
||||||
|
|
||||||
|
|
||||||
def already_notified(version):
|
|
||||||
if not hasattr(already_notified, 'read_cache'):
|
|
||||||
already_notified.read_cache = True
|
|
||||||
with open(version_notification_log()) as f:
|
|
||||||
for line in f:
|
|
||||||
notified_versions.add(tuple(map(int, line.strip().split('.'))))
|
|
||||||
return tuple(version) in notified_versions
|
|
||||||
|
|
||||||
|
from .constants import is_macos, logo_png_file
|
||||||
|
|
||||||
if is_macos:
|
if is_macos:
|
||||||
from .fast_data_types import cocoa_send_notification, cocoa_run_notification_loop
|
from .fast_data_types import cocoa_send_notification
|
||||||
|
|
||||||
def notify(title, body, timeout=5000, application='kitty', icon=True, identifier=None):
|
def notify(
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
timeout=5000,
|
||||||
|
application='kitty',
|
||||||
|
icon=True,
|
||||||
|
identifier=None
|
||||||
|
):
|
||||||
if icon is True:
|
if icon is True:
|
||||||
icon = None
|
icon = None
|
||||||
cocoa_send_notification(identifier, title, body, icon)
|
cocoa_send_notification(identifier, title, body, icon)
|
||||||
|
|
||||||
def notification_activated(notification_identifier):
|
|
||||||
open_url(CHANGELOG_URL)
|
|
||||||
|
|
||||||
def do_check():
|
|
||||||
new_version = get_released_version()
|
|
||||||
if new_version > version and not already_notified(new_version):
|
|
||||||
save_notification(new_version)
|
|
||||||
notify_new_version(new_version)
|
|
||||||
|
|
||||||
def update_check():
|
|
||||||
cocoa_run_notification_loop(notification_activated, do_check, CHECK_INTERVAL)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
def notify(title, body, timeout=5000, application='kitty', icon=True, identifier=None):
|
|
||||||
cmd = ['notify-send', '-t', str(timeout), '-a', application]
|
# libnotify depends on GTK, so we are not using it, instead
|
||||||
|
# use the command line notify-send wrapper it provides
|
||||||
|
# May want to just implement this in glfw using DBUS
|
||||||
|
|
||||||
|
def notify(
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
timeout=-1,
|
||||||
|
application='kitty',
|
||||||
|
icon=True,
|
||||||
|
identifier=None
|
||||||
|
):
|
||||||
|
cmd = ['notify-send', '-a', application]
|
||||||
|
if timeout > -1:
|
||||||
|
cmd.append('-t'), cmd.append(str(timeout))
|
||||||
if icon is True:
|
if icon is True:
|
||||||
icon = logo_png_file
|
icon = logo_png_file
|
||||||
if icon:
|
if icon:
|
||||||
cmd.extend(['-i', icon])
|
cmd.extend(['-i', icon])
|
||||||
subprocess.Popen(
|
subprocess.Popen(
|
||||||
cmd + [title, body], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
|
cmd + [title, body],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
def update_check():
|
stderr=subprocess.DEVNULL,
|
||||||
while True:
|
stdin=subprocess.DEVNULL,
|
||||||
new_version = get_released_version()
|
)
|
||||||
if new_version > version and not already_notified(new_version):
|
|
||||||
save_notification(new_version)
|
|
||||||
notify_new_version(new_version)
|
|
||||||
time.sleep(CHECK_INTERVAL)
|
|
||||||
|
|||||||
109
kitty/update_check.py
Normal file
109
kitty/update_check.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from collections import namedtuple
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
from .config import atomic_save
|
||||||
|
from .constants import cache_dir, get_boss, version
|
||||||
|
from .fast_data_types import add_timer, monitor_pid
|
||||||
|
from .notify import notify
|
||||||
|
from .utils import open_url
|
||||||
|
|
||||||
|
CHANGELOG_URL = 'https://sw.kovidgoyal.net/kitty/changelog.html'
|
||||||
|
RELEASED_VERSION_URL = 'https://sw.kovidgoyal.net/kitty/current-version.txt'
|
||||||
|
CHECK_INTERVAL = 24 * 60 * 60
|
||||||
|
Notification = namedtuple('Notification', 'version time_of_last_notification count')
|
||||||
|
|
||||||
|
|
||||||
|
def notification_activated():
|
||||||
|
open_url(CHANGELOG_URL)
|
||||||
|
|
||||||
|
|
||||||
|
def version_notification_log():
|
||||||
|
override = getattr(version_notification_log, 'override', None)
|
||||||
|
if override:
|
||||||
|
return override
|
||||||
|
return os.path.join(cache_dir(), 'new-version-notifications-1.txt')
|
||||||
|
|
||||||
|
|
||||||
|
def notify_new_version(release_version):
|
||||||
|
notify(
|
||||||
|
'kitty update available!',
|
||||||
|
'kitty version {} released'.format('.'.join(map(str, release_version))),
|
||||||
|
identifier='new-version',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_released_version():
|
||||||
|
try:
|
||||||
|
raw = urlopen(RELEASED_VERSION_URL).read().decode('utf-8').strip()
|
||||||
|
except Exception:
|
||||||
|
raw = '0.0.0'
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def parse_line(line):
|
||||||
|
parts = line.split(',')
|
||||||
|
version, timestamp, count = parts
|
||||||
|
version = tuple(map(int, version.split('.')))
|
||||||
|
return Notification(version, float(timestamp), int(count))
|
||||||
|
|
||||||
|
|
||||||
|
def read_cache():
|
||||||
|
notified_versions = {}
|
||||||
|
try:
|
||||||
|
with open(version_notification_log()) as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
n = parse_line(line)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
notified_versions[n.version] = n
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
return notified_versions
|
||||||
|
|
||||||
|
|
||||||
|
def already_notified(version):
|
||||||
|
notified_versions = read_cache()
|
||||||
|
return version in notified_versions
|
||||||
|
|
||||||
|
|
||||||
|
def save_notification(version):
|
||||||
|
notified_versions = read_cache()
|
||||||
|
if version in notified_versions:
|
||||||
|
v = notified_versions[version]
|
||||||
|
notified_versions[version] = v._replace(time_of_last_notification=time.time(), count=v.count + 1)
|
||||||
|
else:
|
||||||
|
notified_versions[version] = Notification(version, time.time(), 1)
|
||||||
|
lines = []
|
||||||
|
for version in sorted(notified_versions):
|
||||||
|
n = notified_versions[version]
|
||||||
|
lines.append('{},{},{}'.format(
|
||||||
|
'.'.join(map(str, n.version)), n.time_of_last_notification, n.count))
|
||||||
|
atomic_save('\n'.join(lines).encode('utf-8'), version_notification_log())
|
||||||
|
|
||||||
|
|
||||||
|
def process_current_release(raw):
|
||||||
|
release_version = tuple(map(int, raw.split('.')))
|
||||||
|
if release_version > version and not already_notified(release_version):
|
||||||
|
save_notification(release_version)
|
||||||
|
notify_new_version(release_version)
|
||||||
|
|
||||||
|
|
||||||
|
def update_check(timer_id=None):
|
||||||
|
p = subprocess.Popen([
|
||||||
|
'kitty', '+runpy', 'from kitty.update_check import *; import time; time.sleep(2); print(get_released_version())'
|
||||||
|
], stdout=subprocess.PIPE)
|
||||||
|
monitor_pid(p.pid)
|
||||||
|
get_boss().set_update_check_process(p)
|
||||||
|
|
||||||
|
|
||||||
|
def run_update_check(interval=24 * 60 * 60):
|
||||||
|
update_check()
|
||||||
|
add_timer('update_check', update_check, interval)
|
||||||
Loading…
x
Reference in New Issue
Block a user