arbtt 0.8.1.4 → 0.9
raw patch · 6 files changed
+702/−139 lines, 6 filesdep ~process-extras
Dependency ranges changed: process-extras
Files
- arbtt.cabal +9/−8
- doc/arbtt.xml +628/−95
- src/Data/MyText.hs +12/−6
- src/Stats.hs +36/−15
- src/TimeLog.hs +2/−2
- src/stats-main.hs +15/−13
arbtt.cabal view
@@ -1,5 +1,5 @@ name: arbtt-version: 0.8.1.4+version: 0.9 license: GPL license-file: LICENSE category: Desktop@@ -22,6 +22,7 @@ you understand the consequences of a full-time logger and be careful with this data. homepage: http://arbtt.nomeata.de/+bug-reports: https://bitbucket.org/nomeata/arbtt/issues extra-source-files: categorize.cfg, arbtt-capture.desktop, README.md,@@ -48,10 +49,10 @@ TimeLog UpgradeLog1 LeftFold- + ghc-options: -rtsopts - if os(windows) + if os(windows) extra-libraries: psapi cpp-options: -DWIN32 ghc-options: -optl-mwindows@@ -98,7 +99,7 @@ Text.Regex.PCRE.Light.Text TermSize ghc-options: -rtsopts- if os(windows) + if os(windows) cpp-options: -DWIN32 else other-modules:@@ -127,7 +128,7 @@ DumpFormat Data.List.TakeR ghc-options: -rtsopts- if os(windows) + if os(windows) cpp-options: -DWIN32 else other-modules:@@ -151,7 +152,7 @@ CommonStartup TimeLog ghc-options: -rtsopts- if os(windows) + if os(windows) cpp-options: -DWIN32 else other-modules:@@ -174,7 +175,7 @@ CommonStartup TimeLog ghc-options: -rtsopts- if os(windows) + if os(windows) cpp-options: -DWIN32 else other-modules:@@ -195,7 +196,7 @@ , tasty-golden >= 2.2.0.2 && <= 2.3 , tasty-hunit >= 0.2 && < 0.11 , HUnit == 1.2.*- , process-extras == 0.2.*+ , process-extras == 0.2.* || == 0.3.* , deepseq , binary >= 0.5 , bytestring
doc/arbtt.xml view
@@ -330,16 +330,16 @@ <figure id="grammar"> <title>The formal grammar of <filename>categorize.cfg</filename></title>- <productionset>- <production id="g-rules">- <lhs>Rules</lhs>- <rhs>- [ <nonterminal def="#g-aliasspec"/> ]- <nonterminal def="#g-rule"/> ( (<quote>,</quote>- <nonterminal def="#g-rule"/>)* | ( <quote>;</quote>- <nonterminal def="#g-rule"/>)* )- </rhs>- </production>+ <productionset>+ <production id="g-rules">+ <lhs>Rules</lhs>+ <rhs>+ [ <nonterminal def="#g-aliasspec"/> ]+ <nonterminal def="#g-rule"/> ( (<quote>,</quote>+ <nonterminal def="#g-rule"/>)* | ( <quote>;</quote>+ <nonterminal def="#g-rule"/>)* )+ </rhs>+ </production> <production id="g-aliasspec"> <lhs>AliasSpec</lhs>@@ -353,9 +353,9 @@ <rhs>Literal <quote>-></quote> Literal</rhs> </production> - <production id="g-rule">- <lhs>Rule</lhs>- <rhs><quote>{</quote> <nonterminal def="#g-rules"/>+ <production id="g-rule">+ <lhs>Rule</lhs>+ <rhs><quote>{</quote> <nonterminal def="#g-rules"/> <quote>}</quote> </rhs> <rhs>@@ -368,11 +368,11 @@ <rhs> <quote>tag</quote> <nonterminal def="#g-tag"/> </rhs>- </production>+ </production> - <production id="g-cond">- <lhs>Cond</lhs>- <rhs><quote>(</quote> <nonterminal def="#g-cond"/>+ <production id="g-cond">+ <lhs>Cond</lhs>+ <rhs><quote>(</quote> <nonterminal def="#g-cond"/> <quote>)</quote> </rhs> <rhs><quote>!</quote> <nonterminal def="#g-cond"/> |@@ -381,67 +381,67 @@ <nonterminal def="#g-cond"/> <quote>||</quote> <nonterminal def="#g-cond"/> </rhs>- <rhs> <quote>$active</quote> </rhs>+ <rhs> <quote>$active</quote> </rhs> <rhs> <nonterminal def="#g-string"/> <nonterminal def="#g-cmpop"/>- <nonterminal def="#g-string"/> </rhs>+ <nonterminal def="#g-string"/> </rhs> <rhs> <nonterminal def="#g-string"/> <nonterminal def="#g-cmpop"/>- <quote>[</quote> <nonterminal def="#g-listofstring"/>+ <quote>[</quote> <nonterminal def="#g-listofstring"/> <quote>]</quote> </rhs> <rhs> <nonterminal def="#g-string"/> <quote>=~</quote>- <nonterminal def="#g-regex"/></rhs>+ <nonterminal def="#g-regex"/></rhs> <rhs> <nonterminal def="#g-string"/> <quote>=~</quote>- <quote>[</quote> <nonterminal def="#g-listofregex"/>+ <quote>[</quote> <nonterminal def="#g-listofregex"/> <quote>]</quote> </rhs> <rhs> <nonterminal def="#g-number"/> <nonterminal def="#g-cmpop"/>- <nonterminal def="#g-number"/> </rhs>+ <nonterminal def="#g-number"/> </rhs> <rhs> <nonterminal def="#g-timediff"/> <nonterminal def="#g-cmpop"/>- <nonterminal def="#g-timediff"/> </rhs>+ <nonterminal def="#g-timediff"/> </rhs> <rhs> <nonterminal def="#g-date"/> <nonterminal def="#g-cmpop"/>- <nonterminal def="#g-date"/> </rhs>+ <nonterminal def="#g-date"/> </rhs> <rhs> <quote>current window</quote> <nonterminal def="#g-cond"/> </rhs> <rhs> <quote>any window</quote> <nonterminal def="#g-cond"/> </rhs>- </production>+ </production> - <production id="g-string">- <lhs>String</lhs>- <rhs> <quote>$title</quote> </rhs>- <rhs> <quote>$program</quote> </rhs>- <rhs> <quote>$desktop</quote> </rhs>- <rhs> <quote>format</quote> <nonterminal def="#g-date" /> </rhs>- <rhs> <quote>"</quote> string literal <quote>"</quote> </rhs>- </production>+ <production id="g-string">+ <lhs>String</lhs>+ <rhs> <quote>$title</quote> </rhs>+ <rhs> <quote>$program</quote> </rhs>+ <rhs> <quote>$desktop</quote> </rhs>+ <rhs> <quote>format</quote> <nonterminal def="#g-date" /> </rhs>+ <rhs> <quote>"</quote> string literal <quote>"</quote> </rhs>+ </production> - <production id="g-listofstring">- <lhs>ListOfString</lhs>- <rhs> <quote>"</quote> string literal <quote>"</quote> </rhs>- <rhs> <quote>"</quote> string literal <quote>"</quote> , <nonterminal def="#g-listofstring"/> </rhs>- </production>+ <production id="g-listofstring">+ <lhs>ListOfString</lhs>+ <rhs> <quote>"</quote> string literal <quote>"</quote> </rhs>+ <rhs> <quote>"</quote> string literal <quote>"</quote> , <nonterminal def="#g-listofstring"/> </rhs>+ </production> - <production id="g-number">- <lhs>Number</lhs>- <rhs> <quote>$idle</quote> </rhs>- <rhs> <quote>day of week</quote> <nonterminal def="#g-date" /> </rhs>- <rhs> <quote>day of month</quote> <nonterminal def="#g-date" /> </rhs>- <rhs> <quote>month</quote> <nonterminal def="#g-date" /> </rhs>- <rhs> <quote>year</quote> <nonterminal def="#g-date" /> </rhs>- <rhs> number literal </rhs>- </production>+ <production id="g-number">+ <lhs>Number</lhs>+ <rhs> <quote>$idle</quote> </rhs>+ <rhs> <quote>day of week</quote> <nonterminal def="#g-date" /> </rhs>+ <rhs> <quote>day of month</quote> <nonterminal def="#g-date" /> </rhs>+ <rhs> <quote>month</quote> <nonterminal def="#g-date" /> </rhs>+ <rhs> <quote>year</quote> <nonterminal def="#g-date" /> </rhs>+ <rhs> number literal </rhs>+ </production> - <production id="g-date">- <lhs>Date</lhs>- <rhs> <quote>$date</quote> </rhs>- <!-- <rhs> <quote>$now</quote> </rhs> -->- </production>+ <production id="g-date">+ <lhs>Date</lhs>+ <rhs> <quote>$date</quote> </rhs>+ <!-- <rhs> <quote>$now</quote> </rhs> -->+ </production> - <production id="g-timediff">- <lhs>TimeDiff</lhs>- <rhs> <quote>$time</quote> </rhs>- <rhs> <quote>$sampleage</quote> </rhs>- <!-- <rhs> <nonterminal def="#g-date"/> <quote>-</quote> <nonterminal def="#g-date"/></rhs> -->+ <production id="g-timediff">+ <lhs>TimeDiff</lhs>+ <rhs> <quote>$time</quote> </rhs>+ <rhs> <quote>$sampleage</quote> </rhs>+ <!-- <rhs> <nonterminal def="#g-date"/> <quote>-</quote> <nonterminal def="#g-date"/></rhs> --> <rhs>( Digit )* Digit <quote>:</quote> Digit Digit</rhs>- </production>+ </production> <production id="g-tag"> <lhs>Tag</lhs>@@ -457,11 +457,11 @@ character.</lineannotation> </rhs> </production> - <production id="g-listofregex">- <lhs>ListOfRegex</lhs>- <rhs> <quote>"</quote> <nonterminal def="#g-regex"/> <quote>"</quote> </rhs>- <rhs> <quote>"</quote> <nonterminal def="#g-regex"/> <quote>"</quote> , <nonterminal def="#g-listofregex"/> </rhs>- </production>+ <production id="g-listofregex">+ <lhs>ListOfRegex</lhs>+ <rhs> <quote>"</quote> <nonterminal def="#g-regex"/> <quote>"</quote> </rhs>+ <rhs> <quote>"</quote> <nonterminal def="#g-regex"/> <quote>"</quote> , <nonterminal def="#g-listofregex"/> </rhs>+ </production> <production id="g-cmpop"> <lhs>CmpOp</lhs>@@ -469,7 +469,7 @@ | <quote>></quote> | <quote>>=</quote></rhs> </production> - </productionset>+ </productionset> </figure> <para> A <literal>String</literal> refers to a double-quoted string of@@ -507,9 +507,524 @@ expressions. </para> </sect2>- </sect1> +<sect1 id="effective-use">+ <title>Effective Use of Arbtt</title>+ <para>+ Now that the syntax has been described and the toolbox laid out,+ how do you practically go about using and configuring arbtt?+ </para>+ <sect2>+ <title>Enabling data collection</title>+ <para>+ After installing arbtt, you need to configure it to run. There+ are many ways you can run the <literal>arbtt-capture</literal>+ daemon. One standard way is to include the command+ <programlisting>+arbtt-capture &+ </programlisting>+ in your desktop environments startup script, e.g.+ <filename>~/.xinitrc</filename> or similar.+ </para>+ <para>+ Another trick is add it as a <ulink+ url="https://en.wikipedia.org/wiki/Cron"><literal>cron</literal></ulink>+ job. To do so, edit your crontab file (<literal>crontab -e</literal>) and+ add a line like this:+ </para>+ <programlisting>+DISPLAY=:0+@reboot arbtt-capture --logfile=/home/username/doc/arbtt/capture.log+</programlisting>+ <para>+ At boot, <literal>arbtt-capture</literal> will be run in the+ background and will capture a snapshot of the X metadata for+ active windows every 60 seconds (the default). If you want more+ fine-grained time data at the expense of doubling storage use,+ you could increase the sampling rate with an option like+ <literal>--sample-rate=30</literal>. To be resilient to any errors+ or segfaults, you could also wrap it in an infinite loop to restart+ the daemon should it ever crash, with a command like+ </para>+ <programlisting>+DISPLAY=:0+@reboot while true; do arbtt-capture --sample-rate=30; sleep 1m; done+</programlisting>+ </sect2>+ <sect2>+ <title>Checking data availability</title>+ <para>+ arbtt tracks <ulink url="https://en.wikipedia.org/wiki/X_Window_System_protocols_and_architecture#Attributes_and_properties"+ >X</ulink> properties like window title, class, and running+ program, and you write rules to classify those strings as+ you wish; but this assumes that the necessary data is present in+ those properties.+ </para>+ <para>+ For some programs, this is the case. For example, web browsers+ like Firefox typically set the X title to the+ HTML <literal><title></literal> element of the web page in the+ currently-focused tab, which is enough for classification.+ </para>+ <para>+ Some programs do not set titles or class, and all arbtt sees is+ empty strings like <literal>""</literal>; or they may set the title/class+ to a constant like <literal>"Liferea"</literal>, which may be acceptable if+ that program is used for only one purpose, but if it is used for+ many purposes, then you cannot write a rule matching it without+ producing highly-misleading time analyses. (For example, a web+ browser may be used for countless purposes, ranging from work to+ research to music to writing to programming; but if the web+ browser's title/class were always just <literal>"Web browser"</literal>,+ how would you classify 5 hours spent using the web browser? If the+ 5 hours are classified as any or all of those purposes, then the+ results will be misleading garbage - you probably did not spend 5+ hours just listening to music, but a mixture of those purposes,+ which changes from day to day.)+ </para>+ <para>+ You should check for such problematic programs upon starting using+ arbtt. It would be unfortunate if you were to log for a few+ months, go back for a detailed report for some reason, and+ discover that the necessary data was never available for+ arbtt to log!+ </para>+ <para>+ These programs can sometimes be customized internally, a bug+ report filed with the maintainers, or their titles can be+ externally set by+ <ulink url="https://en.wikipedia.org/wiki/Wmctrl"><literal>wmctrl</literal></ulink>+ or+ <ulink url="http://jonisalonen.com/2014/setting-x11-window-properties-with-xprop/"><literal>xprop</literal></ulink>.+ </para>+ <sect3>+ <title><literal>xprop</literal></title>+ <para>+ You can check the X properties of a running window by running+ the command+ <ulink url="http://www.xfree86.org/current/xprop.1.html"><literal>xprop</literal></ulink>+ and clicking on the window; <literal>xprop</literal> will print+ out all the relevant X information. For example, the output for+ Emacs might look like this+ </para>+ <programlisting>+$ xprop | tail -5+WM_CLASS(STRING) = "emacs", "Emacs"+WM_ICON_NAME(STRING) = "emacs@elan"+_NET_WM_ICON_NAME(UTF8_STRING) = "emacs@elan"+WM_NAME(STRING) = "emacs@elan"+_NET_WM_NAME(UTF8_STRING) = "emacs@elan"+</programlisting>+ <para>+ This is not very helpful: it does not tell us the filename being+ edited, the mode being used, or anything. You could classify+ time spent in Emacs as "programming" or+ "writing", but this would be imperfect, especially if+ you do both activities regularly. However, Emacs can be+ customized by editing <literal>~/.emacs</literal>, and after+ some searching with queries like "setting Emacs window+ title", the+ <ulink url="http://www.emacswiki.org/emacs-en/FrameTitle">Emacs+ wiki</ulink> and+ <ulink url="https://www.gnu.org/software/emacs/manual/html_node/efaq/Displaying-the-current-file-name-in-the-titlebar.html">manual</ulink>+ advise us to put something like this Elisp in our+ <literal>.emacs</literal> file:+ </para>+ <programlisting>+(setq frame-title-format "%f")+</programlisting>+ <para>+ Now the output looks different:+ </para>+ <programlisting>+$ xprop | tail -5+WM_CLASS(STRING) = "emacs", "Emacs"+WM_ICON_NAME(STRING) = "/home/gwern/arbtt.page"+_NET_WM_ICON_NAME(UTF8_STRING) = "/home/gwern/arbtt.page"+WM_NAME(STRING) = "/home/gwern/arbtt.page"+_NET_WM_NAME(UTF8_STRING) = "/home/gwern/arbtt.page"+</programlisting>+ <para>+ With this, we can usefully classify all such time samples as+ being “writing”:+ </para>+<programlisting>+current window $title == "/home/gwern/arbtt.page" ==> tag Writing,+</programlisting>+ <para>+ Another common gap is terminals/shells: they often do not+ include information in the title like the current working+ directory or last shell command. For example, urxvt/Bash:+ </para>+ <programlisting>+WM_COMMAND(STRING) = { "urxvt" }+_NET_WM_ICON_NAME(UTF8_STRING) = "urxvt"+WM_ICON_NAME(STRING) = "urxvt"+_NET_WM_NAME(UTF8_STRING) = "urxvt"+WM_NAME(STRING) = "urxvt"+</programlisting>+ <para>+ Programmers may spend many hours in the shell doing a variety of+ things (like Emacs), so this is a problem. Fortunately, this is+ also solvable by customizing one's <literal>.bashrc</literal> to+ set the prompt to emit an escape code interpreted by the+ terminal (baroque, but it works). The following will include the+ working directory, a timestamp, and the last command:+ </para>+ <programlisting>+trap 'echo -ne "\033]2;$(pwd); $(history 1 | sed "s/^[ ]*[0-9]*[ ]*//g")\007"' DEBUG+</programlisting>+ <para>+ Now the urxvt samples are useful:+ </para>+ <programlisting>+_NET_WM_NAME(UTF8_STRING) = "/home/gwern/wiki; 2014-09-03 13:39:32 arbtt-stats --help"+</programlisting>+ <para>+ Some distributions (e.g. Debian) already provide the relevant+ configuration for this to happen. If it does not work for you, you can try to add+ <programlisting>. /etc/profile.d/vte.sh</programlisting>+ to your <filename>~/.bashrc</filename>.+ </para>+ <para>+ A rule could classify based on the directory you are working in,+ the command one ran, or both. Other shells like zsh can be fixed+ this way too but the exact command may differ; you will need to+ research and experiment.+ </para>+ <para>+ Some programs can be tricky to set. The+ <ulink url="http://feh.finalrewind.org/">X image viewer+ feh</ulink> has a <literal>--title</literal> option but it+ cannot be set in the configuration file,+ <literal>.config/feh/themes</literal>, because it needs to be+ specified dynamically; so you need to set up a shell alias or+ script to wrap the command like+ <literal>feh --title "$(pwd) / %f / %n"</literal>.+ </para>+ </sect3>+ <sect3>+ <title>Raw samples</title>+ <para>+ <literal>xprop</literal> can be tedious to use on every running+ window and you may forget to check seldomly used programs. A better+ approach is to use <literal>arbtt-stats</literal>’s+ <literal>--dump-samples</literal> option: this option will print+ out the collected data for specified time periods, allowing you+ to examine the X properties en masse. This option can be used+ with the <literal>--exclude=</literal>+ option to print the samples for <emphasis>samples not matched+ by existing rules</emphasis> as well, which is indispensable for+ improving coverage and suggesting ideas for new rules. A good+ way to figure out what customizations to make is to run arbtt as+ a daemon for a day or so, and then begin examining the raw+ samples for problems.+ </para>+ <example>+ <title>An initial configuration session</title>+ <para>+ An example: suppose I create a simple category file named+ <literal>foo</literal> with just the line+ </para>+ <programlisting>+$idle > 30 ==> tag inactive+</programlisting>+ <para>+ I can then dump all my arbtt samples for the past day with a+ command like this:+ </para>+ <programlisting>+arbtt-stats --categorizefile=foo --m=0 --filter='$sampleage <24:00' --dump-samples+</programlisting>+ <para>+ Because there are so many open windows, this produces a large+ amount (26586 lines) of hard-to-read output:+ </para>+ <programlisting>+...+( ) Navigator: /r/Touhou's Favorite Arranges! Part 71: Retribution for the Eternal Night ~ Imperishable Night : touhou - Iceweasel+( ) Navigator: Configuring the arbtt categorizer (arbtt-stats) - Iceweasel+( ) evince: ATTACHMENT02+( ) evince: 2009-geisler.pdf — Heart rate variability predicts self-control in goal pursuit+( ) urxvt: /home/gwern; arbtt-stats --categorizefile=foo --m=0 --filter='$sampleage <24:00' --dump-samples+( ) mnemosyne: Mnemosyne+( ) urxvt: /home/gwern; 2014-09-03 13:11:45 xprop+( ) urxvt: /home/gwern; 2014-09-03 13:42:17 history 1 | cut --delimiter=' ' --fields=5-+( ) urxvt: /home/gwern; 2014-09-03 13:12:21 git log -p .emacs+(*) emacs: emacs@elan+( ) urxvt: /home/gwern; 2014-09-01 14:50:30 while true; do cd ~/ && getmail_fetch --ssl pop.gmail.com gwern0 'ugaozoumbhwcijxb' ./mail/; done+( ) urxvt: /home/gwern/blackmarket-mirrors/silkroad2-forums; 2014-08-31 23:20:10 mv /home/gwern/cookies.txt ./; http_proxy="localhost:8118" wget...+( ) urxvt: /home/gwern/blackmarket-mirrors/agora; 2014-08-31 23:15:50 mv /home/gwern/cookies.txt ./; http_proxy="localhost:8118" wget --mirror ...+( ) urxvt: /home/gwern/blackmarket-mirrors/evolution-forums; 2014-08-31 23:04:10 mv ~/cookies.txt ./; http_proxy="localhost:8118" wget --mirror ...+( ) puddletag: puddletag: /home/gwern/music+</programlisting>+ <para>+ Active windows are denoted by an asterisk, so I can focus &+ simplify by adding a pipe like <literal>| fgrep '(*)'</literal>,+ producing more manageable output like+ </para>+ <programlisting>+(*) urxvt: irssi+(*) urxvt: irssi+(*) urxvt: irssi+(*) Navigator: Pyramid of Technology - NextNature.net - Iceweasel+(*) Navigator: Search results - gwern0@gmail.com - Gmail - Iceweasel+(*) Navigator: [New comment] The Wrong Path - gwern0@gmail.com - Gmail - Iceweasel+(*) Navigator: Iceweasel+(*) Navigator: Litecoin Exchange Rate - $4.83 USD - litecoinexchangerate.org - Iceweasel+(*) Navigator: PredictionBook: LiteCoin will trade at >=10 USD per ltc in 2 years, - Iceweasel+(*) urxvt: irssi+(*) Navigator: Bug#691547 closed by Mikhail Gusarov <dottedmag@dottedmag.net> (Re: s3cmd: Man page: --default-mime-type documentation incomplete...)+(*) Navigator: Bug#691547 closed by Mikhail Gusarov <dottedmag@dottedmag.net> (Re: s3cmd: Man page: --default-mime-type documentation incomplete...)+(*) Navigator: Bug#691547 closed by Mikhail Gusarov <dottedmag@dottedmag.net> (Re: s3cmd: Man page: --default-mime-type documentation incomplete...)+(*) urxvt: /home/gwern; 2014-09-02 14:25:17 man s3cmd+(*) evince: bayesiancausality.pdf+(*) evince: bayesiancausality.pdf+(*) puddletag: puddletag: /home/gwern/music+(*) puddletag: puddletag: /home/gwern/music+(*) evince: bayesiancausality.pdf+(*) Navigator: ▶ Umineko no Naku Koro ni Music Box 4 - オルガン小曲 第2億番 ハ短調 - YouTube - Iceweasel+...+</programlisting>+ <para>+ This is better. We can see a few things: the windows all now+ produce enough information to be usefully classified (Gmail can+ be classified under email, irssi can be classified as IRC, the+ urxvt usage can clearly be classified as programming, the PDF+ being read is statistics, etc) in part because of customizations+ to bash/urxvt. The duplication still impedes focus, and we don't+ know what's most common. We can use another pipeline to sort,+ count duplicates, and sort by number of duplicates+ (<literal>| sort | uniq --count | sort --general-numeric-sort</literal>),+ yielding:+ </para>+ <programlisting>+ ...+ 14 (*) Navigator: A Bluer Shade of White Chapter 4, a frozen fanfic | FanFiction - Iceweasel+ 14 (*) Navigator: Iceweasel+ 15 (*) evince: 2009-geisler.pdf — Heart rate variability predicts self-control in goal pursuit+ 15 (*) Navigator: Tool use by animals - Wikipedia, the free encyclopedia - Iceweasel+ 16 (*) Navigator: Hacker News | Add Comment - Iceweasel+ 17 (*) evince: bayesiancausality.pdf+ 17 (*) Navigator: Comments - Less Wrong Discussion - Iceweasel+ 17 (*) Navigator: Keith Gessen · Why not kill them all?: In Donetsk · LRB 11 September 2014 - Iceweasel+ 17 (*) Navigator: Notes on the Celebrity Data Theft | Hacker News - Iceweasel+ 18 (*) Navigator: A Bluer Shade of White Chapter 1, a frozen fanfic | FanFiction - Iceweasel+ 19 (*) gl: mplayer2+ 19 (*) Navigator: Neural networks and deep learning - Iceweasel+ 20 (*) Navigator: Harry Potter and the Philosopher's Zombie, a harry potter fanfic | FanFiction - Iceweasel+ 20 (*) Navigator: [OBNYC] Time tracking app - gwern0@gmail.com - Gmail - Iceweasel+ 25 (*) evince: ps2007.pdf — untitled+ 35 (*) emacs: /home/gwern/arbtt.page+ 43 (*) Navigator: CCC comments on The Octopus, the Dolphin and Us: a Great Filter tale - Less Wrong - Iceweasel+ 62 (*) evince: The physics of information processing superobjects - Anders Sandberg - 1999.pdf — Brains2+ 69 (*) liferea: Liferea+ 82 (*) evince: BMS_raftery.pdf — untitled+ 84 (*) emacs: emacs@elan+ 87 (*) Navigator: overview for gwern - Iceweasel+109 (*) puddletag: puddletag: /home/gwern/music+150 (*) urxvt: irssi+</programlisting>+ <para>+ Put this way, we can see what rules we should write to+ categorize: we could categorize the activities here into a few+ categories of "recreational", "statistics",+ "music", "email", "IRC",+ "research", and "writing"; and add to the+ <literal>categorize.cfg</literal> some rules like thus:+ </para>+ <programlisting>+$idle > 30 ==> tag inactive,++current window $title =~ [/.*Hacker News.*/, /.*Less Wrong.*/, /.*overview for gwern.*/, /.*[fF]an[fF]ic.*/, /.* LRB .*/]+ || current window $program == "liferea" ==> tag Recreation,+current window $title =~ [/.*puddletag.*/, /.*mplayer2.*/] ==> tag Music,+current window $title =~ [/.*[bB]ayesian.*/, /.*[nN]eural [nN]etworks.*/, /.*ps2007.pdf.*/, /.*[Rr]aftery.*/] ==> tag Statistics,+current window $title =~ [/.*Wikipedia.*/, /.*Heart rate variability.*/, /.*Anders Sandberg.*/] ==> tag Research,+current window $title =~ [/.*Gmail.*/] ==> tag Email,+current window $title =~ [/.*arbtt.*/] ==> tag Writing,+current window $title == "irssi" ==> tag IRC,+</programlisting>+ <para>+ If we reran the command, we'd see the same output, so we need to+ leverage our new rules and <emphasis>exclude</emphasis> any+ samples matching our current tags, so now we run a command like:+ </para>+ <programlisting>+arbtt-stats --categorizefile=foo --filter='$sampleage <24:00' --dump-samples --exclude=Recreation --exclude=Music --exclude=Statistics+ --exclude=Research --exclude=Email --exclude=Writing --exclude=IRC |+ fgrep '(*)' | sort | uniq --count | sort --general-numeric-sort+</programlisting>+ <para>+ Now the previous samples disappear, leaving us with a fresh+ batch of unclassified samples to work with:+ </para>+ <programlisting>+ 9 (*) Navigator: New Web Order > Nik Cubrilovic - - Notes on the Celebrity Data Theft - Iceweasel+ 9 ( ) urxvt: /home/gwern; arbtt-stats --categorizefile=foo --filter='$sampleage <24:00' --dump-samples | fgrep '(*)' | less+ 10 (*) evince: ATTACHMENT02+ 10 (*) Navigator: These Giant Copper Orbs Show Just How Much Metal Comes From a Mine | Design | WIRED - Iceweasel+ 12 (*) evince: [Jon_Elster]_Alchemies_of_the_Mind_Rationality_an(BookFi.org).pdf — Alchemies of the mind+ 12 (*) Navigator: Morality Quiz/Test your Morals, Values & Ethics - YourMorals.Org - Iceweasel+ 33 ( ) urxvt: /home/gwern; arbtt-stats --categorizefile=foo --filter='$sampleage <24:00' --dump-samples | fgrep '(*)'...+</programlisting>+ <para>+ We can add rules categorizing these as 'Recreational',+ 'Writing', 'Research', 'Recreational', 'Research', 'Writing',+ and 'Writing' respectively; and we might decide at this point+ that 'Writing' is starting to become overloaded, so we'll split+ it into two tags, 'Writing' and 'Programming'. And then after+ tossing another <literal>--exclude=Programming</literal> into+ our rules, we can repeat the process.+ </para>+ <para>+ As we refine our rules, we will quickly spot instances where the+ title/class/program are insufficient to allow accurate+ classification, and we will figure out the best collection of+ tags for our particular purposes. A few iterations is enough for+ most purposes.+ </para>+ </example>+ </sect3>+ </sect2>+ <sect2>+ <title>Categorizing advice</title>+ <para>+ When building up rules, a few rules of thumb should be kept in+ mind:+ </para>+ <sect3>+ <title>+ Categorize by purpose, not by program+ </title>+ <para>+ This leads to misleading time reports. Avoid, for example,+ lumping all web browser time into a single category named+ 'Internet'; this is more misleading than helpful. Good+ categories describe an activity or goal, such as 'Work' or+ 'Recreation', not a tool, like 'Emacs' or 'Vim'.+ </para>+ </sect3>+ <sect3>+ <title>+ When in doubt, write narrow rules and generalize later+ </title>+ <para>+ Regexps are tricky and it can be easy to write rules far+ broader than one intended. The <literal>--exclude</literal>+ filters mean that one will never see samples which are matched+ accidentally. If one is in doubt, it can be helpful to take a+ specific sample one wants to match and several similar strings+ and look at how well one's regexp rule works in Emacs's+ <ulink url="http://www.emacswiki.org/emacs/ReBuilder">regexp-builder</ulink>+ or online regexp-testers like+ <ulink url="http://regexpal.com/">regexpal</ulink>.+ </para>+ </sect3>+ <sect3>+ <title>+ Don't try to classify everything+ </title>+ <para>+ You will never classify 100% of samples because sometimes+ programs do not include useful X properties and cannot be+ fixed, you have samples from before you fixed them, or they+ are too transient (like popups and dialogues) to be worth+ fixing. It is not necessary to classify 100% of your time,+ since as long as the most common programs and, say,+ <ulink url="https://en.wikipedia.org/wiki/Pareto_principle">80%</ulink>+ of your time is classified, then you have most of the value.+ It is easy to waste more time tweaking arbtt than one gains+ from increased accuracy or more finely-grained tags.+ </para>+ </sect3>+ <sect3>+ <title>+ Avoid large and microscopic tags+ </title>+ <para>+ If a tag takes up more than a third or so of your time, it is+ probably too large, masks variation, and can be broken down into more+ meaningful tags. Conversely, a tag too narrow to show up regularly in+ reports (because it is below the default 1% filter) may not be+ helpful because it is usually tiny, and can be combined with the most+ similar tag to yield more compact and easily interpreted reports.+ </para>+ </sect3>+ </sect2>+ <sect2>+ <title>Long-term storage</title>+ <para>+ Each halving of the sampling rate doubles the number of samples+ taken and hence the storage requirement; sampling rates below 20s+ are probably wasteful. But even the default 60s can accumulate+ into a nontrivial amount of data over a year. A+ constantly-changing binary file can interact poorly with backup+ systems, may make arbtt analyses slower, and if one's system+ occasionally crashes or experiences other problems, cause some+ corruption of the log and be a nuisance in having to run+ <literal>arbtt-recover</literal>.+ </para>+ <para>+ Thus it may be a good idea to archive one's+ <literal>capture.log</literal> on an annual basis. If one needs to+ query the historical data, the particular log file can be+ specified as an option like+ <literal>--logfile=/home/gwern/doc/arbtt/2013-2014.log</literal>+ </para>+ </sect2>+ <sect2 id="external-processing">+ <title>External processing of arbtt statistics</title>+ <para>+ arbtt supports CSV export of time by category in various levels of+ granularity in a 'long' format (multiple rows for each day, with+ <emphasis>n</emphasis> row specifying a category's value for that+ day). These CSV exports can be imported into statistical programs+ like R or Excel and manipulated as desired.+ </para>+ <para>+ R users may prefer to have their time data in a 'wide' format+ (each row is 1 day, with <emphasis>n</emphasis> columns for each+ possible category); this can be done with the+ <literal>reshape</literal> default library. After reading in the+ CSV, the time-intervals can be converted to counts and the data to+ a wide data-frame with R code like the following:+ </para>+ <programlisting>+arbtt <- read.csv("arbtt.csv")+interval <- function(x) { if (!is.na(x)) { if (grepl(" s",x)) as.integer(sub(" s","",x))+ else { y <- unlist(strsplit(x, ":"));+ as.integer(y[[1]])*3600 + as.integer(y[[2]])*60 + as.integer(y[[3]]); }+ }+ else NA+ }+arbtt$Time <- sapply(as.character(arbtt$Time), interval)+library(reshape)+arbtt <- reshape(arbtt, v.names="Time", timevar="Tag", idvar="Day", direction="wide")+</programlisting>+ </sect2>+</sect1>+<sect1 id="contributed">+ <title>Contributed tools</title>+ <para>+ Due to the export facilities of arbtt (as explained in <xref+ linkend="external-processing"/>), tools analyzing arbtt’s data can be+ developed independently. This section lists those that we are aware of. If+ you create a tool of your own, or find one somewhere, pleas tell us on the+ mailing list (see <xref linkend="copyright"/>).+ </para>+ <sect2>+ <title>arbtt-graph</title>+ <para>+ Jayesh Kumar Gupta created a nice d3-based visualization of your arbtt+ data, including daily pie charts and barcode graphs.+ </para>+ <para>+ You can find his tool on <ulink url="https://github.com/rejuvyesh/arbtt-graph">https://github.com/rejuvyesh/arbtt-graph</ulink>.+ </para>+ </sect2>+</sect1> <sect1 id="references"> <title>Program references</title> <para>arbtt consists of a few command line tools, the most important one is@@ -747,9 +1262,9 @@ <term><option>--for-each</option> <replaceable>PERIOD</replaceable></term> <listitem><para>This is not a report of its own, but causes the selected report to be executed for each of the given PERIOD (which can be- day, month or year) where there exist selected samples. All the reports- will then be printed one after another or, in the case of CSV output,- with an additional column.</para>+ minute, hour,day, month or year) where there exist selected+ samples. All the reports will then be printed one after another or,+ in the case of CSV output, with an additional column.</para> <para>Note that if this option is enabled, samples that are filtered out are completely ignored (to avoid empty reports for periods with@@ -913,7 +1428,7 @@ <term><option>-V</option></term> <term><option>--version</option></term> <listitem><simpara>shows the version number, and exists.</simpara></listitem>- </varlistentry>+ </varlistentry> <varlistentry> <term><option>-f</option> <replaceable>FILE</replaceable></term> <term><option>--logfile</option> <replaceable>FILE</replaceable></term>@@ -923,7 +1438,7 @@ <term><option>-t</option> <replaceable>FORMAT</replaceable></term> <term><option>--format</option> <replaceable>FORMAT</replaceable></term> <listitem><simpara>dumping format to use, where <replaceable>FORMAT</replaceable> is one of <literal>human</literal> (the default), <literal>show</literal> or <literal>JSON</literal>. Case in-sensitive.</simpara></listitem>- </varlistentry>+ </varlistentry> <varlistentry> <term><option>-l</option> <replaceable>NUMBER</replaceable></term> <term><option>--last</option> <replaceable>NUMBER</replaceable></term>@@ -1045,7 +1560,7 @@ <term><option>-V</option></term> <term><option>--version</option></term> <listitem><simpara>shows the version number, and exists.</simpara></listitem>- </varlistentry>+ </varlistentry> <varlistentry> <term><option>-f</option> <replaceable>FILE</replaceable></term> <term><option>--logfile</option> <replaceable>FILE</replaceable></term>@@ -1111,7 +1626,7 @@ <term><option>-V</option></term> <term><option>--version</option></term> <listitem><simpara>shows the version number, and exists.</simpara></listitem>- </varlistentry>+ </varlistentry> <varlistentry> <term><option>-i</option></term> <term><option>--infile</option></term>@@ -1194,14 +1709,32 @@ The version history with changes relevant for the user is documented here. </para> + <sect2 id="release-notes-0.9">+ <title>Version 0.9</title>+ <itemizedlist>+ <listitem>+ <para>+ The <option>--for-each</option> option of+ <command>arbtt-stats</command> now supports grouping results by+ minute or hour.+ </para>+ </listitem>+ <listitem>+ <para>+ Gwern Branwen contributed the <xref linkend="effective-use"/>.+ </para>+ </listitem>+ </itemizedlist>+ </sect2>+ <sect2 id="release-notes-0.8.1"> <title>Version 0.8.1</title> <itemizedlist> <listitem>- <para>- The syntax now allows for time differences larger than 99:99.+ <para>+ The syntax now allows for time differences larger than 99:99. (<ulink url="https://bitbucket.org/nomeata/arbtt/issue/14/improve-time-categorization-directives">issue #14</ulink>)- </para>+ </para> </listitem> </itemizedlist> </sect2>@@ -1210,12 +1743,12 @@ <title>Version 0.8</title> <itemizedlist> <listitem>- <para>- <command>arbtt-dump</command> can now show the data in other formats- as well, as suggested by Waldir Pimenta (option- <option>--format</option>). This includes a human-readale output and- JSON.- </para>+ <para>+ <command>arbtt-dump</command> can now show the data in other formats+ as well, as suggested by Waldir Pimenta (option+ <option>--format</option>). This includes a human-readale output and+ JSON.+ </para> </listitem> <listitem> <para>New option <option>--last</option> of <command>arbtt-dump</command>.</para>@@ -1272,9 +1805,9 @@ <title>Version 0.6.4.1</title> <itemizedlist> <listitem>- <para>- Added missing module to the packages.- </para>+ <para>+ Added missing module to the packages.+ </para> </listitem> </itemizedlist> </sect2>@@ -1357,7 +1890,7 @@ <title>Version 0.5 (The ZuriHac-Release)</title> <itemizedlist> <listitem>- <para>New command <command>arbtt-import</command>, which imports the output from <command>arbtt-dump</command>.+ <para>New command <command>arbtt-import</command>, which imports the output from <command>arbtt-dump</command>. </para> </listitem> <listitem>@@ -1376,14 +1909,14 @@ </listitem> <listitem> <para>- Unicode is handled correctly in regular expressions.- </para>+ Unicode is handled correctly in regular expressions.+ </para> </listitem> <listitem> <para>- Improved date-handling functions for <filename>categorize.cfg</filename>.+ Improved date-handling functions for <filename>categorize.cfg</filename>. (<xref linkend="sergey"/>)- </para>+ </para> </listitem> </itemizedlist> </sect2>@@ -1393,7 +1926,7 @@ <itemizedlist> <listitem> <para>Bugfix: Added missing modules to the cabal file.- </para>+ </para> </listitem> </itemizedlist> </sect2>@@ -1403,7 +1936,7 @@ <itemizedlist> <listitem> <para>Implement a custom compression method greatly reduce the file size of the log file. Run <command>arbtt-capture</command> to compress the previous samples as well.- </para>+ </para> </listitem> </itemizedlist> </sect2>@@ -1438,11 +1971,11 @@ <command>arbtt-dump</command>.</para> </listitem> <listitem>- <para>New command <command>arbtt-recover</command> to rescue data from- a proken data log file.</para>+ <para>New command <command>arbtt-recover</command> to rescue data from+ a proken data log file.</para> </listitem> <listitem>- <para>Actually include this documentation in the released tarball.</para>+ <para>Actually include this documentation in the released tarball.</para> </listitem> </itemizedlist> </sect2>
src/Data/MyText.hs view
@@ -1,6 +1,7 @@ module Data.MyText where import qualified Data.ByteString.UTF8 as BSU+import qualified Data.ByteString.Lazy.UTF8 as BLU import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString as BS import Data.Binary.Get@@ -27,17 +28,22 @@ -- Binary instance compatible with Binary String instance Binary Text where put = put . unpack+ get = pack <$> get -- The following code exploits that the Binary Char instance uses UTF8 as well -- The downside is that it quietly suceeds for broken input+ -- Unfortunately, with binary-0.7, it is no longer possible to implement+ -- this nice and lazily, so go via String :-(+ {- get = do n <- get :: Get Int- r <- remaining- bs <- lookAhead (getByteString (min (fromIntegral r) (4*n))) -- safe approximation- let utf8bs = BSU.take n bs- unless (BSU.length utf8bs == n) $+ bs <- lookAhead $ getRemainingLazyByteString+ let utf8bs = BLU.take (fromIntegral n) bs+ unless (BLU.length utf8bs == n) $ fail $ "Coult not parse the expected " ++ show n ++ " utf8 characters."- skip (BS.length utf8bs)- return $ Text utf8bs+ let sbs = LBS.toStrict utf8bs+ skip $ BS.length sbs+ return $ Text sbs+ -} {- Possible speedup with a version of binary that provides access to the internals, as the Char instance is actually UTF8, but the length bit is
src/Stats.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE RecordWildCards, NamedFieldPuns, TypeOperators, TupleSections #-}+{-# LANGUAGE RecordWildCards, NamedFieldPuns, TypeOperators, TupleSections, GADTSyntax, ExistentialQuantification #-} module Stats ( Report(..), ReportOptions(..),@@ -57,7 +57,7 @@ data ActivityFilter = ExcludeActivity ActivityMatcher | OnlyActivity ActivityMatcher deriving (Show, Eq) -data Repeater = ByDay | ByMonth | ByYear+data Repeater = ByMinute | ByHour | ByDay | ByMonth | ByYear deriving (Show, Eq) -- Supported report output formats: text, comma-separated values and@@ -174,19 +174,40 @@ let go' m act = M.insertWith' (+) act (fromInteger (tlRate tl)/1000) m in foldl' go' m (snd (tlData tl))) id -processRepeater :: Repeater -> LeftFold (Bool :!: TimeLogEntry (Ctx, ActivityData)) ReportResults -> LeftFold (Bool :!: TimeLogEntry (Ctx, ActivityData)) ReportResults-processRepeater ByDay rep =- filterElems (\(b :!: _) -> b) $- pure (RepeatedReportResults "Day" . map (\(d,rr) -> (showGregorian d, rr)) . M.toList) <*>- multiplex (utctDay . tlTime . Strict.snd) rep-processRepeater ByMonth rep =- filterElems (\(b :!: _) -> b) $- pure (RepeatedReportResults "Month" . map (\((y,m),rr) -> (show y ++ "-" ++ show m, rr)) . M.toList) <*>- multiplex ((\(y,m,_) -> (y, m)). toGregorian . utctDay . tlTime . Strict.snd) rep-processRepeater ByYear rep =- filterElems (\(b :!: _) -> b) $- pure (RepeatedReportResults "Year" . map (\(y,rr) -> (show y, rr)) . M.toList) <*>- multiplex ((\(y,_,_) -> y). toGregorian . utctDay . tlTime . Strict.snd) rep+processRepeater :: TimeZone -> Repeater -> LeftFold (Bool :!: TimeLogEntry (Ctx, ActivityData)) ReportResults -> LeftFold (Bool :!: TimeLogEntry (Ctx, ActivityData)) ReportResults+processRepeater tz r rep = case repeaterImpl r of+ RepeaterImpl catR showR ->+ filterElems (\(b :!: _) -> b) $+ pure (RepeatedReportResults (repeaterTitle r) . map (first showR) . M.toList) <*>+ multiplex (catR . utcToLocalTime tz . tlTime . Strict.snd) rep++data RepeaterImpl where+ RepeaterImpl :: Ord r => (LocalTime -> r) -> (r -> String) -> RepeaterImpl++repeaterTitle :: Repeater -> String+repeaterTitle ByMinute = "Minute"+repeaterTitle ByHour = "Hour"+repeaterTitle ByDay = "Day"+repeaterTitle ByMonth = "Month"+repeaterTitle ByYear = "Year"++repeaterImpl :: Repeater -> RepeaterImpl+repeaterImpl ByMinute = RepeaterImpl+ -- a somewhat lazy implementations, using strings...+ (formatTime defaultTimeLocale "%F %H:%M")+ id+repeaterImpl ByHour = RepeaterImpl+ (formatTime defaultTimeLocale "%F %H:00")+ id+repeaterImpl ByDay = RepeaterImpl+ localDay+ showGregorian+repeaterImpl ByMonth = RepeaterImpl+ ((\(y,m,_) -> (y, m)) . toGregorian . localDay)+ (\(y,m) -> show y ++ "-" ++ show m)+repeaterImpl ByYear = RepeaterImpl+ ((\(y,_,_) -> y) . toGregorian . localDay)+ show processReport :: ReportOptions -> Report -> LeftFold (Bool :!: TimeLogEntry (Ctx, ActivityData)) ReportResults processReport opts GeneralInfos =
src/TimeLog.hs view
@@ -105,13 +105,13 @@ return $ parseTimeLog content parseTimeLog :: (NFData a, ListOfStringable a) => BS.ByteString -> TimeLog a-parseTimeLog input = +parseTimeLog input = if startString == magic then go Nothing rest off else error $ "Timelog starts with unknown marker " ++ show (map (chr.fromIntegral) (BS.unpack startString))- where + where (startString, rest, off) = runGetState (getLazyByteString (BS.length magic)) input 0 go prev input off = let (v, rest, off') = runGetState (ls_get strs) input off
src/stats-main.hs view
@@ -18,6 +18,7 @@ import System.ProgressBar import TermSize import qualified Data.MyText as T+import Data.Time.LocalTime import TimeLog import Categorize@@ -48,7 +49,7 @@ , optLogFile = dir </> "capture.log" , optCategorizeFile = dir </> "categorize.cfg" }- + versionStr, header :: String versionStr = "arbtt-stats " ++ showVersion version header = "Usage: arbtt-stats [OPTIONS...]"@@ -91,7 +92,7 @@ "only consider samples matching the condition" , Option "m" ["min-percentage"] (ReqArg (\arg opt -> let ro = (optReportOptions opt) { roMinPercentage = read arg}- in return opt { optReportOptions = ro }) "COND")+ in return opt { optReportOptions = ro }) "PERC") "do not show tags with a percentage lower than PERC% (default: 1)" , Option "" ["output-exclude"] (ReqArg (\arg opt -> let filters = ExcludeActivity (parseActivityMatcher arg) : roActivityFilter (optReportOptions opt)@@ -141,10 +142,12 @@ readRepeater :: String -> Repeater readRepeater arg = case map toLower arg of- "day" -> ByDay- "month" -> ByMonth- "year" -> ByYear- _ -> error ("Unsupported parameter to --for-each: '" ++ arg ++ "'")+ "minute" -> ByMinute+ "hour" -> ByHour+ "day" -> ByDay+ "month" -> ByMonth+ "year" -> ByYear+ _ -> error ("Unsupported parameter to --for-each: '" ++ arg ++ "'") readReportFormat :: String -> ReportFormat readReportFormat arg =@@ -163,6 +166,7 @@ (_,_,errs) -> do hPutStr stderr (concat errs ++ usageInfo header options) exitFailure+ tz <- getCurrentTimeZone dir <- getAppUserDataDirectory "arbtt" flags <- foldl (>>=) (return (defaultOptions dir)) actions@@ -175,12 +179,12 @@ categorizer <- readCategorizer (optCategorizeFile flags) timelog <- BS.readFile (optLogFile flags)- size <- fileSize <$> getFileStatus (optLogFile flags) isTerm <- hIsTerminalDevice stderr trackedTimelog <- case isTerm of True -> do hSetBuffering stderr NoBuffering+ size <- fileSize <$> getFileStatus (optLogFile flags) trackProgressWithChunkSize (fromIntegral size `div` 100) (\_ b -> do (_height, width) <- getTermSize hPutChar stderr '\r'@@ -195,28 +199,26 @@ let captures = parseTimeLog trackedTimelog let allTags = categorizer captures+ when (null allTags) $ do putStrLn "Nothing recorded yet" exitFailure- + let filters = (if optAlsoInactive flags then id else (defaultFilter:)) $ optFilters flags let rep = case optReports flags of [] -> TotalTime [x] -> x _ -> error "Please specify exactly one report to generate"- let repeater = foldr (.) id $ map processRepeater (optRepeater flags)+ let repeater = foldr (.) id $ map (processRepeater tz) (optRepeater flags) - -- These are defined here, but of course only evaluated when any report- -- refers to them. Some are needed by more than one report, which is then- -- advantageous. let opts = optReportOptions flags let fold = filterPredicate filters `adjoin` repeater (processReport opts rep) let result = runLeftFold fold allTags -- Force the results a bit, to ensure the progress bar to be shown before the title result `seq` return ()- + renderReport opts result {-