- Go 83.7%
- JavaScript 16.3%
Titles say what you were doing (the file in an editor, the session in a terminal) but also hold page titles and chat subjects, so nothing is opted in by default. Applications listed in ~/.config/gosh-gnome/titles get their window title and pid logged; the event detail becomes "Name: title" and the session is the window pid. A title that changes while a window keeps focus is a title event and does not end the span; changes are debounced to one per 10 seconds because a terminal rewrites its title at every prompt. collect re-applies the list when reading the log, so removing an application stops its titles being reported even from an existing log. Titles are cleaned to one line of at most 120 characters. doctor shows what is opted in. |
||
|---|---|---|
| extension | ||
| .gitignore | ||
| focus.go | ||
| focus_test.go | ||
| go.mod | ||
| LICENSE | ||
| main.go | ||
| README.md | ||
gosh-gnome
A gosh plugin that reports which application was in front of you on a GNOME desktop: when you were in your editor, your terminal, your browser, and for how long.
GNOME on Wayland doesn't let a program ask which window has focus (org.gnome.Shell.Introspect is
access-denied, Shell.Eval is off); only code running inside the shell can see it. So there are two parts:
- a small shell extension that appends focus, idle and heartbeat records to
~/.local/state/gosh/gnome-focus.log, next to gosh's owncwd.log; gosh-gnome, which turns that log into gosh events. There is no daemon:gosh collectruns it like any plugin.
| kind | one event per |
|---|---|
focus |
time an application took focus (detail: its name, e.g. Firefox) |
active |
5 minutes it stayed in front of you, plus one where the stretch ended |
title |
change of the focused window's title (only for applications you opt in) |
gosh infers time from the spacing of events, so a long stretch reports itself every 5 minutes and a two-hour session counts as two hours, not 5 minutes.
Install
go install git.batts.cloud/vbatts/gosh-gnome@latest
gosh-gnome install-extension # copies the extension to ~/.local/share/gnome-shell/extensions/
gnome-extensions enable gosh-gnome@batts.cloud
# log out and back in: on Wayland the shell only loads new extensions at login
gosh-gnome doctor # is the log being written?
gosh plugin add gnome
gosh collect
install-extension doesn't enable anything; that step is yours. The extension is embedded in the binary,
so the plugin and the extension are always the same version.
Where activity is reported
gosh plugin protocol 2 lets an event's path be a URI, and each application is app://<desktop file id>:
app://org.mozilla.firefox.desktop, app://org.gnome.Terminal.desktop. gosh groups these in the same tree
as files and web pages, so:
gosh report -by path -only gnome -since 7d # time per application
gosh timeline -prefix app://org.gnome.Terminal.desktop # when you were in the terminal
gosh report -by path -exclude gnome # everything except window focus
Applications with no desktop file are named by their WM_CLASS instead. Windowless time (the overview, the
lock screen, nothing focused) is not reported.
Window focus has no git project, so gosh report -by project shows these under (no repo). Use the path
views, or -exclude gnome there.
How time is worked out
A stretch counts while a window is focused, you are at the keyboard, and the shell is known to be alive:
- Idle. After 2 minutes without input the extension logs
idle. The stretch ends where your input stopped, not 2 minutes later, and resumes at your next input. - Silence. The extension logs a
tickevery minute. If nothing is written for more than 2.5 minutes the shell wasn't running (suspend, crash), so those hours aren't counted. The next tick resumes the stretch, on the assumption you are back; a long suspend that ends with you away will over-count a little. - Still going. A stretch that is still going reports its start and heartbeats but no end yet, and every event's ref depends only on when the stretch began, so collecting repeatedly never duplicates anything.
Window titles
Titles say what you were doing, which an application name can't: the file in an editor, the session or
directory in a terminal. They also hold page titles, document names and chat subjects, so nothing is
opted in for you. List the applications whose titles may be logged, by desktop file id, one per line, in
~/.config/gosh-gnome/titles:
# terminals
ghostty.desktop
For those applications only, the extension logs the window's title and its process id (pid), and the
events carry Name: title as their detail and the pid as their session, so gosh timeline shows
@gnome focus Ghostty: tmux: gosh (app://ghostty.desktop) [4239]. A title that changes while the window
keeps focus is a title event; it does not end the stretch, and a title that keeps changing, such as a
terminal's at every prompt, is logged at most every 10 seconds. Titles are one line of at most 120 characters.
The list is read each time a window takes focus, so editing it needs no restart. gosh-gnome collect applies
the same list when it reads the log: take an application off it and its titles stop being reported at once,
even from a log that already has them. Events gosh has already stored keep what they were sent. doctor shows
what is opted in.
A few things worth knowing:
- The window pid is the process that owns the window, which is not enough to tell windows apart:
Ghostty with
--gtk-single-instance(its default) serves every window from one process. The title does that job. - A terminal's title is whatever it was told to show. Under tmux that is
tmuxunless tmux sets it;set -g set-titles onandset -g set-titles-string '#S: #W'intmux.confmake it name the session and window. - Don't opt in a browser. gosh-firefox already reports pages,
with
detail titleif you want titles, and those titles are the ones most worth keeping out. - Shells that run inside tmux are children of the tmux server, not of the terminal, so their pid has no relation to the window's.
Privacy
The log is a plain file in your state directory (mode 0600). By default it holds timestamps, the
application's desktop file id and name, and its WM_CLASS: nothing about what is inside a window. With
titles it also holds the title and pid for the applications you listed, and only those.
Without titles the plugin can't tell two terminals, or two documents in one editor, apart; it sees
applications, not windows.
Limitations
- The extension is written against GNOME Shell 48 (
metadata.jsonsays"shell-version": ["48"]; the extension API it uses is the same from 45, so widening it is a one-line change once you've tried it). The Go side is tested; the extension's calls were checked against the installed GNOME 48 typelibs and the file syntax-checked, but it has not been loaded into a live shell as part of building this.doctoris how you find out. - A shell extension runs inside the shell, so a bug in it is a bug in your desktop. It's about 100 lines and does one synchronous append on each focus change and each minute; read it before enabling it.
- The log is never rotated and
collectreads all of it each run (about 150 KB a day). Truncating it is safe: gosh already has the events. - Overlap with other plugins is real: Firefox in front of you is both
app://org.mozilla.firefox.desktophere and pages ingosh-firefox. They answer different questions; use-only/-exclude. - X11 sessions: nothing here is Wayland-specific, so it should work the same (untested); Wayland is simply why an extension is needed at all.
Protocol
gosh-gnome describe
gosh-gnome collect --since <unix seconds> [--log <file>] [--titles <file>]
gosh-gnome doctor [--log <file>] [--titles <file>]
gosh-gnome install-extension [--dir <extensions dir>]
The plugin declares protocol 2 (paths may be URIs), so it needs a gosh that supports it. Output is JSON lines; see gosh's docs/PLUGINS.md. Only the standard library is used.
License
MIT, see LICENSE.