mirror of
https://github.com/morgan9e/gnome-screentime
synced 2026-04-14 00:04:16 +09:00
Add files via upload
This commit is contained in:
46
Makefile
Normal file
46
Makefile
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
PREFIX ?= $(HOME)/.local
|
||||||
|
BINDIR = $(PREFIX)/bin
|
||||||
|
EXTENSIONDIR = $(HOME)/.local/share/gnome-shell/extensions/
|
||||||
|
SERVICEDIR = $(HOME)/.config/systemd/user
|
||||||
|
DESKTOPDIR = $(HOME)/.local/share/applications
|
||||||
|
|
||||||
|
EXTENTION = "app-tracker@local"
|
||||||
|
|
||||||
|
.PHONY: all install install-extension install-daemon install-viewer uninstall
|
||||||
|
|
||||||
|
all:
|
||||||
|
@echo "usage: make install"
|
||||||
|
@echo " make uninstall"
|
||||||
|
@echo " make enable"
|
||||||
|
|
||||||
|
install: install-extension install-daemon install-viewer
|
||||||
|
|
||||||
|
install-extension:
|
||||||
|
mkdir -p $(EXTENSIONDIR)
|
||||||
|
cp $(EXTENSION)/extension.js $(EXTENSIONDIR)/$(EXTENSION)
|
||||||
|
cp $(EXTENSION)/metadata.json $(EXTENSIONDIR)/$(EXTENSION)
|
||||||
|
|
||||||
|
install-daemon:
|
||||||
|
mkdir -p $(BINDIR)
|
||||||
|
cp screentimed $(BINDIR)/screentimed
|
||||||
|
chmod +x $(BINDIR)/screentimed
|
||||||
|
mkdir -p $(SERVICEDIR)
|
||||||
|
cp screentimed.service $(SERVICEDIR)/
|
||||||
|
|
||||||
|
install-viewer:
|
||||||
|
mkdir -p $(BINDIR)
|
||||||
|
cp screentime-viewer $(BINDIR)/screentime-viewer
|
||||||
|
chmod +x $(BINDIR)/screentime-viewer
|
||||||
|
mkdir -p $(DESKTOPDIR)
|
||||||
|
cp screentime-viewer.desktop $(DESKTOPDIR)/
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
systemctl --user disable --now screentimed || true
|
||||||
|
gnome-extensions disable $(EXTENSION) || true
|
||||||
|
rm -v $(BINDIR)/screentimed
|
||||||
|
rm -v $(BINDIR)/screentime-viewer
|
||||||
|
rm -v $(SERVICEDIR)/screentimed.service
|
||||||
|
rm -v $(DESKTOPDIR)/screentime-viewer.desktop
|
||||||
|
rm -rv $(EXTENSIONDIR)/$(EXTENSION)
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
@echo "uninstalled."
|
||||||
83
app-tracker@local/extension.js
Normal file
83
app-tracker@local/extension.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import GLib from 'gi://GLib';
|
||||||
|
import Gio from 'gi://Gio';
|
||||||
|
import Shell from 'gi://Shell';
|
||||||
|
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||||
|
|
||||||
|
const IFACE = `<node>
|
||||||
|
<interface name="org.gnome.Shell.UsageTracker">
|
||||||
|
<signal name="FocusChanged">
|
||||||
|
<arg name="app_id" type="s"/>
|
||||||
|
</signal>
|
||||||
|
<signal name="RunningAppsChanged">
|
||||||
|
<arg name="app_ids" type="as"/>
|
||||||
|
</signal>
|
||||||
|
<method name="GetFocus">
|
||||||
|
<arg direction="out" name="app_id" type="s"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetRunningApps">
|
||||||
|
<arg direction="out" name="app_ids" type="as"/>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
</node>`;
|
||||||
|
|
||||||
|
export default class FocusTracker extends Extension {
|
||||||
|
enable() {
|
||||||
|
this._currentApp = '';
|
||||||
|
this._tracker = Shell.WindowTracker.get_default();
|
||||||
|
this._appSystem = Shell.AppSystem.get_default();
|
||||||
|
|
||||||
|
this._dbus = Gio.DBusExportedObject.wrapJSObject(IFACE, this);
|
||||||
|
this._dbus.export(Gio.DBus.session, '/org/gnome/Shell/UsageTracker');
|
||||||
|
|
||||||
|
this._focusSig = this._tracker.connect('notify::focus-app', () => {
|
||||||
|
const id = this._resolveFocusId();
|
||||||
|
if (id !== this._currentApp) {
|
||||||
|
this._currentApp = id;
|
||||||
|
this._dbus.emit_signal('FocusChanged',
|
||||||
|
new GLib.Variant('(s)', [id]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._appSig = this._appSystem.connect('app-state-changed', () => {
|
||||||
|
const ids = this._appSystem.get_running().map(a => this._resolveAppId(a));
|
||||||
|
this._dbus.emit_signal('RunningAppsChanged',
|
||||||
|
new GLib.Variant('(as)', [ids]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_resolveFocusId() {
|
||||||
|
const app = this._tracker.focus_app;
|
||||||
|
if (!app)
|
||||||
|
return '';
|
||||||
|
return this._resolveAppId(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
_resolveAppId(app) {
|
||||||
|
const id = app.get_id();
|
||||||
|
if (id.endsWith('.desktop'))
|
||||||
|
return id;
|
||||||
|
// fallback: use window class
|
||||||
|
const windows = app.get_windows();
|
||||||
|
if (windows.length > 0) {
|
||||||
|
const wmClass = windows[0].get_wm_class();
|
||||||
|
if (wmClass)
|
||||||
|
return `${wmClass}.desktop`;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
this._tracker.disconnect(this._focusSig);
|
||||||
|
this._appSystem.disconnect(this._appSig);
|
||||||
|
this._dbus.unexport();
|
||||||
|
this._dbus = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetFocus() {
|
||||||
|
return this._currentApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetRunningApps() {
|
||||||
|
return this._appSystem.get_running().map(a => this._resolveAppId(a));
|
||||||
|
}
|
||||||
|
}
|
||||||
7
app-tracker@local/metadata.json
Normal file
7
app-tracker@local/metadata.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"uuid": "app-tracker@local",
|
||||||
|
"name": "Application Tracker",
|
||||||
|
"description": "Exposes active app over D-Bus",
|
||||||
|
"shell-version": ["48", "49", "50"],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
436
screentime-viewer
Normal file
436
screentime-viewer
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
#!/usr/bin/python3 -sP
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '4.0')
|
||||||
|
gi.require_version('Adw', '1')
|
||||||
|
gi.require_version('Gio', '2.0')
|
||||||
|
gi.require_version('GLib', '2.0')
|
||||||
|
try:
|
||||||
|
gi.require_version('GioUnix', '2.0')
|
||||||
|
from gi.repository import GioUnix
|
||||||
|
_DesktopAppInfo = GioUnix.DesktopAppInfo
|
||||||
|
except (ValueError, ImportError):
|
||||||
|
from gi.repository import Gio as _Gio
|
||||||
|
_DesktopAppInfo = _Gio.DesktopAppInfo
|
||||||
|
from gi.repository import Gtk, Adw, Gio, GLib, Gdk
|
||||||
|
|
||||||
|
SCREENTIMED_BUS = 'org.gnome.ScreenTime'
|
||||||
|
SCREENTIMED_PATH = '/org/gnome/ScreenTime'
|
||||||
|
SCREENTIMED_IFACE = 'org.gnome.ScreenTime'
|
||||||
|
|
||||||
|
|
||||||
|
def format_duration(seconds):
|
||||||
|
if seconds < 60:
|
||||||
|
return f'{seconds}s'
|
||||||
|
m, s = divmod(seconds, 60)
|
||||||
|
h, m = divmod(m, 60)
|
||||||
|
if h:
|
||||||
|
return f'{h}h {m}m'
|
||||||
|
return f'{m}m'
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_info(app_id):
|
||||||
|
"""Return (display_name, icon) from .desktop file, with fallbacks."""
|
||||||
|
if not app_id.endswith('.desktop'):
|
||||||
|
return None, None # not a real app
|
||||||
|
try:
|
||||||
|
info = _DesktopAppInfo.new(app_id)
|
||||||
|
if info:
|
||||||
|
name = info.get_display_name() or info.get_name()
|
||||||
|
icon = info.get_icon()
|
||||||
|
return name, icon
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
name = app_id.replace('.desktop', '').split('.')[-1]
|
||||||
|
return name, None
|
||||||
|
|
||||||
|
|
||||||
|
def merge_unknown(usage):
|
||||||
|
"""Collapse entries without .desktop into a single 'Unknown' entry."""
|
||||||
|
merged = {}
|
||||||
|
unknown = 0
|
||||||
|
for app_id, secs in usage.items():
|
||||||
|
if app_id.endswith('.desktop'):
|
||||||
|
merged[app_id] = secs
|
||||||
|
else:
|
||||||
|
unknown += secs
|
||||||
|
if unknown:
|
||||||
|
merged['__unknown__'] = unknown
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
# ── D-Bus client ──────────────────────────────────────────
|
||||||
|
|
||||||
|
class ScreenTimeClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.bus = Gio.bus_get_sync(Gio.BusType.SESSION)
|
||||||
|
|
||||||
|
def _call(self, method, args=None, reply_type=None):
|
||||||
|
try:
|
||||||
|
result = self.bus.call_sync(
|
||||||
|
SCREENTIMED_BUS, SCREENTIMED_PATH, SCREENTIMED_IFACE,
|
||||||
|
method,
|
||||||
|
args, GLib.VariantType(reply_type) if reply_type else None,
|
||||||
|
Gio.DBusCallFlags.NONE, -1, None)
|
||||||
|
return result.unpack()
|
||||||
|
except GLib.Error as e:
|
||||||
|
print(f'D-Bus error: {e.message}', file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_usage(self, date_str):
|
||||||
|
r = self._call('GetUsage', GLib.Variant('(s)', (date_str,)),
|
||||||
|
'(a{su}a{su})')
|
||||||
|
if r:
|
||||||
|
return dict(r[0]), dict(r[1])
|
||||||
|
return {}, {}
|
||||||
|
|
||||||
|
def get_usage_by_hour(self, date_str):
|
||||||
|
r = self._call('GetUsageByHour', GLib.Variant('(s)', (date_str,)),
|
||||||
|
'(aa{su}aa{su})')
|
||||||
|
if r:
|
||||||
|
return [dict(h) for h in r[0]], [dict(h) for h in r[1]]
|
||||||
|
return [{} for _ in range(24)], [{} for _ in range(24)]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Hourly bar chart ──────────────────────────────────────
|
||||||
|
|
||||||
|
class HourlyChart(Gtk.DrawingArea):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.focused_hours = [{} for _ in range(24)]
|
||||||
|
self.running_hours = [{} for _ in range(24)]
|
||||||
|
self.set_content_height(110)
|
||||||
|
self.set_draw_func(self._draw)
|
||||||
|
|
||||||
|
self.set_has_tooltip(True)
|
||||||
|
self.connect('query-tooltip', self._on_tooltip)
|
||||||
|
|
||||||
|
def set_data(self, focused_hours, running_hours):
|
||||||
|
self.focused_hours = focused_hours
|
||||||
|
self.running_hours = running_hours
|
||||||
|
self.queue_draw()
|
||||||
|
|
||||||
|
def _on_tooltip(self, widget, x, y, keyboard, tooltip):
|
||||||
|
width = self.get_width()
|
||||||
|
h = int((x / width) * 24)
|
||||||
|
h = max(0, min(23, h))
|
||||||
|
|
||||||
|
focused = self.focused_hours[h]
|
||||||
|
running = self.running_hours[h]
|
||||||
|
|
||||||
|
if not sum(focused.values()) and not sum(running.values()):
|
||||||
|
return False
|
||||||
|
|
||||||
|
lines = [f'<b>{h:02d}:00 – {h + 1:02d}:00</b>']
|
||||||
|
|
||||||
|
all_apps = set(focused.keys()) | set(running.keys())
|
||||||
|
sorted_apps = sorted(all_apps,
|
||||||
|
key=lambda a: focused.get(a, 0),
|
||||||
|
reverse=True)
|
||||||
|
for app_id in sorted_apps[:6]:
|
||||||
|
f = focused.get(app_id, 0)
|
||||||
|
r = running.get(app_id, 0)
|
||||||
|
if app_id.endswith('.desktop'):
|
||||||
|
name, _ = get_app_info(app_id)
|
||||||
|
name = name or app_id
|
||||||
|
else:
|
||||||
|
name = 'Unknown'
|
||||||
|
if r > f + 30:
|
||||||
|
lines.append(f'{name}: {format_duration(f)} / {format_duration(r)}')
|
||||||
|
elif f:
|
||||||
|
lines.append(f'{name}: {format_duration(f)}')
|
||||||
|
elif r:
|
||||||
|
lines.append(f'{name}: running {format_duration(r)}')
|
||||||
|
|
||||||
|
tooltip.set_markup('\n'.join(lines))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _get_colors(self):
|
||||||
|
"""Get accent and foreground RGBA for Cairo drawing."""
|
||||||
|
fg = self.get_color() # GTK4.10+
|
||||||
|
|
||||||
|
# try Adw 1.6+ accent API
|
||||||
|
accent = None
|
||||||
|
try:
|
||||||
|
sm = Adw.StyleManager.get_default()
|
||||||
|
ac = sm.get_accent_color()
|
||||||
|
accent = ac.to_rgba(sm.get_dark())
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if accent is None:
|
||||||
|
accent = Gdk.RGBA()
|
||||||
|
accent.parse('#3584e4')
|
||||||
|
|
||||||
|
return accent, fg
|
||||||
|
|
||||||
|
def _draw(self, area, cr, width, height):
|
||||||
|
f_totals = [sum(h.values()) for h in self.focused_hours]
|
||||||
|
r_totals = [sum(h.values()) for h in self.running_hours]
|
||||||
|
peak = max(max(r_totals), max(f_totals), 1)
|
||||||
|
|
||||||
|
bar_w = width / 24
|
||||||
|
pad = 2
|
||||||
|
label_h = 14
|
||||||
|
chart_h = height - label_h
|
||||||
|
|
||||||
|
accent, fg = self._get_colors()
|
||||||
|
|
||||||
|
# running bars
|
||||||
|
cr.set_source_rgba(accent.red, accent.green, accent.blue, 0.2)
|
||||||
|
|
||||||
|
for i, total in enumerate(r_totals):
|
||||||
|
if total == 0:
|
||||||
|
continue
|
||||||
|
bar_h = (total / peak) * (chart_h - 4)
|
||||||
|
cr.rectangle(i * bar_w + pad, chart_h - bar_h,
|
||||||
|
bar_w - pad * 2, bar_h)
|
||||||
|
cr.fill()
|
||||||
|
|
||||||
|
# focused bars
|
||||||
|
cr.set_source_rgba(accent.red, accent.green, accent.blue, 0.85)
|
||||||
|
|
||||||
|
for i, total in enumerate(f_totals):
|
||||||
|
if total == 0:
|
||||||
|
continue
|
||||||
|
bar_h = (total / peak) * (chart_h - 4)
|
||||||
|
cr.rectangle(i * bar_w + pad, chart_h - bar_h,
|
||||||
|
bar_w - pad * 2, bar_h)
|
||||||
|
cr.fill()
|
||||||
|
|
||||||
|
# labels
|
||||||
|
cr.set_source_rgba(fg.red, fg.green, fg.blue, 0.5)
|
||||||
|
cr.set_font_size(9)
|
||||||
|
for i in (0, 6, 12, 18):
|
||||||
|
cr.move_to(i * bar_w + bar_w / 2 - 6, height - 2)
|
||||||
|
cr.show_text(f'{i:02d}')
|
||||||
|
|
||||||
|
|
||||||
|
# ── App usage row ─────────────────────────────────────────
|
||||||
|
|
||||||
|
class AppUsageRow(Adw.ActionRow):
|
||||||
|
def __init__(self, app_id, focused_secs, running_secs, max_running):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
if app_id == '__unknown__':
|
||||||
|
name = 'Unknown'
|
||||||
|
icon = None
|
||||||
|
self.set_subtitle('Apps without .desktop entries')
|
||||||
|
else:
|
||||||
|
name, icon = get_app_info(app_id)
|
||||||
|
self.set_subtitle(app_id)
|
||||||
|
|
||||||
|
self.set_title(name or app_id)
|
||||||
|
|
||||||
|
if icon:
|
||||||
|
img = Gtk.Image.new_from_gicon(icon)
|
||||||
|
img.set_pixel_size(32)
|
||||||
|
self.add_prefix(img)
|
||||||
|
else:
|
||||||
|
img = Gtk.Image.new_from_icon_name('application-x-executable-symbolic')
|
||||||
|
img.set_pixel_size(32)
|
||||||
|
self.add_prefix(img)
|
||||||
|
|
||||||
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||||
|
box.set_valign(Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
BAR_W = 100
|
||||||
|
r_frac = min(1.0, running_secs / max_running) if max_running else 0
|
||||||
|
f_frac = min(1.0, focused_secs / max_running) if max_running else 0
|
||||||
|
|
||||||
|
bar_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||||
|
bar_box.set_size_request(BAR_W, -1)
|
||||||
|
|
||||||
|
f_bar = Gtk.Box()
|
||||||
|
f_bar.set_size_request(max(2, int(BAR_W * f_frac)), 6)
|
||||||
|
f_bar.set_halign(Gtk.Align.START)
|
||||||
|
f_bar.add_css_class('usage-bar-focused')
|
||||||
|
bar_box.append(f_bar)
|
||||||
|
|
||||||
|
if running_secs > focused_secs + 30:
|
||||||
|
r_bar = Gtk.Box()
|
||||||
|
r_bar.set_size_request(max(2, int(BAR_W * r_frac)), 4)
|
||||||
|
r_bar.set_halign(Gtk.Align.START)
|
||||||
|
r_bar.add_css_class('usage-bar-running')
|
||||||
|
bar_box.append(r_bar)
|
||||||
|
|
||||||
|
box.append(bar_box)
|
||||||
|
|
||||||
|
label_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||||
|
f_label = Gtk.Label(label=format_duration(focused_secs))
|
||||||
|
f_label.add_css_class('caption')
|
||||||
|
label_box.append(f_label)
|
||||||
|
|
||||||
|
if running_secs > focused_secs + 30:
|
||||||
|
r_label = Gtk.Label(label=f'/ {format_duration(running_secs)}')
|
||||||
|
r_label.add_css_class('caption')
|
||||||
|
r_label.add_css_class('dim-label')
|
||||||
|
label_box.append(r_label)
|
||||||
|
|
||||||
|
box.append(label_box)
|
||||||
|
self.add_suffix(box)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main window ───────────────────────────────────────────
|
||||||
|
|
||||||
|
class ScreenTimeWindow(Adw.ApplicationWindow):
|
||||||
|
def __init__(self, app):
|
||||||
|
super().__init__(application=app, title='Screen Time',
|
||||||
|
default_width=420, default_height=600)
|
||||||
|
|
||||||
|
self.client = ScreenTimeClient()
|
||||||
|
self.current_date = date.today()
|
||||||
|
|
||||||
|
css = Gtk.CssProvider()
|
||||||
|
css.load_from_string('''
|
||||||
|
.usage-bar-focused {
|
||||||
|
background: @accent_bg_color;
|
||||||
|
border-radius: 3px;
|
||||||
|
min-height: 6px;
|
||||||
|
}
|
||||||
|
.usage-bar-running {
|
||||||
|
background: alpha(@accent_bg_color, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
min-height: 4px;
|
||||||
|
}
|
||||||
|
.total-time {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
Gtk.StyleContext.add_provider_for_display(
|
||||||
|
Gdk.Display.get_default(), css,
|
||||||
|
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||||
|
|
||||||
|
toolbar = Adw.ToolbarView()
|
||||||
|
header = Adw.HeaderBar()
|
||||||
|
toolbar.add_top_bar(header)
|
||||||
|
|
||||||
|
# navigation
|
||||||
|
nav_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||||
|
nav_box.set_halign(Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
prev_btn = Gtk.Button(icon_name='go-previous-symbolic')
|
||||||
|
prev_btn.connect('clicked', lambda _: self._change_date(-1))
|
||||||
|
nav_box.append(prev_btn)
|
||||||
|
|
||||||
|
self.date_label = Gtk.Label()
|
||||||
|
self.date_label.add_css_class('title-4')
|
||||||
|
nav_box.append(self.date_label)
|
||||||
|
|
||||||
|
next_btn = Gtk.Button(icon_name='go-next-symbolic')
|
||||||
|
next_btn.connect('clicked', lambda _: self._change_date(1))
|
||||||
|
nav_box.append(next_btn)
|
||||||
|
|
||||||
|
header.set_title_widget(nav_box)
|
||||||
|
|
||||||
|
# content
|
||||||
|
scroll = Gtk.ScrolledWindow(vexpand=True)
|
||||||
|
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
self.content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||||
|
self.content.set_margin_start(16)
|
||||||
|
self.content.set_margin_end(16)
|
||||||
|
self.content.set_margin_top(16)
|
||||||
|
self.content.set_margin_bottom(16)
|
||||||
|
scroll.set_child(self.content)
|
||||||
|
toolbar.set_content(scroll)
|
||||||
|
|
||||||
|
# total
|
||||||
|
self.total_label = Gtk.Label()
|
||||||
|
self.total_label.add_css_class('total-time')
|
||||||
|
self.total_label.set_margin_bottom(4)
|
||||||
|
self.content.append(self.total_label)
|
||||||
|
|
||||||
|
self.total_sub = Gtk.Label()
|
||||||
|
self.total_sub.add_css_class('dim-label')
|
||||||
|
self.total_sub.set_margin_bottom(16)
|
||||||
|
self.content.append(self.total_sub)
|
||||||
|
|
||||||
|
# hourly chart
|
||||||
|
hr_group = Adw.PreferencesGroup(title='Hourly')
|
||||||
|
self.hourly = HourlyChart()
|
||||||
|
hr_group.add(self.hourly)
|
||||||
|
self.content.append(hr_group)
|
||||||
|
|
||||||
|
# app list
|
||||||
|
self.app_group = Adw.PreferencesGroup(title='Apps')
|
||||||
|
self.app_group.set_margin_top(16)
|
||||||
|
self.content.append(self.app_group)
|
||||||
|
|
||||||
|
self.set_content(toolbar)
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
def _change_date(self, delta):
|
||||||
|
self.current_date += timedelta(days=delta)
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
def _refresh(self):
|
||||||
|
date_str = self.current_date.isoformat()
|
||||||
|
|
||||||
|
if self.current_date == date.today():
|
||||||
|
self.date_label.set_label('Today')
|
||||||
|
elif self.current_date == date.today() - timedelta(days=1):
|
||||||
|
self.date_label.set_label('Yesterday')
|
||||||
|
else:
|
||||||
|
self.date_label.set_label(
|
||||||
|
self.current_date.strftime('%a, %b %-d'))
|
||||||
|
|
||||||
|
focused, running = self.client.get_usage(date_str)
|
||||||
|
focused = merge_unknown(focused)
|
||||||
|
running = merge_unknown(running)
|
||||||
|
f_hours, r_hours = self.client.get_usage_by_hour(date_str)
|
||||||
|
|
||||||
|
# totals
|
||||||
|
total_focused = sum(focused.values())
|
||||||
|
total_running = sum(running.values())
|
||||||
|
self.total_label.set_label(format_duration(total_focused))
|
||||||
|
if total_running > total_focused + 60:
|
||||||
|
self.total_sub.set_label(
|
||||||
|
f'{format_duration(total_running)} total running')
|
||||||
|
self.total_sub.set_visible(True)
|
||||||
|
else:
|
||||||
|
self.total_sub.set_visible(False)
|
||||||
|
|
||||||
|
# hourly
|
||||||
|
self.hourly.set_data(f_hours, r_hours)
|
||||||
|
|
||||||
|
# app list
|
||||||
|
self.content.remove(self.app_group)
|
||||||
|
self.app_group = Adw.PreferencesGroup(title='Apps')
|
||||||
|
self.app_group.set_margin_top(16)
|
||||||
|
self.content.append(self.app_group)
|
||||||
|
|
||||||
|
all_apps = set(focused.keys()) | set(running.keys())
|
||||||
|
sorted_apps = sorted(all_apps,
|
||||||
|
key=lambda a: focused.get(a, 0),
|
||||||
|
reverse=True)
|
||||||
|
max_running = max(running.values()) if running else 1
|
||||||
|
|
||||||
|
for app_id in sorted_apps:
|
||||||
|
f = focused.get(app_id, 0)
|
||||||
|
r = running.get(app_id, 0)
|
||||||
|
if r < 1 and f < 1:
|
||||||
|
continue
|
||||||
|
row = AppUsageRow(app_id, f, r, max_running)
|
||||||
|
self.app_group.add(row)
|
||||||
|
|
||||||
|
|
||||||
|
# ── App ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class ScreenTimeApp(Adw.Application):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(application_id='org.gnome.ScreenTime.Viewer')
|
||||||
|
|
||||||
|
def do_activate(self):
|
||||||
|
win = self.get_active_window()
|
||||||
|
if not win:
|
||||||
|
win = ScreenTimeWindow(self)
|
||||||
|
win.present()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = ScreenTimeApp()
|
||||||
|
app.run(sys.argv)
|
||||||
428
screentimed
Normal file
428
screentimed
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
#!/usr/bin/python3 -sP
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gio', '2.0')
|
||||||
|
gi.require_version('GLib', '2.0')
|
||||||
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
log = logging.getLogger('screentimed')
|
||||||
|
|
||||||
|
DB_DIR = os.path.join(GLib.get_user_data_dir(), 'screentime')
|
||||||
|
DB_PATH = os.path.join(DB_DIR, 'usage.db')
|
||||||
|
|
||||||
|
FLUSH_INTERVAL = 60
|
||||||
|
|
||||||
|
TRACKER_BUS = 'org.gnome.Shell'
|
||||||
|
TRACKER_IFACE = 'org.gnome.Shell.UsageTracker'
|
||||||
|
TRACKER_PATH = '/org/gnome/Shell/UsageTracker'
|
||||||
|
|
||||||
|
SCREENTIMED_BUS = 'org.gnome.ScreenTime'
|
||||||
|
SCREENTIMED_PATH = '/org/gnome/ScreenTime'
|
||||||
|
SCREENTIMED_IFACE_XML = '''<node>
|
||||||
|
<interface name="org.gnome.ScreenTime">
|
||||||
|
<method name="GetUsage">
|
||||||
|
<arg direction="in" name="date" type="s"/>
|
||||||
|
<arg direction="out" name="focused" type="a{su}"/>
|
||||||
|
<arg direction="out" name="running" type="a{su}"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetUsageToday">
|
||||||
|
<arg direction="out" name="focused" type="a{su}"/>
|
||||||
|
<arg direction="out" name="running" type="a{su}"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetUsageByHour">
|
||||||
|
<arg direction="in" name="date" type="s"/>
|
||||||
|
<arg direction="out" name="focused" type="aa{su}"/>
|
||||||
|
<arg direction="out" name="running" type="aa{su}"/>
|
||||||
|
</method>
|
||||||
|
<method name="GetTimeline">
|
||||||
|
<arg direction="in" name="date" type="s"/>
|
||||||
|
<arg direction="out" name="focused" type="a(sdd)"/>
|
||||||
|
<arg direction="out" name="running" type="a(sdd)"/>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
</node>'''
|
||||||
|
|
||||||
|
|
||||||
|
# ── database ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def open_db():
|
||||||
|
os.makedirs(DB_DIR, exist_ok=True)
|
||||||
|
db = sqlite3.connect(DB_PATH)
|
||||||
|
db.execute('PRAGMA journal_mode=WAL')
|
||||||
|
db.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS focus_sessions (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
app_id TEXT NOT NULL,
|
||||||
|
start_ts REAL NOT NULL,
|
||||||
|
end_ts REAL NOT NULL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
db.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS running_sessions (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
app_id TEXT NOT NULL,
|
||||||
|
start_ts REAL NOT NULL,
|
||||||
|
end_ts REAL NOT NULL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
db.execute('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_focus_time
|
||||||
|
ON focus_sessions (start_ts, end_ts)
|
||||||
|
''')
|
||||||
|
db.execute('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_running_time
|
||||||
|
ON running_sessions (start_ts, end_ts)
|
||||||
|
''')
|
||||||
|
# migrate from old schema
|
||||||
|
try:
|
||||||
|
db.execute('''
|
||||||
|
INSERT INTO focus_sessions (app_id, start_ts, end_ts)
|
||||||
|
SELECT app_id, start_ts, end_ts FROM sessions
|
||||||
|
''')
|
||||||
|
db.execute('DROP TABLE sessions')
|
||||||
|
db.commit()
|
||||||
|
log.info('migrated old sessions table')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
db.commit()
|
||||||
|
return db
|
||||||
|
|
||||||
|
|
||||||
|
def _date_to_ts(date):
|
||||||
|
return time.mktime(time.strptime(date, '%Y-%m-%d'))
|
||||||
|
|
||||||
|
|
||||||
|
def _day_range(date):
|
||||||
|
start = _date_to_ts(date)
|
||||||
|
return start, start + 86400
|
||||||
|
|
||||||
|
|
||||||
|
def _query_db(db, table, date):
|
||||||
|
return db.execute(f'''
|
||||||
|
SELECT app_id, start_ts, end_ts
|
||||||
|
FROM {table}
|
||||||
|
WHERE end_ts > unixepoch(:date)
|
||||||
|
AND start_ts < unixepoch(:date, '+1 day')
|
||||||
|
''', {'date': date}).fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
def _aggregate_daily(sessions, day_start, day_end):
|
||||||
|
usage = {}
|
||||||
|
for app_id, start, end in sessions:
|
||||||
|
overlap = min(end, day_end) - max(start, day_start)
|
||||||
|
if overlap > 0:
|
||||||
|
usage[app_id] = usage.get(app_id, 0) + int(overlap)
|
||||||
|
return usage
|
||||||
|
|
||||||
|
|
||||||
|
def _aggregate_hourly(sessions, day_start):
|
||||||
|
hours = [{} for _ in range(24)]
|
||||||
|
for app_id, start, end in sessions:
|
||||||
|
for h in range(24):
|
||||||
|
h_start = day_start + h * 3600
|
||||||
|
h_end = h_start + 3600
|
||||||
|
overlap = min(end, h_end) - max(start, h_start)
|
||||||
|
if overlap > 0:
|
||||||
|
hours[h][app_id] = hours[h].get(app_id, 0) + int(overlap)
|
||||||
|
return hours
|
||||||
|
|
||||||
|
|
||||||
|
def _clip_to_day(sessions, day_start, day_end):
|
||||||
|
"""Return (app_id, clipped_start, clipped_end) tuples."""
|
||||||
|
out = []
|
||||||
|
for app_id, start, end in sessions:
|
||||||
|
s = max(start, day_start)
|
||||||
|
e = min(end, day_end)
|
||||||
|
if e > s:
|
||||||
|
out.append((app_id, s, e))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ── tracker ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
class Tracker:
|
||||||
|
def __init__(self, db):
|
||||||
|
self.db = db
|
||||||
|
self.focus_buf = []
|
||||||
|
self.running_buf = []
|
||||||
|
|
||||||
|
self.current_app = ''
|
||||||
|
self.session_start = time.time()
|
||||||
|
self.mono_start = time.monotonic()
|
||||||
|
|
||||||
|
self.running_apps = {} # app_id -> (wall_start, mono_start)
|
||||||
|
|
||||||
|
self.idle = False
|
||||||
|
self.flush_timer_id = 0
|
||||||
|
|
||||||
|
# ── focus ──
|
||||||
|
|
||||||
|
def on_focus_changed(self, app_id):
|
||||||
|
if self.idle:
|
||||||
|
return
|
||||||
|
self._close_focus()
|
||||||
|
self.current_app = app_id
|
||||||
|
self.session_start = time.time()
|
||||||
|
self.mono_start = time.monotonic()
|
||||||
|
log.debug('focus: %s', app_id or '(none)')
|
||||||
|
|
||||||
|
def _close_focus(self):
|
||||||
|
elapsed = time.monotonic() - self.mono_start
|
||||||
|
if not self.current_app or elapsed < 1:
|
||||||
|
return
|
||||||
|
end_ts = self.session_start + elapsed
|
||||||
|
self.focus_buf.append((self.current_app, self.session_start, end_ts))
|
||||||
|
|
||||||
|
# ── running ──
|
||||||
|
|
||||||
|
def on_running_changed(self, app_ids):
|
||||||
|
now_wall = time.time()
|
||||||
|
now_mono = time.monotonic()
|
||||||
|
|
||||||
|
current = set(app_ids)
|
||||||
|
previous = set(self.running_apps.keys())
|
||||||
|
|
||||||
|
# closed apps
|
||||||
|
for app_id in previous - current:
|
||||||
|
wall_start, mono_start = self.running_apps.pop(app_id)
|
||||||
|
end_ts = wall_start + (now_mono - mono_start)
|
||||||
|
if end_ts - wall_start >= 1:
|
||||||
|
self.running_buf.append((app_id, wall_start, end_ts))
|
||||||
|
log.debug('closed: %s', app_id)
|
||||||
|
|
||||||
|
# new apps
|
||||||
|
for app_id in current - previous:
|
||||||
|
self.running_apps[app_id] = (now_wall, now_mono)
|
||||||
|
log.debug('opened: %s', app_id)
|
||||||
|
|
||||||
|
def _close_all_running(self):
|
||||||
|
now_wall = time.time()
|
||||||
|
now_mono = time.monotonic()
|
||||||
|
for app_id, (wall_start, mono_start) in self.running_apps.items():
|
||||||
|
end_ts = wall_start + (now_mono - mono_start)
|
||||||
|
if end_ts - wall_start >= 1:
|
||||||
|
self.running_buf.append((app_id, wall_start, end_ts))
|
||||||
|
# reopen immediately
|
||||||
|
for app_id in list(self.running_apps):
|
||||||
|
self.running_apps[app_id] = (now_wall, now_mono)
|
||||||
|
|
||||||
|
# ── idle ──
|
||||||
|
|
||||||
|
def on_idle(self):
|
||||||
|
if self.idle:
|
||||||
|
return
|
||||||
|
self._close_focus()
|
||||||
|
self._close_all_running()
|
||||||
|
self.idle = True
|
||||||
|
self._flush_to_db()
|
||||||
|
self._stop_timer()
|
||||||
|
log.debug('idle (locked/blank)')
|
||||||
|
|
||||||
|
def on_active(self):
|
||||||
|
if not self.idle:
|
||||||
|
return
|
||||||
|
self.idle = False
|
||||||
|
now_wall = time.time()
|
||||||
|
now_mono = time.monotonic()
|
||||||
|
self.session_start = now_wall
|
||||||
|
self.mono_start = now_mono
|
||||||
|
# reopen running timestamps
|
||||||
|
for app_id in list(self.running_apps):
|
||||||
|
self.running_apps[app_id] = (now_wall, now_mono)
|
||||||
|
self._start_timer()
|
||||||
|
log.debug('active (unlocked)')
|
||||||
|
|
||||||
|
# ── flush ──
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
if self.idle:
|
||||||
|
return
|
||||||
|
self._close_focus()
|
||||||
|
self._close_all_running()
|
||||||
|
self._flush_to_db()
|
||||||
|
self.session_start = time.time()
|
||||||
|
self.mono_start = time.monotonic()
|
||||||
|
|
||||||
|
def _flush_to_db(self):
|
||||||
|
if not self.focus_buf and not self.running_buf:
|
||||||
|
return
|
||||||
|
if self.focus_buf:
|
||||||
|
self.db.executemany(
|
||||||
|
'INSERT INTO focus_sessions (app_id, start_ts, end_ts) VALUES (?, ?, ?)',
|
||||||
|
self.focus_buf)
|
||||||
|
if self.running_buf:
|
||||||
|
self.db.executemany(
|
||||||
|
'INSERT INTO running_sessions (app_id, start_ts, end_ts) VALUES (?, ?, ?)',
|
||||||
|
self.running_buf)
|
||||||
|
self.db.commit()
|
||||||
|
log.debug('flushed %d focus + %d running',
|
||||||
|
len(self.focus_buf), len(self.running_buf))
|
||||||
|
self.focus_buf.clear()
|
||||||
|
self.running_buf.clear()
|
||||||
|
|
||||||
|
def _start_timer(self):
|
||||||
|
if self.flush_timer_id == 0:
|
||||||
|
self.flush_timer_id = GLib.timeout_add_seconds(
|
||||||
|
FLUSH_INTERVAL, self._on_timer)
|
||||||
|
|
||||||
|
def _stop_timer(self):
|
||||||
|
if self.flush_timer_id != 0:
|
||||||
|
GLib.source_remove(self.flush_timer_id)
|
||||||
|
self.flush_timer_id = 0
|
||||||
|
|
||||||
|
def _on_timer(self):
|
||||||
|
self.flush()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ── queries ──
|
||||||
|
|
||||||
|
def _all_sessions(self, table, buf, date):
|
||||||
|
day_start, day_end = _day_range(date)
|
||||||
|
sessions = _query_db(self.db, table, date)
|
||||||
|
|
||||||
|
for s in buf:
|
||||||
|
if s[2] > day_start and s[1] < day_end:
|
||||||
|
sessions.append(s)
|
||||||
|
|
||||||
|
return sessions
|
||||||
|
|
||||||
|
def _all_focus(self, date):
|
||||||
|
sessions = self._all_sessions('focus_sessions', self.focus_buf, date)
|
||||||
|
# add live focus
|
||||||
|
if not self.idle and self.current_app:
|
||||||
|
elapsed = time.monotonic() - self.mono_start
|
||||||
|
end_ts = self.session_start + elapsed
|
||||||
|
day_start, day_end = _day_range(date)
|
||||||
|
if end_ts > day_start and self.session_start < day_end:
|
||||||
|
sessions.append((self.current_app, self.session_start, end_ts))
|
||||||
|
return sessions
|
||||||
|
|
||||||
|
def _all_running(self, date):
|
||||||
|
sessions = self._all_sessions('running_sessions', self.running_buf, date)
|
||||||
|
# add live running
|
||||||
|
if not self.idle:
|
||||||
|
now_mono = time.monotonic()
|
||||||
|
day_start, day_end = _day_range(date)
|
||||||
|
for app_id, (wall_start, mono_start) in self.running_apps.items():
|
||||||
|
end_ts = wall_start + (now_mono - mono_start)
|
||||||
|
if end_ts > day_start and wall_start < day_end:
|
||||||
|
sessions.append((app_id, wall_start, end_ts))
|
||||||
|
return sessions
|
||||||
|
|
||||||
|
def get_usage(self, date):
|
||||||
|
day_start, day_end = _day_range(date)
|
||||||
|
focused = _aggregate_daily(self._all_focus(date), day_start, day_end)
|
||||||
|
running = _aggregate_daily(self._all_running(date), day_start, day_end)
|
||||||
|
return focused, running
|
||||||
|
|
||||||
|
def get_usage_by_hour(self, date):
|
||||||
|
day_start, _ = _day_range(date)
|
||||||
|
focused = _aggregate_hourly(self._all_focus(date), day_start)
|
||||||
|
running = _aggregate_hourly(self._all_running(date), day_start)
|
||||||
|
return focused, running
|
||||||
|
|
||||||
|
def get_timeline(self, date):
|
||||||
|
day_start, day_end = _day_range(date)
|
||||||
|
focused = _clip_to_day(self._all_focus(date), day_start, day_end)
|
||||||
|
running = _clip_to_day(self._all_running(date), day_start, day_end)
|
||||||
|
return focused, running
|
||||||
|
|
||||||
|
|
||||||
|
# ── main ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG if os.environ.get('SCREENTIMED_DEBUG') else logging.INFO,
|
||||||
|
format='%(name)s: %(message)s',
|
||||||
|
)
|
||||||
|
|
||||||
|
db = open_db()
|
||||||
|
tracker = Tracker(db)
|
||||||
|
loop = GLib.MainLoop()
|
||||||
|
bus = Gio.bus_get_sync(Gio.BusType.SESSION)
|
||||||
|
|
||||||
|
# listen to the extension
|
||||||
|
bus.signal_subscribe(
|
||||||
|
TRACKER_BUS, TRACKER_IFACE, 'FocusChanged',
|
||||||
|
TRACKER_PATH, None, Gio.DBusSignalFlags.NONE,
|
||||||
|
lambda *a: tracker.on_focus_changed(a[5].unpack()[0]))
|
||||||
|
|
||||||
|
bus.signal_subscribe(
|
||||||
|
TRACKER_BUS, TRACKER_IFACE, 'RunningAppsChanged',
|
||||||
|
TRACKER_PATH, None, Gio.DBusSignalFlags.NONE,
|
||||||
|
lambda *a: tracker.on_running_changed(a[5].unpack()[0]))
|
||||||
|
|
||||||
|
# pause tracking on lock/blank
|
||||||
|
bus.signal_subscribe(
|
||||||
|
'org.gnome.ScreenSaver', 'org.gnome.ScreenSaver', 'ActiveChanged',
|
||||||
|
'/org/gnome/ScreenSaver', None, Gio.DBusSignalFlags.NONE,
|
||||||
|
lambda *a: tracker.on_idle() if a[5].unpack()[0] else tracker.on_active())
|
||||||
|
|
||||||
|
# fetch initial state
|
||||||
|
try:
|
||||||
|
result = bus.call_sync(
|
||||||
|
TRACKER_BUS, TRACKER_PATH, TRACKER_IFACE, 'GetFocus',
|
||||||
|
None, GLib.VariantType('(s)'),
|
||||||
|
Gio.DBusCallFlags.NONE, -1, None)
|
||||||
|
tracker.current_app = result.unpack()[0]
|
||||||
|
|
||||||
|
result = bus.call_sync(
|
||||||
|
TRACKER_BUS, TRACKER_PATH, TRACKER_IFACE, 'GetRunningApps',
|
||||||
|
None, GLib.VariantType('(as)'),
|
||||||
|
Gio.DBusCallFlags.NONE, -1, None)
|
||||||
|
now_wall = time.time()
|
||||||
|
now_mono = time.monotonic()
|
||||||
|
for app_id in result.unpack()[0]:
|
||||||
|
tracker.running_apps[app_id] = (now_wall, now_mono)
|
||||||
|
|
||||||
|
log.debug('initial: focus=%s, running=%s',
|
||||||
|
tracker.current_app, list(tracker.running_apps.keys()))
|
||||||
|
except GLib.Error:
|
||||||
|
log.debug('extension not yet available')
|
||||||
|
|
||||||
|
# expose analytics over d-bus
|
||||||
|
node = Gio.DBusNodeInfo.new_for_xml(SCREENTIMED_IFACE_XML)
|
||||||
|
|
||||||
|
def on_method_call(conn, sender, path, iface, method, params, invocation):
|
||||||
|
if method == 'GetUsageToday':
|
||||||
|
f, r = tracker.get_usage(time.strftime('%Y-%m-%d'))
|
||||||
|
invocation.return_value(GLib.Variant('(a{su}a{su})', (f, r)))
|
||||||
|
elif method == 'GetUsage':
|
||||||
|
f, r = tracker.get_usage(params.unpack()[0])
|
||||||
|
invocation.return_value(GLib.Variant('(a{su}a{su})', (f, r)))
|
||||||
|
elif method == 'GetUsageByHour':
|
||||||
|
f, r = tracker.get_usage_by_hour(params.unpack()[0])
|
||||||
|
invocation.return_value(GLib.Variant('(aa{su}aa{su})', (f, r)))
|
||||||
|
elif method == 'GetTimeline':
|
||||||
|
f, r = tracker.get_timeline(params.unpack()[0])
|
||||||
|
invocation.return_value(GLib.Variant('(a(sdd)a(sdd))', (f, r)))
|
||||||
|
|
||||||
|
bus.register_object_with_closures2(
|
||||||
|
SCREENTIMED_PATH, node.interfaces[0],
|
||||||
|
on_method_call, None, None)
|
||||||
|
|
||||||
|
Gio.bus_own_name_on_connection(
|
||||||
|
bus, SCREENTIMED_BUS, Gio.BusNameOwnerFlags.NONE, None, None)
|
||||||
|
|
||||||
|
tracker._start_timer()
|
||||||
|
|
||||||
|
def on_term():
|
||||||
|
tracker.flush()
|
||||||
|
loop.quit()
|
||||||
|
|
||||||
|
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGTERM, on_term)
|
||||||
|
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, on_term)
|
||||||
|
|
||||||
|
log.info('tracking to %s', DB_PATH)
|
||||||
|
loop.run()
|
||||||
|
log.info('stopped')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
7
screentimed.desktop
Normal file
7
screentimed.desktop
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=ScreenTime
|
||||||
|
Comment=View your screen time usage
|
||||||
|
Exec=screentime-viewer
|
||||||
|
Icon=preferences-system-time-symbolic
|
||||||
|
Type=Application
|
||||||
|
Categories=GNOME;GTK;Utility;
|
||||||
13
screentimed.service
Normal file
13
screentimed.service
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Screen Time Tracking Daemon
|
||||||
|
After=gnome-shell.service
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/bin/screentimed
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical-session.target
|
||||||
Reference in New Issue
Block a user