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
|
||||
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: 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_data import MINIMUM_FONT_SIZE
|
||||
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 (
|
||||
ChildMonitor, background_opacity_of, change_background_opacity,
|
||||
@ -136,6 +137,9 @@ class Boss:
|
||||
self.toggle_fullscreen()
|
||||
else:
|
||||
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):
|
||||
if os_window_id is None:
|
||||
@ -355,6 +359,10 @@ class Boss:
|
||||
if not getattr(self, 'io_thread_started', False):
|
||||
self.child_monitor.start()
|
||||
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):
|
||||
tm = self.os_window_map.get(os_window_id)
|
||||
@ -973,3 +981,18 @@ class Boss:
|
||||
os.remove(path)
|
||||
except FileNotFoundError:
|
||||
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*
|
||||
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>
|
||||
@end
|
||||
@ -177,46 +185,6 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
|
||||
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
|
||||
cocoa_create_global_menu(void) {
|
||||
NSString* app_name = find_app_name();
|
||||
@ -420,13 +388,15 @@ static void
|
||||
cleanup() {
|
||||
if (dockMenu) [dockMenu release];
|
||||
dockMenu = nil;
|
||||
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
|
||||
notification_activated_callback = NULL;
|
||||
}
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
|
||||
{"cocoa_set_new_window_trigger", (PyCFunction)cocoa_set_new_window_trigger, 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 */
|
||||
};
|
||||
|
||||
|
||||
@ -362,8 +362,6 @@ Use negative numbers to change scroll direction.'''))
|
||||
|
||||
# }}}
|
||||
|
||||
# }}}
|
||||
|
||||
g('mouse') # {{{
|
||||
|
||||
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`.
|
||||
'''))
|
||||
|
||||
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):
|
||||
if x.lower() == 'none':
|
||||
|
||||
102
kitty/notify.py
102
kitty/notify.py
@ -2,89 +2,49 @@
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import os
|
||||
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:
|
||||
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:
|
||||
icon = None
|
||||
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:
|
||||
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:
|
||||
icon = logo_png_file
|
||||
if icon:
|
||||
cmd.extend(['-i', icon])
|
||||
subprocess.Popen(
|
||||
cmd + [title, body], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
|
||||
|
||||
def update_check():
|
||||
while True:
|
||||
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)
|
||||
cmd + [title, body],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
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