Somehow LibreOffice doesn’t work without the following:
+
exportGIO_EXTRA_MODULES=""
+
Other package managers
+
Using other package managers with Guix requires some extra work.
+
Cask
+
if[ -d "$HOME/.cask"]; then
+exportPATH="/home/pavel/.cask/bin:$PATH"
+fi
+
Make flatpak apps visible to launchers:
+
if[ -d "$HOME/.local/share/flatpak"]; then
+exportXDG_DATA_DIRS="$XDG_DATA_DIRS:$HOME/.local/share/flatpak/exports/share"
+fi
+
Enable Nix
+
if[ -f /run/current-system/profile/etc/profile.d/nix.sh ]; then
+ . /run/current-system/profile/etc/profile.d/nix.sh
+fi
+
Use Guix fontconfig. Necessary for nix apps
+
if[ -d "$HOME/.guix-extra-profiles/desktop-misc"]; then
+exportFONTCONFIG_PATH="$HOME/.guix-extra-profiles/desktop-misc/desktop-misc/etc/fonts"
+fi
+
Make nix apps visible to launchers:
+
if[ -d "$HOME/.nix-profile"]; then
+exportXDG_DATA_DIRS="$XDG_DATA_DIRS:$HOME/.nix-profile/share/applications"
+fi
+
npm
+
npm is especially cumbersome, for instance because by default it tries to install packages to /gnu/store/.
+
In principle, one can set a prefix like this:
+
prefix=/home/pavel/.npm-packages
+
But I also want to use node from conda occasionally, where prefix is already set correctly. So instead of tangling the above to the ~/.npmrc directly, I set an environment variable in the profile:
if[ -z "$IS_ANDROID"]; then
+ xrdb ~/.Xresources
+fi
+
OFF (OFF) Package manager paths
+
Turned off for now, because probably it won’t be necessary in Guix.
+
LaTeX
+
if[ -d "/usr/local/texlive/2020"]; then
+exportMANPATH="/usr/local/texlive/2020/texmf-dist/doc/man:$MANPATH"
+exportINFOPATH="/usr/local/texlive/2020/texmf-dist/doc/info:$INFOPATH"
+exportPATH="/usr/local/texlive/2020/bin/x86_64-linux:$PATH"
+fi
+
Cargo (Rust)
+
if[ -d "$HOME/.cargo"] ; then
+exportPATH="$HOME/.cargo/bin:$PATH"
+fi
+
RVM (Ruby)
+
if[ -d "$HOME/.rvm"] ; then
+exportPATH="$PATH:$HOME/.rvm/bin"
+fi
+# if [ -d "$HOME/.gem" ]; then
+# export PATH="$HOME/.gem/ruby/2.7.0/bin:$PATH"
+# fi
+
Go
+
if[ -d "$HOME/go"] ; then
+exportPATH="$HOME/go/bin:$PATH"
+fi
+
My .bashrc, which has pieces from the default one in Guix & Manjaro, as well some mine settings.
+
Startup & environment
+
Export ‘SHELL’ to child processes. Programs such as ‘screen’ honor it and otherwise use /bin/sh.
+
export SHELL
+
We are being invoked from a non-interactive shell. If this is an SSH session (as in “ssh host command”), source /etc/profile, so we get PATH and other essential variables.
complete -cf sudo # Sudo autocompletion
+
+shopt -s checkwinsize # Check windows size after each command
+shopt -s expand_aliases # Aliases
+shopt -s autocd # Cd to directory just by typing its name (without cd)
+
if[[ ! -z "$SIMPLE"]]; then
+unalias ls
+aliasll="ls -lah"
+fi
+
Micromamba
+
I’ve moved from conda to micromamba because it’s faster.
+
+
managed by ‘mamba init’ !!!
+
+
Yeah, tell this to yourself
+
init_mamba (){
+exportMAMBA_EXE="/home/pavel/.guix-extra-profiles/dev/dev/bin/micromamba";
+exportMAMBA_ROOT_PREFIX="/home/pavel/micromamba";
+__mamba_setup="$("$MAMBA_EXE" shell hook --shell bash --prefix "$MAMBA_ROOT_PREFIX" 2> /dev/null)"
+if[$? -eq 0]; then
+eval"$__mamba_setup"
+else
+if[ -f "/home/pavel/micromamba/etc/profile.d/micromamba.sh"]; then
+ . "/home/pavel/micromamba/etc/profile.d/micromamba.sh"
+else
+exportPATH="/home/pavel/micromamba/bin:$PATH"# extra space after export prevents interference from conda init
+fi
+fi
+unset __mamba_setup
+}
+
+if[[ ! -z "$INIT_MAMBA"]]; then
+ init_mamba
+fi
+
Starship
+
if[[ -z "$SIMPLE"&&"$TERM" !="dumb"]]; then
+eval"$(starship init bash)"
+fi
+
Yandex Cloud
+
init_yc (){
+# The next line updates PATH for Yandex Cloud CLI.
+if[ -f '/home/pavel/yandex-cloud/path.bash.inc']; thensource'/home/pavel/yandex-cloud/path.bash.inc'; fi
+
+# The next line enables shell command completion for yc.
+if[ -f '/home/pavel/yandex-cloud/completion.bash.inc']; thensource'/home/pavel/yandex-cloud/completion.bash.inc'; fi
+}
+
Fish
+
+
+
+
Guix dependency
+
Description
+
+
+
+
+
fish
+
An alternative non POSIX-compliant shell
+
+
+
+
Fish shell is a non-POSIX-compliant shell, which offers some fancy UI & UX features.
Then, check if launched from Emacs with environment activated.
+
# if test -n "$EMACS_CONDA_ENV";
+# conda activate $EMACS_CONDA_ENV
+# end
+
Colors
+
Fish seems to have hardcoded colorcodes in some color settings. I set these to base16 colors, so they would match Xresources.
+
setfish_color_command cyan
+setfish_color_comment green
+setfish_color_end black
+setfish_color_error red
+setfish_color_escape yellow
+setfish_color_operator yellow
+setfish_color_param magenta
+setfish_color_quote green
+setfish_color_redirection yellow
+
bind h select-pane -L
+bind j select-pane -D
+bind k select-pane -U
+bind l select-pane -R
+
+bind s split-window
+bind v split-window -h
+
+bind-key n new-window
+bind-key t next-window
+bind-key T previous-window
+
Alacritty is a GPU-accelerated terminal emulator. I haven’t found it to be an inch faster than st, but yml configuration is way more convenient than patches.
A script to perform automatic commits in a repository. I use it to sync my org directory and password store. I guess it’s not how git is intended to be used, but it works for me.
+
Usage:
+
autocommit <repository> [-F]
+
Environment:
+
+
+
+
Variable
+
Description
+
Default value
+
+
+
+
+
TIMEOUT_MIN
+
Default timeout
+
60
+
+
+
+
Here’s roughly what the script is doing:
+
+
If there is a merge conflict, notify
+
If there were changed files in the last TIMEOUT_MIN minutes, commit
+
Fetch
+
If there were changes in the last TTMEOUT_MIN, merge (usually the merge is just fast-forward)
+
If the fetch was successful & the merge was either successful or delayed because of changes in the last TIMEOUT_MIN, push
+
Send a notification about the events above
+
Send a separate notification if there is a merge conflict
Parts prefixed with (OFF) are not used, but kept for historic purposes. For some reason GitHub’s org renderer ignores TODO status, hence such a prefix. Round brackets instead of square ones to prevent GitHub’s org renderer from screwing up.
Run xrdb -load ~/.Xresources to apply the changes.
+
Fonts
+
Also, Xresources are used to set Xft settings. Unfortunately, the DPI setting has to be unique for each machine, which means I cannot commit Xresources to the repo.
First things first, Emacs has to be launched as a window manager. On a more conventional system I’d create a .desktop file in some system folder that can be seen by a login manager, but in the case of Guix it’s a bit more complicated, because all such folders are not meant to be changed manually.
+
+
+
+
Category
+
Guix dependency
+
+
+
+
+
desktop-misc
+
xinit
+
+
+
desktop-misc
+
xss-lock
+
+
+
+
However, GDM, the login manager that seems to be the default on Guix, launches ~/.xsession on the startup if it’s present, which is just fine for my purposes.
+
# Source .profile
+. ~/.profile
+
+# Disable access control for the current user
+xhost +SI:localuser:$USER
+
+# Fix for Java applications
+export_JAVA_AWT_WM_NONREPARENTING=1
+
+# Apply XResourses
+xrdb -merge ~/.Xresources
+
+# Turn off the system bell
+xset -b
+
+# Use i3lock as a screen locker
+xss-lock -- i3lock -f -i /home/pavel/Pictures/lock-wallpaper.png &
+
+# Some apps that have to be launched only once.
+picom &
+# nm-applet &
+dunst &
+copyq &
+
+# Run the Emacs startup script as a session.
+# exec dbus-launch --exit-with-session ~/.emacs.d/run-exwm.sh
+exec dbus-launch --exit-with-session emacs -mm --debug-init -l ~/.emacs.d/desktop.el
+
Startup apps
+
Now that Emacs is launched, it is necessary to set up the EXWM-specific parts of config.
+
I want to launch some apps from EXWM instead of the Xsession file for two purposes:
+
+
the app may need to have the entire desktop environment set up
+
or it may need to be restarted if Emacs is killed.
A bunch of functions related to managing windows in EXWM.
+
Moving windows
+
As I wrote in my Emacs and i3 post, I want to have a rather specific behavior when moving windows (which does resemble i3 in some way):
+
+
if there is space in the required direction, move the Emacs window there;
+
if there is no space in the required direction, but space in two orthogonal directions, move the Emacs window so that there is no more space in the orthogonal directions;
+
+
I can’t say it’s better or worse than the built-in functionality or one provided by evil, but I’m used to it and I think it fits better for managing a lot of windows.
+
So, first, we need a predicate that checks whether there is space in the given direction:
+
(require'windmove)
+
+(defunmy/exwm-direction-exists-p (dir)
+"Check if there is space in the direction DIR.
+
+Does not take the minibuffer into account."
+ (cl-some (lambda (dir)
+ (let ((win (windmove-find-other-windowdir)))
+ (andwin (not (window-minibuffer-pwin)))))
+ (pcasedir
+ ('width'(leftright))
+ ('height'(updown)))))
+
And a function to implement that:
+
(defunmy/exwm-move-window (dir)
+"Move the current window in the direction DIR."
+ (let ((other-window (windmove-find-other-windowdir))
+ (other-direction (my/exwm-direction-exists-p
+ (pcasedir
+ ('up'width)
+ ('down'width)
+ ('left'height)
+ ('right'height)))))
+ (cond
+ ((andother-window (not (window-minibuffer-pother-window)))
+ (window-swap-states (selected-window) other-window))
+ (other-direction
+ (evil-move-windowdir)))))
+
My preferred keybindings for this part are, of course, s-<H|J|K|L>.
+
Resizing windows
+
I find this odd that there are different commands to resize tiling and floating windows. So let’s define one command to perform both resizes depending on the context:
+
(setqmy/exwm-resize-value5)
+
+(defunmy/exwm-resize-window (dirkind&optionalvalue)
+"Resize the current window in the direction DIR.
+
+DIR is either 'height or 'width, KIND is either 'shrink or
+ 'grow. VALUE is `my/exwm-resize-value' by default.
+
+If the window is an EXWM floating window, execute the
+corresponding command from the exwm-layout group, execute the
+command from the evil-window group."
+ (unlessvalue
+ (setqvaluemy/exwm-resize-value))
+ (let* ((is-exwm-floating
+ (and (derived-mode-p'exwm-mode)
+exwm--floating-frame))
+ (func (ifis-exwm-floating
+ (intern
+ (concat
+"exwm-layout-"
+ (pcasekind ('shrink"shrink") ('grow"enlarge"))
+"-window"
+ (pcasedir ('height"") ('width"-horizontally"))))
+ (intern
+ (concat
+"evil-window"
+ (pcasekind ('shrink"-decrease-") ('grow"-increase-"))
+ (symbol-namedir))))))
+ (whenis-exwm-floating
+ (setqvalue (*5value)))
+ (funcallfuncvalue)))
+
This function will call exwm-layout-<shrink|grow>[-horizontally] for EXWM floating window and evil-window-<decrease|increase>-<width|height> otherwise.
+
This function can be bound to the required keybindings directly, but I prefer a hydra to emulate the i3 submode:
M-x evil-window-[v]split (bound to C-w v and C-w s by default) are the default evil command to do splits.
+
One EXWM-related issue though is that by default doing such a split “copies” the current buffer to the new window. But as EXWM buffer cannot be “copied” like that, some other buffer is displayed in the split, and generally, that’s not a buffer I want.
+
For instance, I prefer to have Chrome DevTools as a separate window. When I click “Inspect” on something, the DevTools window replaces my Ungoogled Chromium window. I press C-w v, and most often I have something like *scratch* buffer in the opened split instead of the previous Chromium window.
+
To implement better behavior, I define the following advice:
+
(defunmy/exwm-fill-other-window (&rest_)
+"Open the most recently used buffer in the next window."
+ (interactive)
+ (when (and (eqmajor-mode'exwm-mode) (not (eq (next-window) (get-buffer-window))))
+ (let ((other-exwm-buffer
+ (cl-loopwithother-buffer= (persp-other-buffer)
+forbufin (sort (persp-current-buffers) (lambda (a_) (eqaother-buffer)))
+withcurrent-buffer= (current-buffer)
+when (and (not (eqcurrent-bufferbuf))
+ (buffer-live-pbuf)
+ (not (string-match-p (persp--make-ignore-buffer-rx) (buffer-namebuf)))
+ (not (get-buffer-windowbuf)))
+returnbuf)))
+ (whenother-exwm-buffer
+ (with-selected-window (next-window)
+ (switch-to-bufferother-exwm-buffer))))))
+
This is meant to be called after doing an either vertical or horizontal split, so it’s advised like that:
This works as follows. If the current buffer is an EXWM buffer and there are other windows open (that is, (next-window) is not the current window), the function tries to find another suitable buffer to be opened in the split. And that also takes the perspectives into account, so buffers are searched only within the current perspective, and the buffer returned by persp-other-buffer will be the top candidate.
+
Perspectives
+
perspective.el is one package I like that provides workspaces for Emacs, called “perspectives”. Each perspective has a separate buffer list, window layout, and a few other things that make it easier to separate things within Emacs.
+
One feature I’d like to highlight is integration between perspective.el and treemacs, where one perspective can have a separate treemacs tree. Although now tab-bar.el seems to be getting into shape to compete with perspective.el, as of the time of this writing, there’s no such integration, at least not out of the box.
+
perspective.el works with EXWM more or less as one would expect - each EXWM workspace has its own set of perspectives. That way it feels somewhat like having multiple Emacs frames in a tiling window manager, although, of course, much more integrated with Emacs.
+
However, there are still some issues. For instance, I was having strange behaviors with floating windows, EXWM buffers in perspectives, etc. So I’ve made a package called perspective-exwm.el that does two things:
+
+
Advices away the issues I had. Take a look at the package homepage for more detail on that.
+
Provides some additional functionality that makes use of both perspective.el and EXWM.
By default, a new Emacs buffer opens in the current perspective in the current workspace, but sure enough, it’s possible to change that.
+
For EXWM windows, the perspective-exwm package provides a function called perspective-exwm-assign-window, which is intended to be used in exwm-manage-finish-hook, for instance:
using the same commands to switch between windows and monitors.
+
+
Here’s my take on implementing them.
+
Tracking recently used workspaces
+
First up though, we need to track the workspaces in the usage order. I’m not sure if there’s some built-in functionality in EXWM for that, but it seems simple enough to implement.
+
Here is a snippet of code that does it:
+
(setqmy/exwm-last-workspaces'(1))
+
+(defunmy/exwm-store-last-workspace ()
+"Save the last workspace to `my/exwm-last-workspaces'."
+ (setqmy/exwm-last-workspaces
+ (seq-uniq (consexwm-workspace-current-index
+my/exwm-last-workspaces))))
+
+(add-hook'exwm-workspace-switch-hook
+#'my/exwm-store-last-workspace)
+
The variable my/exwm-last-workspaces stores the workspace indices; the first item is the index of the current workspace, the second item is the index of the previous workspace, and so on.
+
One note here is that workspaces may also disappear (e.g. after M-x exwm-workspace-delete), so we also need a function to clean the list:
The second piece of the puzzle is getting the monitor list in the right order.
+
While it is possible to retrieve the monitor list from exwm-randr-workspace-output-plist, this won’t scale well beyond two monitors, mainly because changing this variable may screw up the order.
+
So the easiest way is to just define the variable like that:
If you are changing the RandR configuration on the fly, this variable will also need to be changed, but for now, I don’t have such a necessity.
+
A function to get the current monitor:
+
(defunmy/exwm-get-current-monitor ()
+"Return the current monitor name or nil."
+ (plist-getexwm-randr-workspace-output-plist
+ (cl-position (selected-frame)
+exwm-workspace--list)))
+
And a function to cycle the monitor list in either direction:
+
(defunmy/exwm-get-other-monitor (dir)
+"Cycle the monitor list in the direction DIR.
+
+DIR is either 'left or 'right."
+ (nth
+ (% (+ (cl-position
+ (my/exwm-get-current-monitor)
+my/exwm-monitor-list
+:test#'string-equal)
+ (lengthmy/exwm-monitor-list)
+ (pcasedir
+ ('right1)
+ ('left-1)))
+ (lengthmy/exwm-monitor-list))
+my/exwm-monitor-list))
+
Switch to another monitor
+
With the functions from the previous two sections, we can implement switching to another monitor by switching to the most recently used workspace on that monitor.
+
One caveat here is that on the startup the my/exwm-last-workspaces variable won’t have any values from other monitor(s), so this list is concatenated with the list of available workspace indices.
This is actually quite easy to pull off - one just has to update exwm-randr-workspace-monitor-plist accordingly and run exwm-randr-refresh. I just add another check there because I don’t want some monitor to remain without workspaces at all.
+
(defunmy/exwm-workspace-switch-monitor ()
+"Move the current workspace to another monitor."
+ (interactive)
+ (let ((new-monitor (my/exwm-get-other-monitor'right))
+ (current-monitor (my/exwm-get-current-monitor)))
+ (when (andcurrent-monitor
+ (>=1
+ (cl-loopfor (keyvalue) onexwm-randr-workspace-monitor-plist
+by'cddr
+if (string-equalvaluecurrent-monitor) sum1)))
+ (error"Can't remove the last workspace on the monitor!"))
+ (setqexwm-randr-workspace-monitor-plist
+ (map-deleteexwm-randr-workspace-monitor-plistexwm-workspace-current-index))
+ (whennew-monitor
+ (setqexwm-randr-workspace-monitor-plist
+ (plist-putexwm-randr-workspace-monitor-plist
+exwm-workspace-current-index
+new-monitor))))
+ (exwm-randr-refresh))
+
In my configuration this is bound to s-<tab>.
+
Windmove between monitors
+
And the final (for now) piece of the puzzle is using the same command to switch between windows and monitors. E.g. when the focus is on the right-most window on one monitor, I want the command to switch to the left-most window on the monitor to the right instead of saying “No window right from the selected window”, as windmove-right does.
+
So here is my implementation of that. It always does windmove-do-select-window for 'down and 'up. For 'right and 'left though, the function calls the previously defined function to switch to other monitor if windmove-find-other-window doesn’t return anything.
+
(defunmy/exwm-windmove (dir)
+"Move to window or monitor in the direction DIR."
+ (if (or (eqdir'down) (eqdir'up))
+ (windmove-do-window-selectdir)
+ (let ((other-window (windmove-find-other-windowdir))
+ (other-monitor (my/exwm-get-other-monitordir))
+ (opposite-dir (pcasedir
+ ('left'right)
+ ('right'left))))
+ (ifother-window
+ (windmove-do-window-selectdir)
+ (let ((mouse-autoselect-windownil))
+ (my/exwm-switch-to-other-monitordir))
+ (cl-loopwhile (windmove-find-other-windowopposite-dir)
+do (windmove-do-window-selectopposite-dir))))))
+
Completions
+
Setting up some completion interfaces that fit particularly well to use with EXWM. While rofi also works, I want to use Emacs functionality wherever possible to have one completion interface everywhere.
+
ivy-posframe
+
ivy-posframe is an extension to show ivy candidates in a posframe.
+
Take a look at this issue in the EXWM repo about setting it up.
+
Edit [2022-04-09 Sat]: This looks nice, but unfortunately too unstable. Disabling it.
emojify is an Emacs package that adds emoji display to Emacs. While its primary capacity is no longer necessary in Emacs 28, it a few functions to insert emojis are still handy.
+
(use-packageemojify
+:straightt)
+
Because I occasionally want to type emojis to other programs, I reuse a function from password-store-ivy:
Setting keybindings for EXWM. This actually has to be in the :config block of the use-package form, that is it has to be run after EXWM is loaded, so I use noweb to put this block in the correct place.
+
First, some prefixes for keybindings that are always passed to EXWM instead of the X application in line-mode:
(defunmy/exwm-quit ()
+ (interactive)
+ (when (or (not (eq (selected-window) (next-window)))
+ (y-or-n-p"This is the last window. Are you sure?"))
+ (evil-quit)))
+
And keybindings that are available in both char-mode and line-mode:
It seems like this strange commit: c90ac4 breaks focusing on an X frame when switching to a workspace, at least on Emacs <= 28. This reverts to the previous version.
i3lock is disabled because the global one has to be used.
+
i3wm is a manual tiling window manager, which is currently my window manager of choice. I’ve tried several alternatives, including xmonad & EXWM, but i3 seems to fit my workflow best and decided to switch to EXWM. This section is kept for a few cases when I need to be extra sure that my WM doesn’t fail.
+
i3-gaps is an i3 fork with a few features like window gaps. I like to enable inner gaps when there is at least one container in a workspace.
set $mod Mod4
+font pango:monospace 10
+
+# Use Mouse+$mod to drag floating windows to their wanted position
+floating_modifier $mod
+
+# Move cursor between monitors
+mouse_warping output
+
+# Apply XFCE Settings
+# exec xfsettingsd
+# exec xiccd
+
+# Set screen layout
+exec ~/bin/scripts/screen-layout
+
+# Most needed keybindigs
+# reload the configuration file
+bindsym $mod+Shift+c reload
+
+# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
+bindsym $mod+Shift+r restart
+
+# exit i3 (logs you out of your X session)
+bindsym $mod+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'"
+
Managing windows
+
+
+
+
Guix dependency
+
+
+
+
+
rust-i3-switch-tabs
+
+
+
+
Some keybindings for managing windows.
+
emacs-i3-integration is a script to pass some command to Emacs to get a consistent set of keybindings in both i3 and Emacs. Check out the section in Emacs.org for details.
Rules to automatically assign applications to workspaces and do other stuff, like enable floating.
+
Most apps can be distinguished by a WM class (you can get one with xprop), but in some cases it doesn’t work, e.g. for terminal applications. In that case rules can be based on a window title, for instance.
+
However, watch out for the following: rule such as for_window [title="ncmpcpp.*"] move to workspace $w9 will move any window with a title starting with ncmpcpp to workspace $w9. For instance, it moves your browser when you google “ncmpcpp”.
First of all, we have to distinguish a scratchpad terminal from a normal one. To do that, one can create st with a required classname.
+
Then, it would be cool not to duplicate scratchpads, so the following script first looks for a window with a created classname. If it exists, the script just toggles the scratchpad visibility. Otherwise, a new instance of a window is created.
mode "inner gaps" {
+ bindsym plus gaps inner current plus 5
+ bindsym minus gaps inner current minus 5
+ bindsym Shift+plus gaps inner all plus 5
+ bindsym Shift+minus gaps inner all minus 5
+ bindsym 0 gaps inner current set0
+ bindsym Shift+0 gaps inner all set0
+
+ bindsym r gaps inner current set $default_inner
+ bindsym Shift+r gaps inner all set $default_inner
+
+ bindsym Return mode "default"
+ bindsym Escape mode "default"
+}
+
+mode "outer gaps" {
+ bindsym plus gaps outer current plus 5
+ bindsym minus gaps outer current minus 5
+ bindsym Shift+plus gaps outer all plus 5
+ bindsym Shift+minus gaps outer all minus 5
+ bindsym 0 gaps outer current set0
+ bindsym Shift+0 gaps outer all set0
+
+ bindsym r gaps outer current set $default_outer
+ bindsym Shift+r gaps outer all set $default_outer
+
+ bindsym Return mode "default"
+ bindsym Escape mode "default"
+}
+
+bindsym $mod+g mode "inner gaps"
+bindsym $mod+Shift+g mode "outer gaps"
+
Move & resize windows
+
+
+
+
Guix dependency
+
+
+
+
+
python-i3-balance-workspace
+
+
+
+
A more or less standard set of keybindings to move & resize floating windows. Just be careful to always make a way to return from these new modes, otherwise you’d end up in a rather precarious situation.
+
i3-balance-workspace is a small Python package to balance the i3 windows, but for the Emacs integration I also want this button to balance the Emacs windows, so here is a small script to do just that.
This is the most crazy advanced piece of my literate configuration so far.
+
My polybar has:
+
+
colors from the general color theme;
+
powerline-ish decorations between modules.
+
+
Colors
+
The “colors” part is straightforward enough. Once upon the time it was so…
+
Polybar can use Xresources, but the problem with that is you’re supposed to use colorX as foreground, not as background. This usually works fine with dark themes from doom-themes, but not so much with high-contrast modus-themes.
As for the module decorations though, I find it ironic that with all this fancy rendering around I have to resort to Unicode glyphs.
+
Anyhow, the approach is to put a glyph between two blocks like this:
+
block1 block2
+
And set the foreground and background colors like that:
+
+
+
+
+
block1
+
glyph
+
block2
+
+
+
+
+
foreground
+
F1
+
B2
+
F2
+
+
+
background
+
B1
+
B1
+
B2
+
+
+
+
So, that’s a start. First, let’s define the glyph symbols in the polybar config:
+
[glyph]
+gleft=
+gright=
+
Defining modules
+
As we want to interweave polybar modules with these glyphs in the right order and with the right colors, it is reasonable to define a single source of truth:
+
+
+
+
+
Index
+
Module
+
Color
+
Glyph
+
+
+
+
+
1
+
pulseaudio
+
light-magenta
+
+
+
+
+
2
+
mpd
+
magenta
+
+
+
+
+
9
+
battery
+
light-cyan
+
+
+
+
+
3
+
cpu
+
cyan
+
+
+
+
+
4
+
ram-memory
+
light-green
+
+
+
+
+
5
+
swap-memory
+
green
+
+
+
+
+
6
+
bandwidth
+
light-red
+
+
+
+
+
7
+
openvpn
+
light-red
+
+
+
+
8
+
xkeyboard
+
red
+
+
+
+
+
10
+
weather
+
light-yellow
+
+
+
+
+
12
+
sun
+
yellow
+
+
+
+
+
13
+
aw-afk
+
light-blue
+
+
+
+
+
14
+
date
+
blue
+
+
+
+
+
+
Also excluding some modules from certain monitors, which for now is about excluding battery from the monitors of my desktop PC:
+
+
+
+
+
Monitor
+
Exclude
+
+
+
+
+
DVI-D-0
+
battery
+
+
+
HDMI-A-0
+
battery
+
+
+
+
Another thing we need to do is to set the color of modules in accordance with the polybar_modules table. The background can be determined from the Color column with the following code block:
The polybar config doesn’t support conditional statements, but it does support environment variables, so I pass the parameters from in the launch script.
have different blocks on my two different-sized monitors and my laptop;
+
have different settings on my desktop PC and laptop;
+
+
+
hostname=$(hostname)
+# Settings varying on the hostname
+if["$hostname"="azure"]; then
+TRAY_MONITOR="eDP-1"
+elif["$hostname"="eminence"]; then
+if xrandr --query | grep " connected" | cut -d" " -f1 | grep -q "HDMI-A-0"; then
+TRAY_MONITOR="HDMI-A-0"
+else
+TRAY_MONITOR="eDP"
+fi
+elif["$hostname"="iris"]; then
+TRAY_MONITOR="HDMI-1"
+else
+TRAY_MONITOR="HDMI-A-0"
+fi
+
+# Setting varying on the monitor
+declare -A FONT_SIZES=(
+["eDP"]="13"
+["eDP-1"]="13"
+["DVI-D-0"]="13"
+["HDMI-A-0"]="13"
+["HDMI-1"]="13"
+)
+declare -A EMOJI_SCALE=(
+["eDP"]="9"
+["eDP-1"]="9"
+["DVI-D-0"]="10"
+["HDMI-A-0"]="10"
+["HDMI-1"]="10"
+)
+declare -A BAR_HEIGHT=(
+["eDP"]="29"
+["eDP-1"]="29"
+["DVI-D-0"]="29"
+["HDMI-A-0"]="29"
+["HDMI-1"]="29"
+)
+declare -A BLOCKS=(
+["eDP"]="<<polybar-generate-modules(monitor="eDP")>>"
+["eDP-1"]="<<polybar-generate-modules(monitor="eDP-1")>>"
+["DVI-D-0"]="<<polybar-generate-modules(monitor="DVI-D-0")>>"
+["HDMI-A-0"]="<<polybar-generate-modules(monitor="HDMI-A-0")>>"
+["HDMI-1"]="<<polybar-generate-modules(monitor="HDMI-1")>>"
+)
+
+# Geolocation for some modules
+exportLOC="SPB"
+
+# export IPSTACK_API_KEY=$(pass show My_Online/APIs/ipstack | head -n 1)
+
+pkill polybar
+for m in $(xrandr --query | grep " connected" | cut -d" " -f1); do
+exportMONITOR=$m
+if["$MONITOR"="$TRAY_MONITOR"]; then
+exportTRAY="right"
+else
+exportTRAY="none"
+fi
+SIZE=${FONT_SIZES[$MONITOR]}
+SCALE=${EMOJI_SCALE[$MONITOR]}
+if[[ -z "$SCALE"]]; then
+continue
+fi
+# export FONT0="pango:monospace:size=$SIZE;1"
+# export FONT1="NotoEmoji:scale=$SCALE:antialias=false;1"
+# export FONT2="fontawesome:pixelsize=$SIZE;1"
+# export FONT3="JetBrains Mono Nerd Font:monospace:size=15;1"
+exportHEIGHT=${BAR_HEIGHT[$MONITOR]}
+exportRIGHT_BLOCKS=${BLOCKS[$MONITOR]}
+ polybar mybar &
+done
+
Individual modules
+
Some of the custom modules below use Org mode noweb to evaluate colors, because it’s faster than querying xrdb at runtime. I wish I could reference polybar values there, but it looks like this is impossible.
+
If you want to copy something, you can go to the bin/polybar folder.
[module/i3]
+type=internal/i3
+format=<label-state> <label-mode>
+index-sort=true
+wrapping-scroll=false
+
+; Only show workspaces on the same output as the bar
+pin-workspaces=true
+
+label-mode-padding=1
+label-mode-foreground=${colors.white}
+label-mode-background=${colors.blue}
+
+; focused = Active workspace on focused monitor
+label-focused=%
+label-focused-background=${colors.blue}
+label-focused-underline=${colors.blue}
+label-focused-padding=1
+
+; unfocused = Inactive workspace on any monitor
+label-unfocused=%
+label-unfocused-padding=1
+label-unfocused-foreground=${colors.white}
+
+; visible = Active workspace on unfocused monitor
+label-visible=%
+; label-visible-background = ${self.label-focused-background}
+label-visible-underline=${self.label-focused-underline}
+label-visible-padding=${self.label-focused-padding}
+
+; urgent = Workspace with urgency hint set
+label-urgent=%
+label-urgent-background=${colors.red}
+label-urgent-foreground=${colors.black}
+label-urgent-padding=1
+
Rofi
+
+
+
+
Category
+
Guix dependency
+
+
+
+
+
desktop-rofi
+
rofi
+
+
+
+
rofi is another dynamic menu generator. It can act as dmenu replacement but offers a superset of dmenu’s features.
+
Theme
+
A theme based on the current Emacs theme. Inspired by dracula theme.
A script to open a man page with zathura. There is no particular reason why one should look through man pages in pdf viewer rather than in console, but why not.
Firefox Color is a system that allows for easy experimentation with Firefox themes.
+
It can serialize themes into URLs like https://color.firefox.com/?theme=<theme>, so I thought it would be a piece of cake to generate one from my Emacs theme, right? Well…
+
As it turns out, Firefox uses npm package called json-url to create <theme>, which this package does by the following sequence:
+
+
msgpack v5
+
lzma
+
url-safe base64
+
+
I tried to reproduce the above in Emacs, but in the end gave up and used the package in a simple node script:
keynav is a program for controlling mouse with keyboard, mostly by screen bisection. This is a poor replacement for a proper keyboard-drived sofware, but…
I don’t use stuff like transparency for inactive windows.
+
The first 5 lines of opacity-rule make i3wm’s hidden windows 100% transparent, so I see the background behind the semi-transparent windows in i3wm’s stacked and tabbed layout. Here is StackExchange question about that.
+
I also noticed that for some reason it doesn’t play well with Emacs’s built-in transparency, so the last line sets up Emacs transparency at 90%.
My configuration of GNU Emacs, an awesome text editor piece of software that can do almost anything.
+
At the moment of writing this, that “almost anything” includes:
+
+
Programming environment. With LSP & Co, Emacs is as good as many IDEs and is certainly on par with editors like VS Code.
+Emacs is also particularly great at writing Lisp code, e.g. Clojure, Common Lisp, and of course, Emacs Lisp.
+
Org Mode is useful for a lot of things. My use cases include:
+
Formatting documents. I’ve written my Master’s Thesis in Org Mode.
+
Notetaking, mostly with org-roam and org-journal
+
+
+
File management. Dired is my primary file manager.
+
Email, with notmuch.
+
Multimedia management, with EMMS.
+
RSS feed reader, with elfeed.
+
Managing passwords, with pass.
+
Messengers:
+
+
IRC, with ERC.
+
Telegram, with telega.el
+
+
+
X Window management, with EXWM. I literally live in Emacs.
+
…
+
+
As I mentioned above, this document is a piece of literate configuration, i.e. program code interwoven with (occasionally semi-broken) English-language commentary.
+
I find that approach helpful for maintaining the configuration, but the quality and quantity of comments may vary. I also usually incorporate my Emacs-related blog posts back into this config.
+
So, you might extract something of value from here if you’re an avid Emacs user, but probably not if you’re a newcomer to the Elisp wonderland. If the latter applies to you, I’d advise checking out David Wilson’s System Crafters YouTube channel.
+
Some remarks
+
I decided not to keep configs for features that I do not use anymore because this config is already huge. But here are the last commits that had these features presented.
+
+
+
+
Feature
+
Last commit
+
+
+
+
+
org-roam dailies
+
d2648918fcc338bd5c1cd6d5c0aa60a65077ccf7
+
+
+
org-roam projects
+
025278a1e180e86f3aade20242e4ac1cdc1a2f13
+
+
+
treemacs
+
3d87852745caacc0863c747f1fa9871d367240d2
+
+
+
tab-bar.el
+
19ff54db9fe21fd5bdf404a8d2612176baa8a6f5
+
+
+
spaceline
+
19ff54db9fe21fd5bdf404a8d2612176baa8a6f5
+
+
+
code compass
+
8594d6f53e42c70bbf903e168607841854818a38
+
+
+
vue-mode
+
8594d6f53e42c70bbf903e168607841854818a38
+
+
+
svelte-mode
+
8594d6f53e42c70bbf903e168607841854818a38
+
+
+
pomidor
+
8594d6f53e42c70bbf903e168607841854818a38
+
+
+
elfeed-score
+
8e591e0d2afd909ae5be00caf17f9b17c6cd8b61
+
+
+
org-trello
+
3f5967a5f63928ea9c8567d8d9f31e84cdbbc21f
+
+
+
jabber
+
9b0e73a4703ff35a2d30fd704200052888191217
+
+
+
wallabag
+
9b0e73a4703ff35a2d30fd704200052888191217
+
+
+
conda
+
609fc84e439b11ea5064f3a948079daebb654aca
+
+
+
notmuch tags keybindings
+
eac134c5456051171c1c777254f503cc71ce12cd
+
+
+
expand-region
+
ab0d01c525f2b44dd64ec09747daf0fced4bd9c7
+
+
+
org-latex-impatient
+
ab0d01c525f2b44dd64ec09747daf0fced4bd9c7
+
+
+
dired-single
+
ab0d01c525f2b44dd64ec09747daf0fced4bd9c7
+
+
+
progidy
+
ab0d01c525f2b44dd64ec09747daf0fced4bd9c7
+
+
+
tree-sitter
+
1920a48aec49837d63fa88ca315928dc4e9d14c2
+
+
+
org-roam-protocol
+
2f0c20eb01b8899d00d129cc7ca5c6b263c69c65
+
+
+
+
Initial setup
+
Setting up the environment, performance tuning and a few basic settings.
+
First things first, lexical binding.
+
;;; -*- lexical-binding: t -*-
+
Packages
+
straight.el
+
Straight.el is my Emacs package manager of choice. Its advantages & disadvantages over other options are listed pretty thoroughly in the README file in the repo.
+
The following is the bootstrap script of straight.el.
Also, I sometimes need to know if a program is running inside Emacs (say, inside a terminal emulator). To do that, I set the following environment variable:
+
(setenv"IS_EMACS""true")
+
Finally, I want to have a minimal Emacs config for debugging purposes. This has just straight.el, use-packages, and evil.
A small function to print out the loading time and number of GCs during the loading. Can be useful as a point of data for optimizing Emacs startup time.
Set the following to t to print debug information during the startup. This will include the order in which the packages are loaded and the loading time of individual packages.
I’ve noticed that Emacs occasionally eats a lot of RAM, especially when used with EXWM. This is my attempt to measure RAM usage.
+
I have some concerns that ps -o rss may be unrepresentative because of shared memory, but I guess this shouldn’t be a problem here because there’s only one process of Emacs.
By default Emacs and its packages create a lot files in .emacs.d and in other places. no-littering is a collective effort to redirect all of that to two folders in user-emacs-directory.
+
(use-packageno-littering
+:straightt)
+
Prevent Emacs from closing
+
This adds a confirmation to avoid accidental Emacs closing.
+
(setqconfirm-kill-emacs'y-or-n-p)
+
General settings
+
Keybindings
+
general.el
+
general.el provides a convenient interface to manage Emacs keybindings.
A package that displays the available keybindings in a popup. The package is pretty useful, as Emacs seems to have more keybindings than I can remember at any given point.
Various keybinding settings that I can’t put anywhere else.
+
Escape key
+
Use the escape key instead of C-g whenever possible No, not really after 2 years… But I’ll keep this fragment.
+
I must have copied it from somewhere, but as I googled to find out the source, I discovered quite a number of variations of the following code over time. I wonder if Richard Dawkins was inspired by something like this a few decades ago.
+
(defunminibuffer-keyboard-quit ()
+"Abort recursive edit.
+In Delete Selection mode, if the mark is active, just deactivate it;
+then it takes a second \\[keyboard-quit] to abort the minibuffer."
+ (interactive)
+ (if (anddelete-selection-modetransient-mark-modemark-active)
+ (setqdeactivate-markt)
+ (when (get-buffer"*Completions*") (delete-windows-on"*Completions*"))
+ (abort-recursive-edit)))
+
+(defunmy/escape-key ()
+ (interactive)
+ (evil-ex-nohighlight)
+ (keyboard-quit))
+
+(general-define-key
+:keymaps'(normalvisualglobal)
+ [escape] #'my/escape-key)
+
+(general-define-key
+:keymaps'(minibuffer-local-map
+minibuffer-local-ns-map
+minibuffer-local-completion-map
+minibuffer-local-must-match-map
+minibuffer-local-isearch-map)
+ [escape] 'minibuffer-keyboard-quit)
+
general.el has a nice integration with which-key, so I use that to show more descriptive annotations for certain groups of keybindings (the default annotation is just prefix).
+
(my-leader-def
+"a"'(:which-key"apps"))
+
Universal argument
+
Change the universal argument to M-u. I use C-u to scroll up, as I’m used to from vim.
Some keybindings I used in vim to switch buffers and can’t let go of. But I think I started to use these less since I made an attempt in i3 integration.
The most versatile is the built-in hs-minor-mode, which seems to work out of the box for Lisps, C-like languages, and Python. outline-minor-mode works for org-mode, LaTeX and the like. There is a 3rd-party solution origami.el, which I found to be somewhat less stable.
+
Evil does a pretty good job of abstracting all these packages with a set of vim-like keybindings. I was using SPC in vim, but as now this isn’t an option, I set TAB to toggle folding.
(defunmy/zoom-in ()
+"Increase font size by 10 points"
+ (interactive)
+ (set-face-attribute'defaultnil
+:height
+ (+ (face-attribute'default:height) 10)))
+
+(defunmy/zoom-out ()
+"Decrease font size by 10 points"
+ (interactive)
+ (set-face-attribute'defaultnil
+:height
+ (- (face-attribute'default:height) 10)))
+
+;; change font size, interactively
+(global-set-key (kbd"C-+") 'my/zoom-in)
+(global-set-key (kbd"C-=") 'my/zoom-out)
+
i3 integration
+
UPD <2021-11-27 Sat>. I have finally switched to EXWM as my window manager, but as long as I keep i3 as a backup solution, this section persists. Check out the post for a somewhat better presentation.
+
One advantage of EXWM for an Emacs user is that EXWM gives one set of keybindings to manage both Emacs windows and X windows. In every other WM, like my preferred i3wm, two orthogonal keymaps seem to be necessary. But, as both programs are quite customizable, I want to see whether I can replicate at least some part of the EXWM goodness in i3.
+
But why not just use EXWM? One key reason is that to my taste (and perhaps on my hardware) EXWM didn’t feel snappy enough. Also, I really like i3’s tree-based layout structure; I feel like it fits my workflow much better than anything else I tried, including the master/stack paradigm of XMonad, for instance.
+
One common point of criticism of i3 is that it is not extensible enough, especially compared to WMs that are configured in an actual programing language, like the mentioned XMonad, Qtile, Awesome, etc. But I think i3’s extensibility is underappreciated, although the contents of this section may lie closer to the limits of how far one can go there.
+
The basic idea is to launch a normal i3 command with i3-msg in case the current window is not Emacs, otherwise pass that command to Emacs with emacsclient. In Emacs, execute the command if possible, otherwise pass the command back to i3.
+
This may seem like a lot of overhead, but I didn’t feel it even in the worst case (i3 -> Emacs -> i3), so at least in that regard, the interaction feels seamless. The only concern is that this command flow is vulnerable to Emacs getting stuck, but it is still much less of a problem than with EXWM.
+
One interesting observation here is that Emacs windows and X windows are sort of one-level entities, so I can talk just about “windows”.
+
At any rate, we need a script to do the i3 -> Emacs part:
Now we have to handle the required set of i3 commands. It is worth noting here that I’m not trying to implement a general mechanism to apply i3 commands to Emacs, rather I’m implementing a small subset that I use in my i3 configuration and that maps reasonably to the Emacs concepts.
+
Also, I use evil-mode and generally configure the software to have vim-style bindings where possible. So if you don’t use evil-mode you’d have to detangle the given functions from evil, but then, I guess, you do not use super+hjkl to manage windows either.
+
First, for the focus command I want to move to an Emacs window in the given direction if there is one, otherwise move to an X window in the same direction. Fortunately, i3 and windmove have the same names for directions, so the function is rather straightforward.
+
One caveat here is that the minibuffer is always the bottom-most Emacs window, so it is necessary to check for that as well.
if there is space in the required direction, move the Emacs window there;
+
if there is no space in the required direction, but space in two orthogonal directions, move the Emacs window so that there is no more space in the orthogonal directions;
+
otherwise, move an X window (Emacs frame).
+
+
For the first part, window-swap-states with windmove-find-other-window do well enough.
+
evil-move-window works well for the second part. By itself it doesn’t behave quite like i3, for instance, (evil-move-window 'right) in a three-column split would move the window from the far left side to the far right side (bypassing center). Hence the combination as described here.
+
So here is a simple predicate which checks whether there is space in the given direction.
Next on the line are resize grow and resize shrink. evil-window- functions do nicely for this task.
+
This function also checks whether there is space to resize in the given direction with the help of the predicate defined above. The command is forwarded back to i3 if there is not.
transpose-frame is a package to “transpose” the current frame layout, which behaves someone similar to the layout toggle split command in i3, so I’ll use it as well.
Finally, the entrypoint for the Emacs integration. In addition to the commands defined above, it processes split and kill commands and passes every other command back to i3.
I have to switch layouts all the time, especially in LaTeX documents, because for some reason the Bolsheviks abandoned the idea of replacing Russian Cyrillic letters with Latin ones.
A minor mode to deal with pairs. Its functionality overlaps with evil-surround, but smartparens provides the most comfortable way to do stuff like automatically insert pairs.
I used to have Treemacs here, but in the end decided that dired with dired-sidebar does a better job. Dired has its separate section in “Applications”.
+
Projectile
+
Projectile gives a bunch of useful functions for managing projects, like finding files within a project, fuzzy-find, replace, etc.
+
defadvice is meant to speed projectile up with TRAMP a bit.
Word wrapping. These settings aren’t too obvious compared to :set wrap from vim:
+
+
word-wrap means just “don’t split one word between two lines”. So, if there isn’t enough place to put a word at the end of the line, it will be put on a new one. Run M-x toggle-word-wrap to toggle that.
+
visual-line-mode seems to be a superset of word-wrap. It also enables some editing commands to work on visual lines instead of logical ones, hence the naming.
+
auto-fill-mode does the same as word-wrap, except it actually edits the buffer to make lines break in the appropriate places.
+
truncate-lines truncates long lines instead of continuing them. Run M-x toggle-truncate-lines to toggle that. I find that truncate-lines behaves strangely when visual-line-mode is on, so I use one or another.
+
+
+
(setqword-wrap1)
+(global-visual-line-mode1)
+
Custom frame format
+
Title format, which used to look something like emacs:project@hostname. Now it’s just emacs.
Here I define a few things on the top of Emacs theme, because:
+
+
Occasionally I want to have more theme-derived faces
+
I also want Emacs theme to be applied to the rest of the system (see the Desktop config on that)
+
+
Theme-derived faces have to placed in a custom theme, because if one calls custom-set-faces and custom-set-variables in code, whenever a variable is changed and saved in a customize buffer, data from all calls of these functions is saved as well.
+
Get color values
+
Here’s a great package with various color tools:
+
(use-packagect
+:straightt)
+
As of now I want this to support doom-themes and modus-themes. So, let’s get which one is enabled:
And the same for modus-themes. my/modus-color has to accept the same arguments as I use for my/doom-color for backward compatibility, which requires a bit more tuning.
perspective.el is a package that groups buffers in “perspectives”.
+
tab-bar.el can be configured to behave in a similar way, but I’m too invested in this package already.
+
One thing I don’t like is that the list perspectives is displayed in the modeline, but I’ll probably look how to move them to the bar at the top of the frame at some point.
Out-of-the-box, perspective.el doesn’t feature much (or any) capacity for automation. We’re supposed to manually assign buffers to perspectives, which kinda makes sense… But I still want automation.
If EXWM is available, then so is mine perspective-exwm package, which features a convenient procedure called perspective-exwm-assign-window. Otherwise, we just work with perspectives.
+
Now, we have to put this function somewhere, and after-change-major-mode-hook seems like a perfect place for it.
Also, the logic above works only for cases when the buffer is created. Occasionally, packages run switch-to-buffer, which screws both EXWM workspaces and perspectives; to work around that, I define a macro that runs a command in the context of a given perspective and workspace.
This is meant to be used in the definitions of general.el.
+
Programming
+
General setup
+
Treemacs
+
Treemacs is a rather large & powerful package, but as of now I’ve replaced it with dired. I still have a small configuration because lsp-mode and dap-mode depend on it.
I don’t like some keybindings in the built-in hydra, and there seems to be no easy way to modify the existing hydra, so I create my own. I tried to use transient, but the transient buffer seems to conflict with special buffers of DAP, and hydra does not.
+
Also, I want the hydra to toggle UI windows instead of just opening them, so here is a macro that defines such functions:
First, DAP uses Treemacs buffers quite extensively, and they hide the doom modeline for some reason, so I can’t tell which buffer is active and can’t see borders between buffers.
+
Second, lines are truncated in some strange way, but calling toggle-truncate-lines seems to fix that.
+
So I define a macro that creates a function that I can further use in advices.
One significant improvement over Chrome Inspector for my particular stack is an ability to filter the stack frame list, for instance, to see only frames that relate to my current project.
+
So, here are functions that customize the filters:
And here is a version of dap-switch-stack-frame that uses the said filter.
+
(defunmy/dap-switch-stack-frame ()
+"Switch stackframe by selecting another stackframe stackframes from current thread."
+ (interactive)
+ (when (not (dap--cur-session))
+ (error"There is no active session"))
+
+ (-if-let (thread-id (dap--debug-session-thread-id (dap--cur-session)))
+ (-if-let (stack-frames
+ (gethash
+thread-id
+ (dap--debug-session-thread-stack-frames (dap--cur-session))))
+ (let* ((index0)
+ (stack-framces-filtered
+ (-filter
+#'my/dap-stack-frame-filter
+stack-frames))
+ (new-stack-frame
+ (dap--completing-read
+"Select active frame: "
+stack-framces-filtered
+ (-lambda ((frame&as&hash"name"))
+ (if-let (frame-path (dap--get-path-for-frameframe))
+ (format"%s: %s (in %s)"
+ (cl-incfindex) nameframe-path)
+ (format"%s: %s" (cl-incfindex) name)))
+nil
+t)))
+ (dap--go-to-stack-frame (dap--cur-session) new-stack-frame))
+ (->> (dap--cur-session)
+dap--debug-session-name
+ (format"Current session %s is not stopped")
+error))
+ (error"No thread is currently active %s" (dap--debug-session-name (dap--cur-session)))))
+
Smarter switch to stack frame
+
+
CREDIT: Thanks @yyoncho on the Emacs LSP Discord for helping me with this!
+
+
By default, when a breakpoint is hit, dap always pop us the buffer in the active EXWM workspace and in the active perspective. I’d like it to switch to an existing buffer instead.
+
So first we need to locate EXWM workspace for the file with path:
A general-purpose package to run formatters on files. While the most popular formatters are already packaged for Emacs, those that aren’t can be invoked with this package.
+
(use-packagereformatter
+:straightt)
+
copilot
+
GitHub Copilot is a project of GitHub and OpenAI that provides code completions. It’s somewhat controversial in the Emacs community but I opt in for now.
ltex-ls is a tool that wraps LanguageTool into a language server.
+
It takes maybe 10 seconds to run on my Master’s thesis file (M-x count words: 13453 words and 117566 characters), but it’s totally worth it. And it’s much faster on smaller files. The good thing is that it supports markup syntaxes like Org and Markdown, whereas LanguageTool by itself produces a lot of false positives on these files.
+
It shouldn’t be too hard to package that for guix, but I’ve installed the nix version for now.
LanguageTool is a great offline spell checker. For some reason, the download link is nowhere to be found on the home page, so it is listed in the references as well.
sql-formatter is a nice JavaScript package for pretty-printing SQL queries. It is not packaged for Emacs, so the easiest way to use it seems to be to define a custom formatter via reformatter.
+
Also, I’ve made a simple function to switch dialects because I often alternate between them.
+
So far I didn’t find a nice SQL client for Emacs, but I occasionally run SQL queries in Org Mode, so this quite package is handy.
This enables encryption for Org segments tagged :crypt:.
+
Another way to encrypt Org files is to save them with the extension .org.gpg. However, by default EPA always prompts for the key, which is not what I want when there is only one key to select. Hence the following advice:
Kernelspecs by default are hashed, so even switching Anaconda environments doesn’t change the kernel (i.e. kernel from the first environment is run after the switch to the second one).
Also, if some kernel wasn’t present at the moment of the load of emacs-jupyter, it won’t be added to the org-src-lang-modes list. E.g. I have Hy kernel installed in a separate Anaconda environment, so if Emacs hasn’t been launched in this environment, I wouldn’t be able to use hy in org-src blocks.
+
Fortunately, emacs-jupyter provides a function for that problem as well.
Emacs-jupyter has its own insertion mechanisms, which always prepends output statements with :. That is not desirable in cases where a kernel supports only plain output, e.g. calysto_hy kernel.
+
So there we have a minor mode that overrides this behavior.
A few tricks to do literate programming. I actually have only one (sqrt-data), and I’m not convinced in the benefits of the approach…
+
Anyway, Org files are better off in a separated directory (e.g. org). So I’ve come up with the following solution to avoid manually prefixing the :tangle arguments.
+
Set up the following argument with the path to the project root:
I wanted org-mode-line-string to be prepended to global-mode-string rather than appended, but somehow the modeline stops working if org-mode-line-string is the first element… So I’ll at least put it before my exwm-modeline-segment.
By default, org-clock stores its results only in the :LOGBOOK: drawer, which doesn’t get parsed by org-element-at-point. As such, clock resutls are inaccessible from org-ql.
+
This ensures that the total clocked time is also saved in the :PROPERTIES: drawer.
+
We can get the clocked value in minutes with org-clock-sum. This weird function stores what I need in buffer-local variables and text-properties.
+
(defunmy/org-clock-get-total-minutes-at-point ()
+"Get total clocked time for heading at point."
+ (let* ((element (org-element-at-point-no-context))
+ (s (buffer-substring-no-properties
+ (org-element-property:beginelement)
+ (org-element-property:endelement))))
+ (with-temp-buffer
+ (inserts)
+ (org-clock-sum)
+org-clock-file-total-minutes)))
+
And use the function to set the total clocked time.
+
(defconstmy/org-clock-total-prop:CLOCK_TOTAL)
+
+(defunmy/org-clock-set-total-clocked ()
+"Set total clocked time for heading at point."
+ (interactive)
+ (save-excursion
+ (org-back-to-headingt)
+ (org-set-property
+ (substring
+ (symbol-namemy/org-clock-total-prop)
+1)
+ (org-duration-from-minutes
+ (my/org-clock-get-total-minutes-at-point)))))
+
+(add-hook'org-clock-in-hook#'my/org-clock-set-total-clocked)
+(add-hook'org-clock-out-hook#'my/org-clock-set-total-clocked)
+(add-hook'org-clock-cancel-hook#'my/org-clock-set-total-clocked)
+
org-super-agenda
+
org-super-agenda is alphapapa’s extension to group items in org-agenda. I don’t use it instead of the standard agenda, but org-ql uses it for some of its views.
+
(use-packageorg-super-agenda
+:straightt
+:after (org)
+:config
+;; Alphapapa doesn't like evil
+ (general-define-key
+:keymaps'(org-super-agenda-header-map)
+"h"nil
+"j"nil
+"k"nil
+"l"nil)
+ (org-super-agenda--def-auto-groupoutline-path-file"their outline paths & files"
+:key-form
+ (org-super-agenda--when-with-marker-buffer (org-super-agenda--get-markeritem)
+;; org-ql depends on f and s anyway
+ (s-join"/" (cons
+ (f-filename (buffer-file-name))
+ (org-get-outline-path))))))
+
I use the property predicate to find tasks linked to meetings, and I want to link some tasks to multiple meetings. So I modified the property predicate to support that.
+
I can’t contribute that back to org-ql because it requires copyright assignment, so here it is.
+
(with-eval-after-load'org-ql
+ (org-ql-defpredproperty (property&optionalvalue&keyinheritmulti)
+"Return non-nil if current entry has PROPERTY, and optionally VALUE.
+If INHERIT is nil, only match entries with PROPERTY set on the
+entry; if t, also match entries with inheritance. If INHERIT is
+not specified, use the Boolean value of
+`org-use-property-inheritance', which see (i.e. it is only
+interpreted as nil or non-nil). If MULTI is non-nil, also check for
+multi-value properties."
+:normalizers ((`(,predicate-names)
+;; HACK: This clause protects against the case in
+;; which the arguments are nil, which would cause an
+;; error in `rx-to-string' in other clauses. This
+;; can happen with `org-ql-completing-read',
+;; e.g. when the input is "property:" while the user
+;; is typing.
+;; FIXME: Instead of this being moot, make this
+;; predicate test for whether an entry has local
+;; properties when no arguments are given.
+ (list'property""))
+ (`(,predicate-names,property,value.,plist)
+;; Convert keyword property arguments to strings. Non-sexp
+;; queries result in keyword property arguments (because to do
+;; otherwise would require ugly special-casing in the parsing).
+ (when (keywordpproperty)
+ (setfproperty (substring (symbol-nameproperty) 1)))
+ (list'propertypropertyvalue
+:inherit (if (plist-memberplist:inherit)
+ (plist-getplist:inherit)
+org-use-property-inheritance)
+:multi (when (plist-memberplist:multi)
+ (plist-getplist:multi)))))
+;; MAYBE: Should case folding be disabled for properties? What about values?
+;; MAYBE: Support (property) without args.
+
+;; NOTE: When inheritance is enabled, the preamble can't be used,
+;; which will make the search slower.
+:preambles ((`(,predicate-names,property,value,(map:multi) .,(map:inherit))
+;; We do NOT return nil, because the predicate still needs to be tested,
+;; because the regexp could match a string not inside a property drawer.
+ (list:regexp (unlessinherit
+ (rx-to-string`(seqbol (0+space) ":",property
+,@(whenmulti'((? "+"))) ":"
+ (1+space) ,value (0+space) eol)))
+:queryquery))
+ (`(,predicate-names,property,(map:multi) .,(map:inherit))
+;; We do NOT return nil, because the predicate still needs to be tested,
+;; because the regexp could match a string not inside a property drawer.
+;; NOTE: The preamble only matches if there appears to be a value.
+;; A line like ":ID: " without any other text does not match.
+ (list:regexp (unlessinherit
+ (rx-to-string`(seqbol (0+space) ":",property
+,@(whenmulti'((? "+")))
+":" (1+space)
+ (minimal-match (1+not-newline)) eol)))
+:queryquery)))
+:body
+ (pcaseproperty
+ ('nil (user-error"Property matcher requires a PROPERTY argument"))
+ (_ (pcasevalue
+ ('nil
+;; Check that PROPERTY exists
+ (org-ql--value-at
+ (point) (lambda ()
+ (org-entry-get (point) property))))
+ (_
+;; Check that PROPERTY has VALUE.
+
+;; TODO: Since --value-at doesn't account for inheritance,
+;; we should generalize --tags-at to also work for property
+;; inheritance and use it here, which should be much faster.
+ (ifmulti
+ (when-let (values (org-ql--value-at
+ (point) (lambda ()
+;; The default separator is space
+ (let ((org-property-separators`((,property."\n"))))
+ (org-entry-get (point) propertyinherit)))))
+ (seq-some (lambda (v)
+ (string-equalvaluev))
+ (split-stringvalues"\n")))
+ (string-equalvalue (org-ql--value-at
+ (point) (lambda ()
+ (org-entry-get (point) propertyinherit)))))))))))
+
Recent items
+
I just want to change the default grouping in org-ql-view-recent-items…
+
(cl-defunmy/org-ql-view-recent-items
+ (&keynum-days (type'ts)
+ (files (org-agenda-files))
+ (groups'((:auto-outline-path-filet)
+ (:auto-todot))))
+"Show items in FILES from last NUM-DAYS days with timestamps of TYPE.
+TYPE may be `ts', `ts-active', `ts-inactive', `clocked', or
+`closed'."
+ (interactive (list:num-days (read-number"Days: ")
+:type (->>'(tsts-activets-inactiveclockedclosed)
+ (completing-read"Timestamp type: ")
+intern)))
+;; It doesn't make much sense to use other date-based selectors to
+;; look into the past, so to prevent confusion, we won't allow them.
+ (-let* ((query (pcase-exhaustivetype
+ ((or'ts'ts-active'ts-inactive)
+`(,type:from,(-num-days) :to0))
+ ((or'clocked'closed)
+`(,type:from,(-num-days) :to0)))))
+ (org-ql-searchfilesquery
+:title"Recent items"
+:sort'(todoprioritydate)
+:super-groupsgroups)))
+
Return all TODOs
+
A view to return all TODOs in a category.
+
(defunmy/org-ql-all-todo ()
+ (interactive)
+;; The hack I borrowed from notmuch to make " " a separator
+ (let* ((crm-separator" ")
+ (crm-local-completion-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parentmapcrm-local-completion-map)
+ (define-keymap" "'self-insert-command)
+map))
+ (ivy-prescient-sort-commandsnil)
+ (categories (completing-read-multiple
+"Categories: "
+'("TEACH""EDU""JOB""LIFE""CONFIG"))))
+ (org-ql-search (org-agenda-files)
+`(and (todo)
+,@(unless (seq-empty-pcategories)
+`((category,@categories))))
+:sort'(prioritytododeadline)
+:super-groups'((:auto-outline-path-filet)))))
+
The workflow here is basically link some tasks to meeting(s) and create a report from tasks linked to a particular meetings. The report also shows the time spent per task thanks to the last modification to org-ql.
+
This is essentially to avoid having to scramble my mind for hints of what I’m supposed to tell I was doing at each meeting.
See this answer at Emacs StackExchange for filtering the agenda block by tag:
+
(defunmy/org-match-at-point-p (match)
+"Return non-nil if headline at point matches MATCH.
+Here MATCH is a match string of the same format used by
+`org-tags-view'."
+ (funcall (cdr (org-make-tags-matchermatch))
+ (org-get-todo-state)
+ (org-get-tags-at)
+ (org-reduced-level (org-current-level))))
+
+(defunmy/org-agenda-skip-without-match (match)
+"Skip current headline unless it matches MATCH.
+
+Return nil if headline containing point matches MATCH (which
+should be a match string of the same format used by
+`org-tags-view'). If headline does not match, return the
+position of the next headline in current buffer.
+
+Intended for use with `org-agenda-skip-function', where this will
+skip exactly those headlines that do not match."
+ (save-excursion
+ (unless (org-at-heading-p) (org-back-to-heading))
+ (let ((next-headline (save-excursion
+ (or (outline-next-heading) (point-max)))))
+ (if (my/org-match-at-point-pmatch) nilnext-headline))))
+
Me at 10:00: Open Org Agenga oh, there’s a meeting at 15:00
+
Me at 14:00: Open Org Agenda oh, there’s a meeting at 15:00
+
Me at 14:45: Gotta remember to join in 15 minutes
+
Me at 14:55: Gotta remember to join in 5 minutes
+
Me at 15:05: Sh*t
+
+
Okay, I will set up org-alert some custom alert system.
+
I want to have multiple warnings, let it be 10 minutes in advance and 1 minute in advance for now.
+
(setqmy/org-alert-notify-times'(60060))
+
And IDK if that makes much sense, but I’ll try to avoid re-creating timers. So, here are functions to schedule showing some label at some time and to check whether the label is scheduled:
+
(setqmy/org-alert--alerts (make-hash-table:test#'equal))
+
+(defunmy/org-alert--is-scheduled (labeltime)
+"Check if LABEL is scheduled to be shown an TIME."
+ (gethash (conslabeltime)
+my/org-alert--alertsnil))
+
+(defunmy/org-alert--schedule (labeltime)
+"Schedule LABEL to be shown at TIME, unless it's already scheduled."
+ (unless (my/org-alert--is-scheduledlabeltime)
+ (puthash (conslabeltime)
+ (run-at-timetime
+nil
+ (lambda ()
+ (alertlabel
+:title"PROXIMITY ALERT")))
+my/org-alert--alerts)))
+
And unschedule items that need to be unscheduled:
+
(defunmy/org-alert-cleanup (&optionalkeys)
+"Unschedule items that do not appear in KEYS.
+
+KEYS is a list of cons cells like (<label> . <time>)."
+ (let ((existing-hash (make-hash-table:test#'equal)))
+ (cl-loopforkeyinkeys
+do (puthashkeytexisting-hash))
+ (cl-loopforkeybeingthehash-keysofmy/org-alert--alerts
+unless (gethashkeyexisting-hash)
+do (progn
+ (cancel-timer (gethashkeymy/org-alert--alerts))
+ (remhashkeymy/org-alert--alerts)))))
+
And a function to extract the required items with org-ql-query and schedule them:
I don’t have any idea why, but evaluating (my/org-alert-mode) just after org breaks font-lock after I try to open inbox.org. emacs-startup-hook, however, works fine.
Naturally, I want a way to copy such records. Org Mode already has a function called org-clone-subtree-with-time-shift, that does everything I want except for updating the numbers.
+
Unfortunately, I see no way to advise the original function, so here’s my version that makes use of evil-numbers:
org-journal is a package for maintaining a journal in org mode.
+
This part turned out to be great. I even consulted the journal a few times to check if something actually happened, which makes me uneasy now that I think about it…
+
One issue I found is that it’s kinda hard to find anything in the journal, and I’m not eager to open the journal for a random date anyway. So I’ve made a package called org-journal-tags.
+
My initial desire was to be able to query the journal for my thoughts on a particular subject or theme, for progress on some project, or for records related to some person… Which is kinda useful, although not quite as much as I expected it to be. Relatively fast querying of the journal is also nice.
+
The section I named “on this day” turned out to be particularly interesting, as it kinda allowed me to connect with past versions of myself.
+
And it was interesting to find the reinforcement effect of checked dates on the calendar.
There is a Zotero extension called better bibtex, which allows for having one bibtex file that is always syncronized with the library. That comes quite handy for Emacs integration.
+
org-ref
+
org-ref is an excellent package by John Kitchin that provides support for managing citations and references in Org Mode.
+
It may have become less relevant since org-cite was merged into plain Org, but org-ref is still just as usable.
+
As of now, this package loads Helm on start. To avoid this, I have to exclude Helm from the Package-requires in the org-ref.el file. I haven’t found a way to do this without modifying the package source yet.
+
There’s a package called org-roam-bibtex that allows to keep literature notes in org-roam and access them from org-ref, but as for now I store literature notes separately.
ivy-bibtex is an Ivy interface to bibtex. It uses the same configuration variables as org-ref, or rather, both packages use variables from the built-in bibtex.el
Writing a journal with org-roam-dailies.
+Didn’t work out as I expected, so I’ve made org-journal-tags after I understood better what I want.
+
+
Regardless, it turned out to be great for managing Zettelkasten, which is the original purpose of the package anyway. I didn’t expect to ever get into something like this, but I guess I was wrong.
+
Some resources that helped me along the way (and still help):
+
+
Sönke Ahrens’ book “How to take smart notes”
+
https://zettelkasten.de/ - a lot of useful stuff here, especially in the “Getting Started” section.
About installing the package on Guix (CREDIT: thanks @Ashraz on the SystemCrafters discord)
+
+
So, for all those interested: unfortunately, org-roam (or rather emacsql-sqlite) cannot compile the sqlite.c and emacsql.c due to missing headers (linux/falloc.h) on Guix. You would have to properly set all the include paths on Guix, and also adjust the PATH to have gcc actually find as later on in the compilation process.
+
Instead, you should remove all Org-Roam related packages from your Emacs installation (via M-x package-delete org-roam RET and M-x package-autoremove RET y RET) and then use the Guix package called emacs-org-roam.
Now we can iterate over all roam links in the buffer, count the number of backlinks via org-roam-db-query and invoke my/org-roam--count-overlay-make if that number is greater than zero:
My take on a review workflow. As a baseline, I want to have a template that lists the important changes since the last review and other basic information. I’m doing reviews regularly, but the time intervals still may vary, hence this flexibility.
+
This section has seen some updates over time.
+
Data from git
+
First, as I have autocommit set up in my org directory, here is a handy function to get an alist of changed files of a form (status . path). In principle, the rev parameter can be a commit, tag, etc but here I’m interested in a form like @{2021-08-30}.
+
Also in principle, Org Roam DB also stores stuff like creation time and modification time, but I started this section before I started using Org Roam extensively, so git works fine for me.
Now that we have the list of new & changed files, I want to sort into a bunch of categories: projects, log entries, etc. The categories are defined by tags.
+
So here is a list of plists that sets these categories. The properties are as follows:
+
+
:status is a git status for the file
+
:tags is a plist that sets up the following conditions for the Roam node
+
+
:include - should be empty or one of these should be present
+
:exclude - should be empty or none of these should be present
+
+
+
:title is the name of category as I want it to be seen in the review template
This list is used to extract & format the relevant section of the review template.
+
cl-loop seems pretty good as a control flow structure, but I’ll see if it is also pretty good at producing poorly maintainable code. At least at the moment of this writing, the function below looks rather concise.
+
(defunmy/org-review-format-roam (changes)
+ (cl-loopforqueryinmy/org-review-roam-queries
+withnodes= (org-roam-node-list)
+withnode-tags= (mapcar#'org-roam-node-tagsnodes)
+forinclude-tags= (plist-get (plist-getquery:tags) :include)
+forexclude-tags= (plist-get (plist-getquery:tags) :exclude)
+;; List of nodes filtered by :tags in query
+forfiltered-nodes=
+ (cl-loopfornodeinnodes
+fortagsinnode-tags
+if (and
+ (or (seq-empty-pinclude-tags)
+ (seq-intersectioninclude-tagstags))
+ (or (seq-empty-pexclude-tags)
+ (not (seq-intersectionexclude-tagstags))))
+collectnode)
+;; List of changes filtered by :status in query
+forfiltered-changes=
+ (cl-loopforchangeinchanges
+if (and (eq (carchange) (plist-getquery:status))
+ (string-match-p (rxbos"roam") (cdrchange)))
+collect (cdrchange))
+;; Intersection of the two filtered lists
+forfinal-nodes=
+ (cl-loopfornodeinfiltered-nodes
+forpath= (file-relative-name (org-roam-node-filenode)
+org-directory)
+if (memberpathfiltered-changes)
+collectnode)
+;; If the intersction list is not empty, format it to the result
+iffinal-nodes
+concat (format"** %s\n" (plist-getquery:title))
+;; FInal list of links, sorted by title
+andconcat (cl-loopfornodein (seq-sort
+ (lambda (node1node2)
+ (string-lessp
+ (org-roam-node-titlenode1)
+ (org-roam-node-titlenode2)))
+final-nodes)
+concat (format"- [[id:%s][%s]]\n"
+ (org-roam-node-idnode)
+ (org-roam-node-titlenode)))))
+
Data from org-agenda via org-ql
+
Third second, I want to list some changes in my agenda. This section will change depending on what I’m currently working on.
+
So, here is a list of queries results of which I want to see in the review template. The format is (name date-field order-by-field query).
Now, we have to put all this together and define a capture template for the review.
+
I’ll use a separate directory for the review files, just like for org-journal and org-roam. I’ll store the review files in org-roam. Time will tell if that’s a good idea. The filename will have a format YYYY-MM-DD.org, which will also free me from the effort of storing the last review date somewhere.
+
If somehow there are no files in the folder, fallback to the current date minus one two week. Also featuring the most awkward date transformation I’ve ever done just to add one date.
Also, LaTeX fragments preview tends to break whenever the are custom #+LATEX_HEADER entries. To circumvent this, I add a custom header and modify the org-preview-latex-process-alist variable
A “profile” in Guix is a way to group package installations. For instance, I have a “music” profile that has software like MPD, ncmpcpp that I’m still occasionally using because of its tag editor, etc. Corresponding to that profile, there’s a manifest named music.scm that looks like this:
I could generate this file with org-babel as any other, but that is often not so convenient. For example, I have a polybar module that uses sunwait to show sunset and sunrise times, and ideally, I want to declare sunwait to be in the “desktop-polybar” profile in the same section that has the polybar module definition and the bash script.
+
So here’s an approach I came up with. The relevant section of the config looks like this:
+
*** sun
+| Category | Guix dependency |
+|-----------------+-----------------|
+| desktop-polybar | sunwait |
+
+Prints out the time of sunrise/sunset. Uses [[https://github.com/risacher/sunwait][sunwait]]
+
+#+begin_src bash :tangle ./bin/polybar/sun.sh :noweb no-export
+...script...
+#+end_src
+
+#+begin_src ini :noweb no-export
+...polybar module definition...
+#+end_src
+
So sunwait is declared in an Org table with Guix dependency in the header. Such tables are spread through my configuration files.
+
Thus I made a function that extracts packages from all such tables from the current Org buffer. The rules are as follows:
+
+
If a column name matches [G|g]uix.*dep, its contents are added to the result.
+
If CATEGORY is passed, a column with name [C|c]ategory is used to filter results. That way, one Org file can be used to produce multiple manifests.
+
If CATEGORY is not passed, entries with the non-empty category are filtered out
+
If there is a [D|d]isabled column, entries that have a non-empty value in this column are filtered out.
There’s a newline symbol between “(” and <<packages("desktop-polybar")>> because whenever a noweb expression expands into multiple lines, for each new line noweb duplicates contents between the start of the line and the start of the expression.
+
One reason this is so is to support languages where indentation is a part of the syntax, for instance, Python:
+
classTestClass:
+<<class-contents>>
+
So every line of <<class-contents>> will be indented appropriately. In our case though, it is a minor inconvenience to be aware of.
+
Noweb evaluations
+
One note is that by default running these commands will require the user to confirm evaluation of each code block. To avoid that, I set org-confirm-babel-evaluate to nil:
I used to use dired+, which provides a lot of extensions for dired functionality, but it also creates some new problems, so I opt out of it. Fortunately, the one feature I want from this package - adding more colors to dired buffers - is available as a separate package.
Display git info, such as the last commit for file and stuff. It’s pretty useful but also slows down Dired a bit, hence I don’t turn it out by default.
TRAMP is a package that provides remote editing capacities. It is particularly useful for remote server management.
+
Unfortunately, many Emacs packages don’t exactly moderate their rate of filesystem operations, and on TRAMP over network each operation adds additional overhead, so… it can get pretty slow. To debug these issues, set the following variable to 6:
+
(setqtramp-verbose6)
+
I used to launch a separate Emacs instance for TRAMP, which had some of these packages disabled via environment variables, my advice-fu got better since then.
A package used by Magit to use the current Emacs instance as the $EDITOR.
+
That is, with the help of this function, I can just write e <filename>, edit the file, and then return to the same vterm buffer. No more running vim inside Emacs.
(defunmy/elfeed-show-visit-eww ()
+"Visit the current entry in eww"
+ (interactive)
+ (let ((link (elfeed-entry-linkelfeed-show-entry)))
+ (whenlink
+ (ewwlink))))
+
Custom faces
+
Setting up custom faces for certain tags to make the feed look a bit nicer.
+
(deffaceelfeed-videos-entrynil
+"Face for the elfeed entries with tag \"videos\"")
+
+(deffaceelfeed-twitter-entrynil
+"Face for the elfeed entries with tah \"twitter\"")
+
+(deffaceelfeed-emacs-entrynil
+"Face for the elfeed entries with tah \"emacs\"")
+
+(deffaceelfeed-music-entrynil
+"Face for the elfeed entries with tah \"music\"")
+
+(deffaceelfeed-podcasts-entrynil
+"Face for the elfeed entries with tag \"podcasts\"")
+
+(deffaceelfeed-blogs-entrynil
+"Face for the elfeed entries with tag \"blogs\"")
+
+(deffaceelfeed-govt-entrynil
+"Face for the elfeed entries with tag \"blogs\"")
+
+(my/use-colors
+ (elfeed-search-tag-face:foreground (my/color-value'yellow))
+ (elfeed-videos-entry:foreground (my/color-value'red))
+ (elfeed-twitter-entry:foreground (my/color-value'blue))
+ (elfeed-emacs-entry:foreground (my/color-value'magenta))
+ (elfeed-music-entry:foreground (my/color-value'green))
+ (elfeed-podcasts-entry:foreground (my/color-value'yellow))
+ (elfeed-blogs-entry:foreground (my/color-value'orange))
+ (elfeed-govt-entry:foreground (my/color-value'dark-cyan)))
+
+(with-eval-after-load'elfeed
+ (setqelfeed-search-face-alist
+'((podcastselfeed-podcasts-entry)
+ (musicelfeed-music-entry)
+ (govelfeed-govt-entry)
+ (twitterelfeed-twitter-entry)
+ (videoselfeed-videos-entry)
+ (emacselfeed-emacs-entry)
+ (blogselfeed-blogs-entry)
+ (unreadelfeed-search-unread-title-face))))
+
elfeed-summary
+
elfeed-summary is my package that provides a feed summary interface for elfeed.
+
The default interface of elfeed is just a list of all entries, so it gets hard to navigate when there are a lot of sources with varying frequencies of posts. This is my attempt to address this issue.
rdrview is a command-line tool to strip webpages from clutter, extracting only parts related to the actual content. It’s a standalone port of the corresponding feature of Firefox, called Reader View.
+
+
+
+
Guix dependency
+
+
+
+
+
rdrview
+
+
+
+
It seems like the tool isn’t available in a whole lot of package repositories, but it’s pretty easy to compile. I’ve put together a Guix definition, which one day I’ll submit to the upstream.
+
Integrating rdrview with Emacs
+
Let’s start by integrating rdrview with Emacs. In the general case, we want to fetch both metadata and the actual content from the page.
+
However, the interface of rdrview is a bit awkward in this part, so we have the following options:
+
+
call rdrview two times: with -M flag to fetch the metadata, and without the flag to fetch the HTML;
+
call rdrview with -T flag to append the metadata to the resulting HTML.
+
+
I’ve decided to go with the second option. Here is a function that calls rdrview with the required flags:
The function calls callback with the output of rdrview. This usually doesn’t take long, but it’s still nice to avoid freezing Emacs that way.
+
Now we have to parse the output. The -T flag puts the title in the <h1> tag, the site name site in the <h2> tag, and the content in a <div>. What’s more, headers of the content are often shifted, e.g. the top-level header may well end up being and <h2> or <h3>, which does not look great in LaTeX.
+
With that said, here’s a function that does the required changes:
Because I didn’t find a smart way to advise the desired behavior into elfeed, here’s a modification of the elfeed-show-refresh--mail-style function with two changes:
+
+
it uses rdrview to fetch the HTML;
+
it saves the resulting HTML into a buffer-local variable (we’ll need that later).
Of my other subscriptions, it does a pretty good job with The Verge, which by default sends entries truncated by the words “Read the full article”. For Ars Technica, it works only if the story is not large enough, otherwise the site returns its HTML-based pagination interface.
+
For paywalled sites such as New York Times or The Economist, this usually doesn’t work (by the way, what’s the problem with providing individual RSS feeds for subscribers?). If you need this kind of thing, I’d suggest using the RSS-Bridge project. And if something is not available, contributing business logic there definitely makes more sense than implementing workarounds in Emacs Lisp.
+
LaTeX and pandoc
+
However, I also find that I’m not really a fan of reading articles from Emacs. Somehow what works for program code doesn’t work that well for natural text. When I have to, I usually switch the Emacs theme to a light one.
+
But the best solution I’ve found so far is to render the required articles as PDFs. I may even print out some large articles I want to read.
+
Template
+
So first, we need a LaTeX template. Pandoc already ships with one, but I don’t like it too much, so I’ve put up a template from my LaTeX styles, targeting my preferred XeLaTeX engine.
+
The code for the template is available dotfiles repo. If you use LaTeX, you’ll probably be better off using your own setup. Be sure to define the following variables:
+
+
main-lang and other-lang for polyglossia (or remove them if you have only one language)
+
title
+
subtitle
+
author
+
date
+
+
Invoking pandoc
+
Now that we have the template, let’s save it somewhere and store the path to a variable:
And let’s invoke pandoc. We need to pass the following flags:
+
+
--pdf-engine=xelatex, of course
+
--template <path-to-template>;
+
-o <path-to-pdf>;
+
--variable key=value.
+
+
In fact, pandoc is a pretty awesome tool in the sense that it allows for feeding custom variables to rich-language templates.
+
So, the rendering function is as follows:
+
(cl-defunmy/rdrview-render (contenttypevariablescallback
+&keyfile-nameoverwrite)
+"Render CONTENT with pandoc.
+
+TYPE is a file extension as supported by pandoc, for instance,
+html or txt. VARIABLES is an alist that is fed into the
+template. After the rendering is complete successfully, CALLBACK
+is called with the resulting PDF.
+
+FILE-NAME is a path to the resulting PDF. If nil it's generated
+randomly.
+
+If a file with the given FILE-NAME already exists, the function will
+invoke CALLBACK straight away without doing the rendering, unless
+OVERWRITE is non-nil."
+ (unlessfile-name
+ (setqfile-name (format"/tmp/%d.pdf" (random100000000))))
+ (let (params
+ (temp-file-name (format"/tmp/%d.%s" (random100000000) type)))
+ (cl-loopfor (key.value) invariables
+whenvalue
+do (progn
+ (push"--variable"params)
+ (push (format"%s=%s"keyvalue) params)))
+ (setqparams (nreverseparams))
+ (if (and (file-exists-pfile-name) (notoverwrite))
+ (funcallcallbackfile-name)
+ (with-temp-filetemp-file-name
+ (insertcontent))
+ (let ((proc (apply#'start-process
+"pandoc" (get-buffer-create"*Pandoc*") "pandoc"
+temp-file-name"-o"file-name
+"--pdf-engine=xelatex""--template"my/rdrview-template
+params)))
+ (set-process-sentinel
+proc
+ (lambda (process_msg)
+ (let ((status (process-statusprocess))
+ (code (process-exit-statusprocess)))
+ (cond ((and (eqstatus'exit) (=code0))
+ (progn
+ (message"Done!")
+ (funcallcallbackfile-name)))
+ ((or (and (eqstatus'exit) (>code0))
+ (eqstatus'signal))
+ (user-error"Error in pandoc. Check the *Pandoc* buffer"))))))))))
+
Opening elfeed entries
+
Now we have everything required to open elfeed entries.
+
Also, in my case elfeed entries come in two languages, so I have to set main-lang and other-lang variables accordingly. Here’s the main function:
If the my/elfeed-show-rdrview-html variable is bound and true, then the content in this buffer was retrieved via rdrview, so we’ll use that instead of the output of elfeed-deref.
Now we can open elfeed entries in a PDF viewer, which I find much nicer to read. Given that RSS feeds generally ship with simpler HTML than the regular websites, results usually look awesome.
+
Opening arbitrary sites
+
As you may have noticed, we also can display arbitrary web pages with this setup, so let’s go ahead and implement that:
Unfortunately, this part doesn’t work that well, so we can’t just uninstall Firefox or Chromium and browse the web from a PDF viewer.
+
The most common problem I’ve encountered is incorrectly formed pictures, such as .png files without the boundary info. I’m sure you’ve also come across this if you ever tried to insert a lot of Internet pictures into a LaTeX document.
+
However, sans the pictures issue, for certain sites like Wikipedia this is usable.
+
YouTube transcripts
+
Getting subtitles
+
Finally, let’s get to transcripts.
+
+
+
+
Guix package
+
+
+
+
+
python-youtube-transcript-api
+
+
+
+
In principle, the YouTube API allows for downloading subtitles, but I’ve found this awesome Python script which does the same. You can install it from pip, or here’s mine Guix definition once again.
+
Much like the previous cases, we need to invoke the program and save the output. The WebVTT format will work well enough for our purposes. Here comes the function:
+
(cl-defunmy/youtube-subtitles-get (video-idcallback&keyfile-nameoverwrite)
+"Get subtitles for VIDEO-ID in WebVTT format.
+
+Call CALLBACK when done.
+
+FILE-NAME is a path to the resulting WebVTT file. If nil it's
+generated randomly.
+
+If a file with the given FILE-NAME already exists, the function will
+invoke CALLBACK straight away without doing the rendering, unless
+OVERWRITE is non-nil."
+ (interactive (list (read-string"Video ID: ")
+ (lambda (file-name)
+ (find-filefile-name))
+:file-namenil
+:overwritet))
+ (unlessfile-name
+ (setqfile-name (format"/tmp/%d.vtt" (random100000000))))
+ (if (and (file-exists-pfile-name) (notoverwrite))
+ (funcallcallbackfile-name)
+ (let* ((buffer (generate-new-buffer"youtube-transcripts"))
+ (proc (start-process"youtube_transcript_api"buffer
+"youtube_transcript_api"video-id
+"--languages""en""ru""de"
+"--format""webvtt")))
+ (set-process-sentinel
+proc
+ (lambda (process_msg)
+ (let ((status (process-statusprocess))
+ (code (process-exit-statusprocess)))
+ (cond ((and (eqstatus'exit) (=code0))
+ (progn
+ (with-current-buffer (process-bufferprocess)
+ (setqbuffer-file-namefile-name)
+ (save-buffer))
+ (kill-buffer (process-bufferprocess))
+ (funcallcallbackfile-name)))
+ ((or (and (eqstatus'exit) (>code0))
+ (eqstatus'signal))
+ (let ((err (with-current-buffer (process-bufferprocess)
+ (buffer-string))))
+ (kill-buffer (process-bufferprocess))
+ (user-error"Error in youtube_transcript_api: %s"err)))))))
+proc)))
+
elfeed and subed
+
Now that we have a standalone function, let’s invoke it with the current elfeed-show-entry:
+
(setqmy/elfeed-srt-dir (expand-file-name"~/.elfeed/srt/"))
+
+(defunmy/elfeed-youtube-subtitles (entry&optionalarg)
+"Get subtitles for the current elfeed ENTRY.
+
+Works only in the entry is a YouTube video.
+
+If ARG is non-nil, re-fetch the subtitles regardless of whether
+they were fetched before."
+ (interactive (listelfeed-show-entrycurrent-prefix-arg))
+ (let ((video-id (cadr
+ (assoc"watch?v"
+ (url-parse-query-string
+ (substring
+ (url-filename
+ (url-generic-parse-url (elfeed-entry-linkentry)))
+1))))))
+ (unlessvideo-id
+ (user-error"Can't get video ID from the entry"))
+ (my/youtube-subtitles-get
+video-id
+ (lambda (file-name)
+ (with-current-buffer (find-file-other-windowfile-name)
+ (setq-localelfeed-show-entryentry)
+ (goto-char (point-min))))
+:file-name (concatmy/elfeed-srt-dir
+ (elfeed-ref-id (elfeed-entry-contententry))
+".vtt")
+:overwritearg)))
+
That opens up a .vtt buffer with the subtitles for the current video, which means now we can use the functionality of Sacha Chua’s awesome package called subed.
+
This package, besides syntax highlighting, allows for controlling the MPV playback, for instance by moving the cursor in the subtitles buffer. Using that requires having the URL of the video in this buffer, which necessitates the line with setq-local in the previous function.
+
Also, the package launches its own instance of MPV to control it via JSON-IPC, so there seems to be no easy way to integrate it with EMMS. But at least I can reuse the emms-player-mpv-parameters variable, the method of setting which I’ve discussed above. The function is as follows:
+
(defunmy/subed-elfeed (entry)
+"Open the video file from elfeed ENTRY in MPV.
+
+This has to be launched from inside the subtitles buffer, opened
+by the `my/elfeed-youtube-subtitles' function."
+ (interactive (listelfeed-show-entry))
+ (unlessentry
+ (user-error"No entry!"))
+ (unless (derived-mode-p'subed-mode)
+ (user-error"Not subed mode!"))
+ (setq-localsubed-mpv-arguments
+ (seq-uniq
+ (appendsubed-mpv-argumentsemms-player-mpv-parameters)))
+ (setq-localsubed-mpv-video-file (elfeed-entry-linkentry))
+ (subed-mpv--playsubed-mpv-video-file))
+
Keep in mind that this function has to be launched inside the buffer opened by the my/elfeed-youtube-subtitles function.
+
Podcast transcripts
+
In my experience, finding something in a podcast can be particularly troublesome. For instance, at times, I want to refer to a specific line in the podcast to make an org-roam node, and I need to check if I got that part right. And I have no reasonable way to get there because audio files, in themselves, don’t allow for random access, i.e. there are no “landmarks” that point to a particular portion of the file. At least if nothing like a transcript is available.
+
For obvious reasons, podcasts rarely ship with transcripts. So in this post section I’ll be using a speech recognition engine to make up for that. The general idea is to obtain the podcast information from elfeed, process it with OpenAI Whisper and feed it to subed to control the playback in MPV.
+
Edit <2022-10-08 Sat>: Changed vosk-api to OpenAI Whisper.
+
Whisper
+
OpenAI Whisper is an amazing speech recognition toolkit.
+
The implementation by OpenAI is rather slow on my PC (speed around 0.75 on tiny.en), but whisper.cpp by Georgi Gerganov works much faster (5.9x). I’ve packaged the latter for Guix.
+
+
+
+
Guix dependency
+
+
+
+
+
whisper-cpp
+
+
+
+
Running it from Emacs
+
Running the program from Emacs is rather straightforward with asyncronous processes.
+
I’m using an English-language-only model because that’s the only language I need at the moment.
+
(defunmy/invoke-whisper--direct (inputoutput-dirremove-wav)
+"Extract subtitles from a WAV audio file.
+
+INPUT is the absolute path to audio file, OUTPUT-DIR is the path to
+the directory with resulting files."
+ (let* ((default-directoryoutput-dir)
+ (buffer (generate-new-buffer"whisper"))
+ (proc (start-process
+"whisper"buffer
+"whisper-cpp""--model""/home/pavel/.whisper/ggml-tiny.en.bin"
+"-otxt""-ovtt""-osrt"input)))
+ (set-process-sentinel
+proc
+ (lambda (process_msg)
+ (let ((status (process-statusprocess))
+ (code (process-exit-statusprocess)))
+ (cond ((and (eqstatus'exit) (=code0))
+ (notifications-notify:body"Audio conversion completed"
+:title"Whisper")
+ (whenremove-wav
+ (delete-fileinput))
+ (dolist (extension'(".txt"".vtt"".srt"))
+ (rename-file (concatinputextension)
+ (concat (file-name-sans-extensioninput) extension)))
+ (kill-buffer (process-bufferprocess)))
+ ((or (and (eqstatus'exit) (>code0))
+ (eqstatus'signal))
+ (let ((err (with-current-buffer (process-bufferprocess)
+ (buffer-string))))
+ (user-error"Error in Whisper: %s"err)))))))))
+
+(defunmy/invoke-whisper (inputoutput-dir)
+"Extract subtitles from the audio file.
+
+INPUT is the absolute path to the audio file, OUTPUT-DIR is the path
+to the directory with resulting files.
+
+Run ffmpeg if the file is not WAV."
+ (interactive
+ (list
+ (read-file-name"Input file: "nilnilt)
+ (read-directory-name"Output directory: ")))
+ (if (string-match-p (rx".wav"eos) input)
+ (my/invoke-whisper--directinputoutput-dir)
+ (let* ((ffmpeg-proc
+ (start-process
+"ffmpef"nil"ffmpeg""-i"input"-ar""16000""-ac""1""-c:a"
+"pcm_s16le" (concat (file-name-sans-extensioninput) ".wav"))))
+ (set-process-sentinel
+ffmpeg-proc
+ (lambda (process_msg)
+ (let ((status (process-statusprocess))
+ (code (process-exit-statusprocess)))
+ (cond ((and (eqstatus'exit) (=code0))
+ (my/invoke-whisper--direct
+ (concat (file-name-sans-extensioninput) ".wav") output-dirt))
+ ((or (and (eqstatus'exit) (>code0))
+ (eqstatus'signal))
+ (let ((err (with-current-buffer (process-bufferprocess)
+ (buffer-string))))
+ (user-error"Error in running ffmpeg: %s"err))))))))))
+
If run interactively, the defined function prompts for paths to both files.
+
The process sentinel sends a desktop notification because it’s a bit more noticeable than message, and the process is expected to take some time.
+
Integrating with elfeed
+
To actually run the function from the section above, we need to download the file in question.
+
The whisper executable, given the file <file>.<extension>, creates files named <file>.vtt, <file>.srt, <file>.txt. So first we need to save the file under the correct name.
+
I use a library called request.el to download files elsewhere, so I’ll re-use it here. You can just as well invoke curl or wget via a asynchronous process.
+
This function downloads the file to a non-temporary folder, which is ~/.elfeed/podcast-files/ if you didn’t move the elfeed database. That is so because a permanently downloaded file works better for the next section.
I also experimented with a bunch of options to write binary data in Emacs, of which the way with write-region (as implemented in f.el) seems to be the fastest. This thread on StackExchange suggests that it may screw some bytes towards the end, but whether or not this is the case, mp3 files survive the procedure. The proposed solution with seq-doseq takes at least a few seconds.
+
As my/invoke-whisper creates multiple files, here’s a function to select related files:
Finally, we need a function to show the transcript if it exists or invoke my/elfeed-whisper-get-transcript-new if it doesn’t. And this is the function that we’ll call from an elfeed-entry buffer.
+
(defunmy/elfeed-whisper-get-transcript (entry)
+"Retrieve transcript for the enclosure of the current elfeed ENTRY."
+ (interactive (listelfeed-show-entry))
+ (let ((enclosure (caar (elfeed-entry-enclosuresentry))))
+ (unlessenclosure
+ (user-error"No enclosure found!"))
+ (let ((srt-path (concatmy/elfeed-srt-dir
+ (elfeed-ref-id (elfeed-entry-contententry))
+".srt")))
+ (if (file-exists-psrt-path)
+ (let ((buffer (find-file-other-windowsrt-path)))
+ (with-current-bufferbuffer
+ (setq-localelfeed-show-entryentry)))
+ (my/elfeed-whisper-get-transcript-newentry)))))
+
Integrating with subed
+
Now that we’ve produced a .srt file, we can use a package called subed to control the playback, as I had done in the previous post.
+
By the way, this wasn’t the most straightforward thing to figure out, because the MPV window doesn’t show up for an audio file, and the player itself starts in the paused state. So I thought nothing was happening until I enabled the debug log.
+
With that in mind, here’s a function to launch MPV from the buffer generated by my/elfeed-whisper-get-transcript:
+
(defunmy/elfeed-whisper-subed (entry)
+"Run MPV for the current Whisper-generated subtitles file.
+
+ENTRY is an instance of `elfeed-entry'."
+ (interactive (listelfeed-show-entry))
+ (unlessentry
+ (user-error"No entry!"))
+ (unless (derived-mode-p'subed-mode)
+ (user-error"Not subed mode!"))
+ (setq-localsubed-mpv-video-file
+ (expand-file-name
+ (concatmy/elfeed-whisper-podcast-files-directory
+ (my/get-file-name-from-url
+ (caar (elfeed-entry-enclosuresentry))))))
+ (subed-mpv--playsubed-mpv-video-file))
+
After running M-x my/elfeed-whisper-subed, run M-x subed-toggle-loop-over-current-subtitle (C-c C-l), because somehow it’s turned on by default, and M-x subed-toggle-pause-while-typing (C-c C-p), because sometimes this made my instance of MPV lag.
+
After that, M-x subed-mpv-toggle-pause should start the playback, which you can control by moving the cursor in the buffer.
+
You can also run M-x subed-toggle-sync-point-to-player (C-c .) to toggle syncing the point in the buffer to the currently played subtitle (this automatically gets disabled when you switch buffers).
+
Running M-x subed-toggle-sync-player-to-point (C-c ,) does the opposite, i.e. sets the player position to the subtitle under point. These two functions are useful since the MPV window controls aren’t available.
+
Running it for random files
+
Apparently I also need to run whisper for random files from the Internet.
Vosk API works much faster than Whisper. The smallest Vosk model requires ~10 times less than the playback time, and even the tiny.en Whisper model on my PC requires maybe 1.2x playback time.
+
However, the quality of the output for Whisper is just so much better so I consider it to be worth the wait. Even with the tiny model, the transcript is almost perfect, provided that the audio is of reasonable quality.
Set a custom regex for MPD. EMMS sets up the default one from MPD’s diagnostic output so that regex opens basically everything, including videos, https links, etc. That is fine if MPD is the only player in EMMS, but as I want to use MPV as well, I override the regex.
After all this is done, run M-x emms-cache-set-from-mpd-all to set cache from MPD. If everything is correct, EMMS browser will be populated with MPD database.
+
MPV
+
+
+
+
Guix dependency
+
+
+
+
+
mpv
+
+
+
yt-dlp
+
+
+
+
mpv is a decent media player, which has found a place in this configuration because it integrates with youtube-dl yt-dlp.
+
It looks like YouTube has started to throttle youtube-dl, and yt-dlp has a workaround for that particular case. Just don’t forget to add the following like to the mpv config:
+
script-opts=ytdl_hook-ytdl_path=yt-dlp
+
It seems a bit strange to keep the MPV config in this file, but I don’t use the program outside Emacs.
+
(add-to-list'emms-player-list'emms-player-mpvt)
+
Also a custom regex. My demands for MPV include running yt-dlp, so there is a regex that matches youtube.com or some of the video formats.
By default, MPV plays the video in the best possible quality, which may be pretty high, even too high with limited bandwidth. So here is the logic to choose the quality.
Now emms-add-url should work on YouTube URLs just fine. Just keep in mind that it will only add the URL to the playlist, not play it right away.
+
Cache cleanup
+
All the added URLs reside in the EMMS cache after being played. I don’t want them to stay there for a long time, so here is a handy function to clean it.
The original implementation creates a new alist whenever it encounters a tag it has already put in the current alist. Which doesn’t work too well if some tags don’t repeat, if the order is messed up, etc.
+
Fortunately, according to the protocol specification, each new record has to start with file, directory or playlist. I’ve overridden the function with that in mind and it fixed the import, at least for my case.
+
(defunemms-player-mpd-get-alists (info)
+"Turn the given parsed INFO from MusicPD into an list of alists.
+
+The list will be in reverse order."
+ (when (andinfo
+ (null (carinfo)) ; no error has occurred
+ (cdrinfo)) ; data exists
+ (let ((alistsnil)
+ (alistnil)
+cell)
+ (dolist (line (cdrinfo))
+ (when (setqcell (emms-player-mpd-parse-lineline))
+ (if (member (carcell) '("file""directory""playlist"))
+ (setqalists (consalistalists)
+alist (listcell))
+ (setqalist (conscellalist)))))
+ (whenalist
+ (setqalists (consalistalists)))
+alists)))
+
ytel
+
ytel is a YouTube (actually Invidious) frontend, which lets one search YouTube (whereas the setup with elfeed just lets one view the pre-defined subscriptions).
+
Package config
+
The package doesn’t provide evil bindings, so I define my own.
Invidious instances aren’t particularly reliable, but there plenty of them, and there’s an API at invidious.io that returns the available instances and their health, so we can use that.
(defunmy/ytel-instances-fetch-json ()
+"Fetch list of invidious instances as json, sorted by health."
+ (let
+ ((url-request-method"GET")
+ (url-request-extra-headers
+'(("Accept"."application/json"))))
+ (with-current-buffer
+ (url-retrieve-synchronouslymy/invidious-instances-url)
+ (goto-char (point-min))
+ (re-search-forward"^$")
+ (let* ((json-object-type'alist)
+ (json-array-type'list)
+ (json-key-type'string))
+ (json-read)))))
+
+(defunmy/ytel-instances-alist-from-json ()
+"Make the json of invidious instances into an alist."
+ (let ((jsonlist (my/ytel-instances-fetch-json))
+ (inst ()))
+ (whilejsonlist
+ (push (concat"https://" (caarjsonlist)) inst)
+ (setqjsonlist (cdrjsonlist)))
+ (nreverseinst)))
+
+(defunmy/ytel-choose-instance ()
+"Prompt user to choose an invidious instance to use."
+ (interactive)
+ (setqytel-invidious-api-url
+ (or (condition-casenil
+ (completing-read"Using instance: "
+ (cl-subseq (my/ytel-instances-alist-from-json) 011) nil"confirm""https://")
+ (errornil))
+"https://invidious.synopyta.org")))
+
Some fixes
+
At some point in the last 2 years, Invidious started to return videos with null fields. I have no idea what causes that, but I suspect it’s related to YouTube Music.
+
ytel hasn’t been updated in these two years, so it doesn’t account for that change.
alphapapa 🐃> And, yes, that is a currently unsolved problem. As I said, in the future we can try using a different API endpoint to access those notifications similarly to Element. In the meantime, you can load old messages (e.g. “C-u 1000 M-v” to load 1000 old ones at a time), until you find it, maybe using “C-s sqrtm” to search for messages mentioning you.
+
Or you can load up Element for a moment to see what the mention was, if that’s easier.
Building telega-server can create problems. It requires the latest version of tdlib, which isn’t available anywhere, but I can inherit the Guix package definition.
And custom online status. By default it marks you online when the Emacs frame is active, but I use EXWM, so I change that to when telega.el buffer is active. Otherwise, I’m online all the time.
tldr is a collaborative project providing cheatsheets for various console commands. For some reason, the built-in download in the package is broken, so I use my own function.
My filesystem is, shall we say, not the most orderly place.
+
+
+
+
It’s been somewhat messy, and messy in different ways across my three machines. For instance, my laptop had work projects in ~/Code/Job, my work machine had just ~/Code, and so forth.
+
Strangely, I couldn’t find and existing solution to that problem. Surely, I can’t be the only one facing that issue, can I?
+
Fortunately, I’m well-acquainted with (make-yourself-a) Swiss Army Knife of computing called Emacs, so… below is my attempt to make something of it. And another addition to the already substantial list of my Emacs uses.
+
Idea
+
So, I decided to try declarative filesystem management.
+
At the core is my work-in-progress adaptation of Johnny.Decimal. Essentially, it suggests prefixing your folders with numbers like 12.34, where:
The point is to organize your folder structure, limiting its depth for quicker and more straightforward access. Check the website for a more thorough description.
+
So, what I want is to:
+
+
define a Jonny.Decimal-esque file tree in a single Org file;
+
have different nodes of that file tree active on different machines, e.g. I don’t want my Emacs stuff on my work machine;
+
use different tools to sync different nodes (currently git, MEGA, and “nothing”).
+
+
Folder structure
+
As I said, I tried (and still trying) to adapt the proposed scheme to better suit my needs. Here’s a subset of my current tree:
+
10-19 Code
+ 10 [REDACTED]
+ 10.02 Digital Schedule ; project root
+ 10.03 Digital Trajectories ; project root
+ 12 My Emacs Packages
+ 12.01 lyrics-fetcher.el ; managed by git
+ 12.02 pomm.el ; managed by git
+ 15 Other Projects
+ 15.04 ZMU_2022 ; I'm done with this and don't need it on any machine
+20-29 Education
+ 24 Publications ; the entrire area is managed by MEGA
+ 24.Y20.01 [bibtex code]
+ 24.Y20.02 [bibtex code]
+ 26 Students
+ 26.Y22.01 [student name]
+30-39 Life
+ 32 org-mode
+ 33 Library
+
The root of the tree is my $HOME. The entry at the third (or second) level can be either an entity itself (such as a git repository), or a “project root”.
+
In several places, I use year references (Y20) instead of the plain AC.ID. This is mainly to group things by academic years, e.g. to find all my publications or students in a specific year, which I need for occasional reports. I also have semester references (SEM10) for my undergraduate studies.
+
The project structure is more or less standard. Johnny.Decimal proposes using PRO.AC.ID to manage multiple projects, but this doesn’t seem to fit quite as well in my case. So I came up with the following:
+
10.03 Digital Trajectories ; project root
+ 10.03.A Artifacts ; managed by MEGA
+ 10.03.A.04 library queries (Jan 23)
+ 10.03.D Documents ; managed by MEGA
+ 10.03.D.01 Initial design
+ 10.03.R Repos
+ 10.03.R.00 digital-trajectories-deploy ; managed by MEGA
+ 10.03.R.01 digital-trajectories-backend ; managed by git
+ 10.03.U Dumps ; managed by nothing, no need to sync this
+
I also use year references on the third level for courses I happen to teach across multiple academic years.
+
Perhaps this is too verbose (10.03.R.01), but it works for now.
+
Tools choice
+
As I mentioned earlier, my current options to manage a particular node are:
MEGA - for files that don’t fit into git, such as DOCX documents, photos, etc.;
+
“nothing” - for something that I don’t need to sync across machines, e.g. database dumps.
+
+
Another tool I considered was restic. It’s an interesting backup & sync solution with built-in encryption, snapshots, etc.
+
However, a challenge I encountered is that its repositories are only accessible via restic. So, even if I use something like MEGA as a backend, I won’t be able to use the MEGA file-sharing features, which I occasionally want for document or photo folders. Hence, for now, I’m more interested in synchronizing the file tree in MEGA with MEGAcmd (and also clean up the mess up there).
+
Another interesting tool is rclone, which provides a single interface for multiple services like Google Drive, Dropbox, S3, WebDAV. It also supports MEGA, but it requires turning off the two-factor authentication, which I don’t want.
So, let’s parse the Org tree. This is done by recursively traversing the tree returned by org-element-parse-buffer.
+
(defunmy/index--tree-get-recursive (heading&optionalpath)
+"Read the index tree recursively from HEADING.
+
+HEADING is an org-element of type `headline'.
+
+If PATH is provided, it is the path to the current node. If not
+provided, it is assumed to be the root of the index.
+
+The return value is an alist; see `my/index--tree-get' for details."
+ (when (eq (org-element-typeheading) 'headline)
+ (let (val
+ (new-path (concat
+ (orpathmy/index-root)
+ (org-element-property:raw-valueheading)
+"/")))
+ (when-let* ((children (thread-last
+ (org-element-contentsheading)
+ (mapcar (lambda (e)
+ (my/index--tree-get-recursive
+enew-path)))
+ (seq-filter#'identity))))
+ (setf (alist-get:childrenval) children))
+ (when-let ((machine (org-element-property:MACHINEheading)))
+ (setf (alist-get:machineval) (split-stringmachine)))
+ (when-let ((symlink (org-element-property:SYMLINKheading)))
+ (setf (alist-get:symlinkval) symlink))
+ (when (org-element-property:PROJECTheading)
+ (setf (alist-get:projectval) t))
+ (when-let* ((kind-str (org-element-property:KINDheading))
+ (kind (internkind-str)))
+ (setf (alist-get:kindval) kind)
+ (when (equalkind'git)
+ (let ((remote (org-element-property:REMOTEheading)))
+ (unlessremote
+ (user-error"No remote for %s" (alist-get:nameval)))
+ (setf (alist-get:remoteval) remote))))
+ (setf (alist-get:nameval) (org-element-property:raw-valueheading)
+ (alist-get:pathval) new-path)
+val)))
+
+(defunmy/index--tree-get ()
+"Read the index tree from the current org buffer.
+
+The return value is a list of alists, each representing a
+folder/node. Alists can have the following keys:
+- `:name'
+- `:path'
+- `:children' - child nodes
+- `:machine' - list of machines on which the node is active
+- `:symlink' - a symlink to create
+- `:kind' - one of \"git\", \"mega\", or \"dummy\"
+- `:remote' - the remote to use for git nodes"
+ (let* ((tree
+ (thread-last
+ (org-element-map (org-element-parse-buffer) 'headline#'identity)
+ (seq-filter (lambda (el)
+ (and
+ (= (org-element-property:levelel) 1)
+ (seq-contains-p
+ (mapcar#'substring-no-properties (org-element-property:tagsel))
+"folder"))))
+ (mapcar#'my/index--tree-get-recursive))))
+tree))
+
Verify tree
+
I also want to make sure that I didn’t mess up the numbers, i.e., didn’t place 10.02 under 11, and so on.
+
To do that, we first need to extract the number from the name:
+
(defunmy/index--extact-number (name)
+"Extract the number from the index NAME.
+
+NAME is a string. The number is the first sequence of digits, e.g.:
+- 10-19
+- 10.01
+- 10.01.Y22.01"
+ (save-match-data
+ (string-match (rxbos (+ (|numalpha".""-"))) name)
+ (match-string0name)))
+
Then, we can recursively verify the numbers:
+
(defunmy/tree--verfify-recursive (elem&optionalcurrent)
+"Verify that ELEM is a valid tree element.
+
+CURRENT is the current number or name of the parent element."
+ (let* ((name (alist-get:nameelem))
+ (number (my/index--extact-numbername)))
+ (unlessnumber
+ (user-error"Can't find number: %s"name))
+ (cond
+ ((and (listpcurrent) (not (nullcurrent)))
+ (unless (seq-some (lambda (cand) (string-prefix-pcandname)) current)
+ (user-error"Name: %s doesn't match: %s"namecurrent)))
+ ((stringpcurrent)
+ (unless (string-prefix-pcurrentname)
+ (user-error"Name: %s doesn't match: %s"namecurrent))))
+ (let ((recur-value
+ (if (string-match-p (rx (+num) "-" (+num)) number)
+ (let* ((borders (split-stringnumber"-"))
+ (start (string-to-number (nth0borders)))
+ (end (string-to-number (nth1borders))))
+ (cl-loopforifromstartto (1-end) collect (number-to-stringi)))
+number)))
+ (mapcar (lambda (e) (my/tree--verfify-recursiveerecur-value))
+ (alist-get:childrenelem))))
+t)
+
+(defunmy/index--tree-verify (tree)
+"Verify that TREE is a valid tree.
+
+Return t if it is valid, otherwise raise an error.
+
+See `my/index--tree-get' for the format of TREE."
+ (mapcar#'my/tree--verfify-recursivetree))
+
Narrow tree
+
Finally, we need to narrow the tree to only leave nodes that are active for the current machine.
+
(defunmy/index--tree-narrow-recursive (elemmachine)
+"Remove all children of ELEM that are not active on MACHINE."
+ (unless (when-let ((elem-machines (alist-get:machineelem)))
+ (not (seq-some (lambda (elem-machine)
+ (string-equalelem-machinemachine))
+elem-machines)))
+ (setf (alist-get:childrenelem)
+ (seq-filter
+#'identity
+ (mapcar (lambda (e)
+ (my/index--tree-narrow-recursiveemachine))
+ (alist-get:childrenelem))))
+elem))
+
+(defunmy/index--tree-narrow (tree)
+"Remove all elements of TREE that are not active on machine."
+ (seq-filter
+#'identity
+ (mapcar
+ (lambda (elem) (my/index--tree-narrow-recursiveelem (system-name)))
+ (copy-treetree))))
+
my/index--tree-narrow
+
Commands
+
Next, apply the tree to the filesystem.
+
I’ve decided to implement this by generating a bash script and executing it with bash +x. This way, I can check the required changes in advance and avert potential data loss if something unexpected happens.
+
One command for the script will be a list like:
+
+
(<command> <category> <priority>)
+
+
Filesystem
+
First, we need to create non-existing folders and remove folders that aren’t supposed to exist.
+
To do that, we need to find all such folders:
+
(defunmy/index--filesystem-tree-mapping (full-treetree&optionalactive-paths)
+"Return a \"sync state\" between the filesystem and the tree.
+
+FULL-TREE and TREE are forms as defined by `my/index--tree-get'. TREE
+is the narrowed FULL-TREE (returned by `my/index--tree-narrow').
+
+ACTIVE-PATHS is a list of paths that are currently active. If not
+provided, it is computed from TREE.
+
+The return value is a list of alists with the following keys:
+- path - the path of the folder
+- exists - whether the folder exists on the filesystem
+- has-to-exist - whether the folder exists in the tree
+- extra - if the folder exists in the filesystem but not in the tree.
+- children - a list of alists with the same keys for the children of
+ the folder."
+ (let ((active-paths (oractive-paths (my/index--tree-get-pathstree))))
+ (cl-loopforeleminfull-tree
+forpath= (alist-get:pathelem)
+forextra-folders= (when (and (alist-get:childrenelem)
+ (file-directory-ppath))
+ (seq-difference
+ (mapcar (lambda (d) (if (file-directory-pd)
+ (concatd"/")
+d))
+ (directory-filespatht (rx (not".") eos)))
+ (cl-loopforchildin (alist-get:childrenelem)
+collect (alist-get:pathchild))))
+forfolder-exists= (file-directory-ppath)
+forfolder-has-to-exist= (seq-contains-pactive-pathspath)
+collect`((path.,path)
+ (exists.,folder-exists)
+ (has-to-exist.,folder-has-to-exist)
+ (children.,(append
+ (cl-loopforfinextra-folders
+collect`((path.,f)
+ (exists.t)
+ (has-to-exist.nil)
+ (extra.t)))
+ (my/index--filesystem-tree-mapping
+ (alist-get:childrenelem) treeactive-paths)))))))
+
And generate commands from the results of the above:
+
(defunmy/index--filesystem-commands (mapping)
+"Get commands to sync filesystem with the tree.
+
+MAPPING is a form generated by `my/index--filesystem-tree-mapping'
+that describes the \"sync state\" between the filesystem and the
+tree.
+
+The return value is a list of commands as defined by
+`my/index--commands-display'."
+ (cl-loopforeleminmapping
+forpath= (alist-get'pathelem)
+forexists= (alist-get'existselem)
+forhas-to-exist= (alist-get'has-to-existelem)
+forextra= (alist-get'extraelem)
+when (and (notexists) has-to-exist)
+collect (list (format"mkdir \"%s\""path) "Make directories"1)
+when (andexists (nothas-to-exist))
+collect (list (format"rm -rf \"%s\""path)
+ (ifextra"Remove extra files""Remove directories")
+ (ifextra2010))
+append (my/index--filesystem-commands (alist-get'childrenelem))))
+
MEGA
+
As I said above, MEGA provides MEGAcmd, which is a convenient way to access MEGA via CLI.
+
To initialize the session, run
+
mega-login <login> <password>
+
Then you’ll be able to run the rest of mega-* commands.
+
The command I want to run, mega-sync, prints the results in a table-like way. So let’s parse that.
+
(defunmy/parse-table-str (string)
+"Convert a table-like STRING into alist.
+
+The input format is as follows:
+HEADER1 HEADER2 HEADER3
+value1 value2 3
+value4 value5 6
+
+Which creates the following output:
+\(((HEADER1. \"value1\") (HEADER2 . \"value2\") (HEADER3 . \"3\"))
+ ((HEADER1. \"value4\") (HEADER2 . \"value5\") (HEADER3 . \"6\")))
+
+The functions also skips lines in [square brackets] and ones that
+start with more than 3 spaces."
+ (when-let* ((lines (seq-filter
+ (lambda (s) (not (or (string-empty-ps)
+ (string-match-p (rxbos"[" (*nonl) "]") s)
+ (string-match-p (rxbos (>=3" ")) s))))
+ (split-stringstring"\n")))
+ (first-line (carlines))
+ (headers (split-stringfirst-line))
+ (header-indices (mapcar
+ (lambda (header)
+ (cl-searchheaderfirst-line))
+headers)))
+ (cl-loopforlinein (cdrlines)
+collect (cl-loopforheaderinheaders
+forstartinheader-indices
+forendin (append (cdrheader-indices)
+ (list (lengthline)))
+collect (cons
+ (internheader)
+ (string-trim
+ (substringlinestartend)))))))
+
Now we can invoke mega-sync to get the current sync status. --path-display-size=10000 disables truncation of long paths.
+
(defunmy/index--mega-data-from-sync ()
+"Get the current MEGA sync status.
+
+The return value is a list of alists with the following keys:
+- path - path to file or directory
+- enabled - whether the file or directory is enabled for sync"
+ (let ((mega-result (my/parse-table-str
+ (shell-command-to-string"mega-sync --path-display-size=10000"))))
+ (cl-loopforvalueinmega-result
+forlocalpath= (alist-get'LOCALPATHvalue)
+collect`((path.,(if (file-directory-plocalpath)
+ (concatlocalpath"/")
+localpath))
+ (enabled.,(string-equal (alist-get'ACTIVEvalue)
+"Enabled"))))))
+
And get the same data from the tree.
+
(defunmy/index--tree-get-paths (tree&optionalkind)
+"Get paths from TREE.
+
+TREE is a form a defined by `my/index--tree-get'. KIND is either a
+filter by the kind attribute or nil, in which case all paths are
+returned.
+
+The return value is a list of strings."
+ (cl-loopforelemintree
+when (or (nullkind) (eq (alist-get:kindelem) kind))
+collect (alist-get:pathelem)
+append (my/index--tree-get-paths
+ (alist-get:childrenelem) kind)))
+
With that information, we can generate commands to synchronize the required and actual sync paths.
+
(defunmy/index--mega-local-path (path)
+"Get path in the MEGA cloud by the local path PATH."
+ (string-replacemy/index-root"/"path))
+
+(defunmy/index--mega-commands (full-treetree)
+"Get commands to sync the mega-sync state with TREE.
+
+FULL-TREE and TREE are forms as defined by `my/index--tree-get'. TREE
+is the narrowed FULL-TREE (returned by `my/index--tree-narrow').
+
+The return value is a list of commands as defined by
+`my/index--commands-display'."
+ (let* ((paths-all (my/index--tree-get-pathsfull-tree))
+ (mega-paths-to-enable (my/index--tree-get-pathstree'mega))
+ (mega-info (my/index--mega-data-from-sync))
+ (mega-paths-enabled (seq-map
+ (lambda (e) (alist-get'pathe))
+ (seq-filter (lambda (e) (alist-get'enablede))
+mega-info)))
+ (mega-paths-disabled (seq-map
+ (lambda (e) (alist-get'pathe))
+ (seq-filter (lambda (e) (not (alist-get'enablede)))
+mega-info))))
+ (append
+ (cl-loopforpathin (seq-differencemega-paths-to-enablemega-paths-enabled)
+if (seq-contains-pmega-paths-disabledpath)
+collect (list (format"mega-sync -e \"%s\""path) "Mega enable sync"5)
+elseappend (list
+ (list (format"mega-mkdir -p \"%s\""
+ (my/index--mega-local-pathpath))
+"Mega mkdirs"4)
+ (list (format"mega-sync \"%s\" \"%s\""
+path (my/index--mega-local-pathpath))
+"Mega add sync"5)))
+ (cl-loopforpathin (seq-difference
+ (seq-intersectionmega-paths-enabledpaths-all)
+mega-paths-to-enable)
+collect (list
+ (format"mega-sync -d \"%s\""
+ (substringpath0 (1- (lengthpath))))
+"Mega remove sync"4)))))
+
my/index--mega-commands
+
Git repos
+
To sync git, we just need to clone the required git repos. Removing the repos is handled by the folder sync commands.
+
(defunmy/index--git-commands (tree)
+"Get commands to clone the yet uncloned git repos in TREE.
+
+TREE is a form a defined by `my/index--tree-get'. This is supposed to
+be the tree narrowed to the current machine (`my/index--tree-narrow').
+
+The return value is a list of commands as defined by
+`my/index--commands-display'."
+ (cl-loopforelemintree
+forpath= (alist-get:pathelem)
+when (and (eq (alist-get:kindelem) 'git)
+ (or (not (file-directory-ppath))
+ (directory-empty-ppath)))
+collect (list (format"git clone \"%s\" \"%s\""
+ (alist-get:remoteelem)
+path)
+"Init git repos"2)
+append (my/index--git-commands (alist-get:childrenelem))))
+
Wakatime
+
So, that’s it for synchronization. A few other things are needed here.
+
I use WakaTime to track my coding activity, and I don’t like the alphanumeric prefixes in my coding stats. Fortunately, wakatime-cli provides an option called projectmap to rename projects, so we just have to generate its contents.
+
(defunmy/index--bare-project-name (name)
+"Remove the alphanumeric prefix from NAME.
+
+E.g. 10.03.R.01 Project Name -> Project Name."
+ (replace-regexp-in-string
+ (rxbos (+ (|numalpha".""-")) space) ""name))
+
+(defunmy/index--wakatime-escape (string)
+"Escape STRING for use in a WakaTime config file."
+ (thread-last
+string
+ (replace-regexp-in-string (rx"'") "\\\\'")
+ (replace-regexp-in-string (rx"(") "\\\\(")
+ (replace-regexp-in-string (rx")") "\\\\)")))
+
+(defunmy/index--wakatime-get-map-tree (tree)
+"Get a list of (folder-name . bare-project-name) pairs from TREE.
+
+TREE is a form as defined by `my/index--tree-get'.
+\"bare-project-name\" is project name without the alphanumeric
+prefix."
+ (cl-loopforelemintree
+forname= (alist-get:nameelem)
+if (eq (alist-get:kindelem) 'git)
+collect (cons (my/index--wakatime-escapename)
+ (my/index--wakatime-escape
+ (my/index--bare-project-namename)))
+if (and (eq (alist-get:kindelem) 'git)
+ (alist-get:symlinkelem))
+collect (cons (my/index--wakatime-escape
+;; lmao
+;; /a/b/c/ -> c
+;; /a/b/c -> b
+ (file-name-nondirectory
+ (directory-file-name
+ (file-name-directory (alist-get:symlinkelem)))))
+ (my/index--wakatime-escape
+ (my/index--bare-project-namename)))
+append (my/index--wakatime-get-map-tree (alist-get:childrenelem))))
+
And insert that in wakatime.cfg if necessary.
+
(defunmy/index--wakatime-commands (tree)
+"Get commands to update WakaTime config from TREE.
+
+TREE is a form a defined by `my/index--tree-get'. The return value is
+a list of commands as defined by `my/index--commands-display'."
+ (let* ((map-tree (my/index--wakatime-get-map-treetree))
+ (map-tree-encoding (ini-encode`(("projectmap".,map-tree))))
+ (map-tree-saved (with-temp-buffer
+ (insert-file-contents (expand-file-name"~/.wakatime.cfg"))
+ (string-match-p (regexp-quotemap-tree-encoding)
+ (buffer-string)))))
+ (unlessmap-tree-saved
+ (let ((insert-command (list (format"echo \"\n\n%s\" >> ~/.wakatime.cfg"
+map-tree-encoding)
+"Update WakaTime config"9)))
+ (list (list (format"sed -i -z 's/\\[projectmap\\]\\n[^[]*//g' ~/.wakatime.cfg")
+"Update WakaTime config"9)
+insert-command)))))
+
my/index--wakatime-commands
+
Symlinks
+
The last part here is creating symbolic links.
+
(defunmy/index-get-symlink-commands (tree)
+"Get commands to create symlinks from TREE.
+
+TREE is a form a defined by `my/index--tree-get'. The return value is
+a list of commands as defined by `my/index--commands-display'."
+ (cl-loopforelemintree
+forpath= (alist-get:pathelem)
+forsymlink= (alist-get:symlinkelem)
+when (andsymlink (not (string-match-p (rx"/"eos) symlink)))
+do (user-error"Wrong symlink: %s (should be a directory)"symlink)
+when (andpathsymlink
+ (or (file-exists-psymlink)
+ (file-exists-p (substringsymlink0-1)))
+ (not (file-symlink-p (substringsymlink0-1))))
+collect (list (format"rm -rf %s" (substringsymlink0-1))
+"Remove files to make symlinks"6)
+when (andpathsymlink
+ (not (file-symlink-p (substringsymlink0-1))))
+collect (list (format"ln -s '%s' '%s'"path
+ (substringsymlink0-1))
+"Make symlinks"7)
+append (my/index-get-symlink-commands (alist-get:childrenelem))))
+
my/index-get-symlink-commands
+
Run all commands
+
And put that all together.
+
First, as I want to check what’s going to be executed, let’s make a function to display commands in a separate buffer. Making it sh-mode is enough for now.
+
(defvar-localmy/index-commandsnil
+"Commands to be executed by `my/index-commands-exec'")
+
+(defunmy/index--commands-display (commands)
+"Display COMMANDS in a buffer.
+
+COMMANDS is a list of commands as defined by `my/index--commands-display'."
+ (unlesscommands
+ (user-error"No commands to display"))
+ (let ((buffer (get-buffer-create"*index commands*"))
+ (groups (seq-sort-by
+ (lambda (g) (nth2 (nth1g)))
+#'<
+ (seq-group-by (lambda (c) (nth1c))
+commands))))
+ (with-current-bufferbuffer
+ (sh-mode)
+ (let ((inhibit-read-onlyt)
+commands-sequence)
+ (erase-buffer)
+ (setq-localmy/index-commandsnil)
+ (cl-loopforgingroups
+forgroup-name= (carg)
+forelems= (cdrg)
+do (insert"# "group-name"\n")
+do (cl-loopforeleminelems
+do (push (nth0elem) my/index-commands)
+do (insert (nth0elem) "\n")))
+ (setq-localbuffer-read-onlyt)))
+ (switch-to-bufferbuffer)))
+
In order to execute these commands, compile with bash -x on a temporary file is quite sufficient.
I’ll also try to save some time by caching the resulting index tree. file-has-changed-p is pretty helpful in that.
+
(defvarmy/index--treenil
+"The last version of the index tree.")
+
+(defunmy/index--tree-retrive ()
+"Retrive the last version of the index tree.
+
+This function returns the last saved version of the index tree if it
+is still valid. Otherwise, it re-parses the index file."
+ (setq
+my/index--tree
+ (cond ((string-equal (buffer-file-name) my/index-file)
+ (my/index--tree-get))
+ ((or (nullmy/index--tree)
+ (file-has-changed-pmy/index-file'index))
+ (with-temp-buffer
+ (insert-file-contentsmy/index-file)
+ (let ((buffer-file-namemy/index-file))
+ (my/index--tree-get))))
+ (tmy/index--tree))))
+
Of course, plain dired does the job fine, thanks to the relatively low-depth filesystem structure. But I still want a navigation interface like M-x projectile-switch-project.
+
Navigation data
+
There are two slight problems with that.
+
First, the index tree does not always have the full info. For instance, I have the 10.03.A Artifacts folder, which I sync with MEGA and which has child folders like 10.03.A.01 smth and so on. Names of the latter are not stored anywhere because I don’t see the point, which means we have to extract that from the filesystem.
+
Second, as it turns out, there have to be two levels for navigation, which are delimited by the project property. I’m not sure if that the optimal way to implement Jonny.Decimal, but it works for me.
+
So, a function to tackle the first problem:
+
(defunmy/index--nav-extend (namepath)
+"Find all index-related files in PATH.
+
+NAME is the name of the root index entry, e.g. \"10.01
+Something\". If PATH containts folders like \"10.01.01
+Something\", \"10.01.02 ...\", they will be returned.
+
+The return value is a form as defined by `my/index--nav-get'."
+ (when (file-directory-ppath)
+ (let* ((number (my/index--extact-numbername))
+ (files (mapcar
+ (lambda (f) (consf (concatpathf)))
+ (seq-filter (lambda (f) (not (string-prefix-p"."f)))
+ (directory-filespath))))
+ (matching-files
+ (seq-filter
+ (lambda (f) (and (file-directory-p (cdrf))
+ (string-prefix-pnumber (carf))))
+files)))
+ (when (and (length>matching-files0)
+ (length<matching-files (lengthfiles)))
+ (user-error"Extraneuous files in %s"path))
+ (cl-loopfor (name-1.path-1) inmatching-files
+append (if-let ((child-files (my/index--nav-extendname-1 (concatpath-1"/"))))
+ (mapcar
+ (lambda (child-datum)
+ (pushname-1 (alist-get:nameschild-datum))
+child-datum)
+child-files)
+`(((:names. (,name-1))
+ (:path.,(concatpath-1"/")))))))))
+
And one to get the navigation data structure.
+
(defunmy/index--nav-get (tree&optionalnames)
+"Get the navigation structure from TREE.
+
+TREE is a form as defined by `my/index--tree-get'. NAMES is a
+list of names of the parent entries, e.g. (\"10.01 Something\"), used
+for recursive calls.
+
+The result is a list of alists with the following keys:
+- `:names` - list of names, e.g.
+ (\"10.01 Something\" \"10.01.01 Something\")
+- `:path` - path to the folder, e.g.
+ \"/path/10 stuff/10.01 Something/10.01.01 Something/\"
+- `:child-navs` - list of child navigation structures (optional)"
+ (seq-sort-by
+ (lambda (item) (alist-get:pathitem))
+#'string-lessp
+ (cl-reduce
+ (lambda (accelem)
+ (let* ((name (alist-get:nameelem))
+ (path (alist-get:pathelem)))
+ (cond ((alist-get:projectelem)
+ (let ((current-nav`((:names. (,@names,name))
+ (:path.,path))))
+ (when-let (child-navs
+ (and (alist-get:childrenelem)
+ (my/index--nav-get (alist-get:childrenelem))))
+ (setf (alist-get:child-navscurrent-nav) child-navs))
+ (pushcurrent-navacc)))
+ ((alist-get:childrenelem)
+ (when-let (child-navs (my/index--nav-get
+ (alist-get:childrenelem)
+`(,@names,name)))
+ (cl-loopforchild-navinchild-navs
+do (pushchild-navacc))))
+ (t (if-let ((extended-nav (my/index--nav-extendnamepath)))
+ (cl-loopforchild-navinextended-nav
+do (setf (alist-get:nameschild-nav)
+ (appendnames (listname)
+ (alist-get:nameschild-nav)))
+do (pushchild-navacc))
+ (push`((:names. (,@names,name))
+ (:path.,path))
+acc))))
+acc))
+tree
+:initial-valuenil)))
+
It also makes sense to cache results of the above.
+
(defvarmy/index--navnil
+"Navigation stucture for the index.")
+
+(defunmy/index--nav-retrive ()
+"Retrive the navigation structure from the index file.
+
+The return value is a form as defined by `my/index--nav-get'."
+ (if (or (nullmy/index--nav)
+ (file-has-changed-pmy/index-file'nav))
+ (let ((tree (my/index--tree-retrive)))
+ (setqmy/index--nav (my/index--nav-get
+ (my/index--tree-narrowtree))))
+my/index--nav))
+
Emacs interface
+
As for Emacs interface, completing-read is sufficient, except that I don’t want prescient.el to interfere with the default ordering of elements.
+
(defunmy/index--nav-prompt (nav)
+"Prompt the user for the navigation item to select.
+
+NAV is a structure as defined by `my/index--nav-get'."
+ (let* ((collection
+ (mapcar (lambda (item)
+ (cons (car (last (alist-get:namesitem)))
+ (alist-get:pathitem)))
+nav))
+ (ivy-prescient-sort-commandsnil))
+ (cdr
+ (assoc
+ (completing-read"Index: "collectionnilt)
+collection))))
+
+(defunmy/index--nav-find-path (navpath)
+"Find the navigation item in NAV with the given PATH.
+
+NAV is a structure as defined by `my/index--nav-get'."
+ (seq-find
+ (lambda (item)
+ (string-prefix-p (alist-get:pathitem) path))
+nav))
+
+(defunmy/index-nav (arg&optionalfunc)
+"Navigate the filesystem index.
+
+ARG is the prefix argument. It modifies the behavior of the
+command as follows:
+- If not in an indexed directory, or in an indexed directory with no
+ indexed children:
+ - nil: Select an indexed directory.
+ - '(4): Select an indexed directory, and select a child indexed
+ directory if available.
+- If in an indexed directory with indexed children (a project):
+ - nil: Select another indexed directory from the project.
+ - '(4): Select a top-level indexed directory (the same as nil for
+ the previous case).
+ - '(16): The same as '(4) for the previous case.
+
+FUNC is the function to call with the selected path. It defaults
+to `dired' if used interactively."
+ (interactive (listcurrent-prefix-arg#'dired))
+ (let* ((nav (my/index--nav-retrive))
+ (current-nav (my/index--nav-find-path
+nav (expand-file-namedefault-directory)))
+ (current-child-navs (alist-get:child-navscurrent-nav)))
+ (cond
+ ((or (and (nullarg) (nullcurrent-child-navs))
+ (and (equalarg'(4)) current-child-navs))
+ (funcall
+func
+ (my/index--nav-promptnav)))
+ ((or (and (equalarg'(4)) (nullcurrent-child-navs))
+ (and (equalarg'(16)) current-child-navs))
+ (let ((selected (my/index--nav-find-path
+nav
+ (my/index--nav-promptnav))))
+ (if-let (child-navs (alist-get:child-navsselected))
+ (funcallfunc (my/index--nav-promptchild-navs))
+ (funcallfunc (alist-get:pathselected)))))
+ ((and (nullarg) current-child-navs)
+ (funcallfunc (my/index--nav-promptcurrent-child-navs))))))
+
Finally, something that I can bind to a key.
+
(my-leader-def
+"i"#'my/index-nav)
+
Utilities
+
pass
+
I use pass as my password manager. Expectedly, there is Emacs frontend for it.
+
This package is pretty good to manage the password database. I use password-store-ivy (another package of mine) to actually type passwords. rofi-pass is another good option.
Also I use password-store-get in a few places in my config, and by default it returns nil if I make an error in the password, which inconvinient if I want to run the command in setq. So:
Atomic Chrome is an extension that allows to edit browser text fields in Emacs. Despite its name, it also works for Firefox with GhostText, which is what I use.
Gource is a program that draws an animated graph of users changing the repository over time.
+
Although it can work without extra effort (just run gource in a git repo), there are some tweaks that can be done:
+
+
Gource supports using custom pictures for users. Gravatar is an obvious place to get these.
+
Occasionally, the same people have different names and/or emails in history.
+It may happen when people use forges like GitLab or just have different settings on different machines. It would be nice to merge these names.
+
Visualizing the history of multiple repositories (e.g. frontend and backend) requires combining multiple gource logs.
+
+
So, why not try doing that with Emacs?
+
Gravatars
+
Much to my surprise, Emacs turned out to have a built-in package called gravatar.el.
+
So, let’s make a function to retrieve a gravatar and save it:
+
(defunmy/gravatar-retrieve-sync (emailfile-name)
+"Get gravatar for EMAIL and save it to FILE-NAME."
+ (let ((gravatar-default-image"identicon")
+ (gravatar-sizenil)
+ (coding-system-for-write'binary)
+ (write-region-annotate-functionsnil)
+ (write-region-post-annotation-functionnil))
+ (write-region
+ (image-property (gravatar-retrieve-synchronouslyemail) :data)
+nilfile-namenil:silent)))
+
To use these images, we need to save them to some folder and use usernames as file names. The folder:
The output is a list of pipe-separated strings, where the values are:
+
+
Number of occurrences for this combination of username and email
+
Email
+
Username
+
+
Of course, that part would have to be changed appropriately for other version control systems if you happen to use one.
+
So, below is one hell of a function that wraps this command and tries to merge emails and usernames belonging to one author:
+
(defunmy/git-get-authors (repo&optionalauthors-init)
+"Extract and merge all combinations of authors & emails from REPO.
+
+REPO is the path to a git repository.
+
+AUTHORS-INIT is the previous output of `my/git-get-authors'. It can
+be used to extract that information from multiple repositories.
+
+The output is a list of alists with following keys:
+- emails: list of (<email> . <count>)
+- authors: list of (<username> . <count>)
+- email: the most popular email
+- author: the most popular username
+I.e. one alist is all emails and usernames of one author."
+ (let* ((default-directoryrepo)
+ (data (shell-command-to-string
+"git log --pretty=format:\"%ae|%an\" | sort | uniq -c | sed \"s/^[ \t]*//;s/ /|/\""))
+ (authors
+ (cl-loopforstringin (split-stringdata"\n")
+if (= (length (split-stringstring"|")) 3)
+collect (let ((datum (split-stringstring"|")))
+`((count.,(string-to-number (nth0datum)))
+ (email.,(downcase (nth1datum)))
+ (author.,(nth2datum)))))))
+ (mapcar
+ (lambda (datum)
+ (setf (alist-get'authordatum)
+ (car (cl-reduce
+ (lambda (accauthor)
+ (if (> (cdrauthor) (cdracc))
+author
+acc))
+ (alist-get'authorsdatum)
+:initial-value'(nil.-1))))
+ (setf (alist-get'emaildatum)
+ (car (cl-reduce
+ (lambda (accemail)
+ (if (> (cdremail) (cdracc))
+email
+acc))
+ (alist-get'emailsdatum)
+:initial-value'(nil.-1))))
+datum)
+ (cl-reduce
+ (lambda (accval)
+ (let* ((author (alist-get'authorval))
+ (email (alist-get'emailval))
+ (count (alist-get'countval))
+ (saved-value
+ (seq-find
+ (lambda (cand)
+ (or (alist-getemail (alist-get'emailscand)
+nilnil#'string-equal)
+ (alist-getauthor (alist-get'authorscand)
+nilnil#'string-equal)
+ (alist-getemail (alist-get'authorscand)
+nilnil#'string-equal)
+ (alist-getauthor (alist-get'emailscand)
+nilnil#'string-equal)))
+acc)))
+ (ifsaved-value
+ (progn
+ (if (alist-getemail (alist-get'emailssaved-value)
+nilnil#'string-equal)
+ (cl-incf (alist-getemail (alist-get'emailssaved-value)
+nilnil#'string-equal)
+count)
+ (push (consemailcount) (alist-get'emailssaved-value)))
+ (if (alist-getauthor (alist-get'authorssaved-value)
+nilnil#'string-equal)
+ (cl-incf (alist-getauthor (alist-get'authorssaved-value)
+nilnil#'string-equal)
+count)
+ (push (consauthorcount) (alist-get'authorssaved-value))))
+ (setqsaved-value
+ (push`((emails. ((,email.,count)))
+ (authors. ((,author.,count))))
+acc)))
+acc))
+authors
+:initial-valueauthors-init))))
+
Despite the probable we-enjoy-typing-ness of the implementation, it’s actually pretty simple:
+
+
The output of git log is parsed into a list of alists with count, email and author as keys.
+
This list is reduced by cl-reduce into a list of alists with emails and authors as keys and the respective counts as values, e.g. ((<email-1> . 1) (<email-2> . 3)).
+I’ve seen a couple of cases where people would swap their username and email (lol), so seq-find also looks for an email in the list of authors and vice versa.
+
The mapcar call determines the most popular email and username for each authors.
+
+
The output is another list of alists, now with the following keys:
+
+
emails - list of elements like (<email> . <count>)
+
authors - list of elements like (<author-name> . <count>)
As I said above, by default gource just creates a visualization for the current repo. To change something in it, we need to invoke the program like that: gource --output-custom-log PATH, where PATH is either the path to the log file or - for stdout.
+
The log consists of lines of pipe-separated strings, e.g.:
Replaces all usernames of one author with the most frequent one
+
Prepends the file path with the repository name.
+
+
The output is a string in the gource log format as described above.
+
Finally, as we need to invoke all of this for multiple repositories, why not do that with dired:
+
(defunmy/gource-dired-create-logs (reposlog-name)
+"Create combined gource log for REPOS.
+
+REPOS is a list of strings, where a string is a path to a git repo.
+LOG-NAME is the path to the resulting log file.
+
+This function is meant to be invoked from `dired', where the required
+repositories are marked."
+ (interactive (list (or (dired-get-marked-filesnilnil#'file-directory-p)
+ (user-error"Select at least one directory"))
+ (read-file-name"Log file name: "nil"combined.log")))
+ (let ((authors
+ (cl-reduce
+ (lambda (accrepo)
+ (my/git-get-authorsrepoacc))
+repos
+:initial-valuenil)))
+ (with-temp-filelog-name
+ (insert
+ (string-join
+ (seq-filter
+ (lambda (line)
+ (not (string-empty-pline)))
+ (seq-sort-by
+ (lambda (line)
+ (if-let (time (car (split-stringline"|")))
+ (string-to-numbertime)
+0))
+#'<
+ (split-string
+ (mapconcat
+ (lambda (repo)
+ (my/gource-prepare-logrepoauthors))
+repos"\n")
+"\n")))
+"\n")))))
+
This function extracts authors from each repository and merges the logs as required by gource, that is sorting the result by time in ascending order.
+
Using the function
+
To use the function above, mark the required repos in a dired buffer and run M-x my/gource-dired-create-logs. This also works nicely with dired-subtree, in case your repos are located in different folders.
+
The function will create a combined log file (by default combined.log). To visualize the log, run:
Check the README for possible parameters, such as the speed of visualization, different elements, etc. That’s it!
+
I thought about making something like a transient.el wrapper around the gource command but figured it wasn’t worth the effort for something that I run just a handful of times in a year.
Yes, all my machines are named after colors I like.
+
Base configuration
+
The base configuration is shared between all the machines.
+
While it’s possible to make a single .scm file with base configuration and load it, I noticed that it produces more cryptic error messages whenever there is an error in the base file, so I opt-in for noweb.
+
guix system invocation is as follows:
+
sudo -E guix system reconfigure ~/.config/guix/systems/[system].scm
+
In principle, we could define a variable called base-operating-system and extend it in ancestors. However, then we would have to define mandatory fields like host-name, bootloader with dummy values. Since I’m already using noweb, there is little point.
+
The following code will be inserted at the top of the operating-system definition.
+
Use the full Linux kernel. I hope I’ll be able to use Libre kernel somewhere later.
+
Inferior in the kernel is used to avoid recompilation. It looks like I can pin these to different commits than in my channels.scm
In my case, the provided ISO doesn’t work because of the Libre kernel.
+
Fortunately, David Wilson has made a repository with a toolchain to make an ISO with the full kernel. In case it won’t be an option, the nonguix repo also has instructions on how to do that.
+
When an ISO is there, we have to write it on a USB stick. Run sudo fdisk -l to get a list of disks.
+
The approach given in the official instruction is to create a bootable USB with dd:
And activate the required profiles. Again, downloading & building Emacs, Starship and stuff will take a while.
+
Don’t forget to install JetBrainsMono Nerd Font.
+
Misc software & notes
+
+
+
+
Category
+
Guix dependency
+
Description
+
+
+
+
+
system
+
patchelf
+
A program to modify existsing ELF executables
+
+
+
system
+
glibc
+
A lot of stuff, including ELF interpeter and ldd
+
+
+
system
+
tor-client
+
+
+
+
system
+
torsocks
+
+
+
+
system
+
vnstat
+
+
+
+
+
OpenVPN
+
+
+
+
Category
+
Guix dependency
+
+
+
+
+
system
+
openvpn
+
+
+
system
+
openvpn-update-resolve-conf
+
+
+
system
+
openresolv
+
+
+
system
+
vpnc
+
+
+
+
Update [2023-06-29 Thu]: My censors seem to be putting sticks in the wheels of OpenVPN… Switched to Wireguard for now. It can be configured with Network Manager.
+
I’m not sure how to properly spin up VPN on Guix, so here is what ended I’m doing after some trial and error.
+
I’m using Mullvad VPN. The ~/.vpn folder stores its OpenVPN config (openvpn.ovpn), modified as follows:
+
+
+
paths to ca, cert and key are made absolute
+
ca /home/pavel/.vpn/ca.crt
+cert /home/pavel/.vpn/client.crt
+key /home/pavel/.vpn/client.key
+
setenv PATH is necessary because both resolvconf (openresolve) and update-resolv-conf.sh are shell scripts which need GNU coreutils and stuff, and OpenVPN clears PATH by default.
echo"Adding default route to $route_vpn_gateway with /0 mask..."
+
+IP=/run/current-system/profile/sbin/ip
+
+$IP route add default via $route_vpn_gateway
+
+echo"Removing /1 routes..."
+$IP route del 0.0.0.0/1 via $route_vpn_gateway
+$IP route del 128.0.0.0/1 via $route_vpn_gateway
+
+
+
vpn-start
+
As of now, CyberGhost doesn’t provide ipv6, so we have to disable it.
+
Mullvad seems to provide it, so the script just launches openvpn with pkexec.
+
exportDISPLAY=:0
+CONN=$(nmcli -f NAME con show --active | grep -Ev "(.*docker.*|NAME|br-.*|veth.*|tun.*|vnet.*|virbr.*)" | sed 's/ *$//g')
+
+if[ -z "$CONN"]; then
+echo"No connection!"
+ notify-send "VPN""No connection for VPN to run"
+exit
+fi
+
+# if [[ "$CONN" != *"Wired"* ]]; then
+# echo "Connection: $CONN"
+# notify-send "VPN" "Initializing for connection: $CONN"
+
+# pkexec nmcli con modify "$CONN" ipv6.method ignore
+# nmcli connection up "$CONN"
+# fi
+VPN_FILE=~/.vpn/sqrtminusone-$(hostname).ovpn
+if[[$(hostname)=='iris']]; then
+VPN_FILE=~/.vpn/mullvad_openvpn_linux_se_all/mullvad_se_all.conf
+fi
+echo$VPN_FILE
+pkexec openvpn --config $VPN_FILE
+
vpn-stop
+
Also a script to reverse the changes
+Also not necessary now. Just herd stop vpn and sudo pkill vpn.
+
CONN=$(nmcli -f NAME con show --active | grep -Ev "(.*docker.*|NAME|br-.*|veth.*|tun.*)" | sed 's/ *$//g')
+echo"Connection: $CONN"
+
+pkexec nmcli con modify "$CONN" ipv6.method auto
+nmcli connection up "$CONN"
+
Wireguard
+
So, yeah, wireguard can be configured with NetworkManager just fine.
+
The issue with DNS leaks remains, but fortunately NetworkManager runs all scripts in /etc/NetworkManager/dispatcher.d/ when a connection changes, provided that scripts are:
#!/bin/sh
+GREP=/run/current-system/profile/bin/grep
+NMCLI=<<get-nmcli()>>
+
+# Run only if wireguard is active
+if$NMCLI connection show --active | $GREP -q wireguard; then
+echo"nameserver 8.8.8.8" > /etc/resolv.conf
+fi
+
Expand the noweb with C-c C-v v, put it in dispatcher.d and run chmod 700.
+
flatpak
+
As for now, the easiest way to install most of proprietary software is via flatpak. See the relevant section in Desktop.org.
+
micromamba
+
condamamba is a package manager that I use for managing various versions of Python & Node.js.
+
mamba is a reimplementation of conda in C++. mamba is notably much faster and mostly compatible with conda, and micromamba is a tiny version of mamba that is contained in one statically linked exectuable. I’ve migrated to micromamba mostly because of speed.
+
conda is packaged for Guix with its fair share of quirks, mostly concerning the impossibility of changing the base environment in /gnu/store/. micromamba has none of that because it doesn’t ship with a base environment. It’s not packaged for Guix yet, so I’ve made a definition with binary-build-system in my channel.
+
You may need to unset $PYTHONPATH if you have any global packages installed, otherwise Python from the environemnt will try to import them instead of the conda versions.
+
I also want to have an ability to use global npm. Some settings for that are located in Console.org. Here we want to unset NPM_CONFIG_USERCONFIG if there is npm available in the environment.
+
So here is a script to set up conda hooks:
+
# Get writable conda envs with npm & without it
+readarray -t CONDA_ENVS_ALL <<<$(micromamba env list --json | jq '.envs[]')
+CONDA_ENVS_NPM=()
+CONDA_ENVS_NO_NPM=()
+for env in "${CONDA_ENVS_ALL[@]}"; do
+env="${env:1:${#env}-2}"
+if[ -w "$env"]; then
+if[ -f "$env/bin/npm"]; then
+CONDA_ENVS_NPM+=($env)
+else
+CONDA_ENVS_NO_NPM+=($env)
+fi
+fi
+done
+
+for env in "${CONDA_ENVS_NPM[@]}"; do
+echo"Found npm in $env"
+ mkdir -p "$env/etc/conda/activate.d"
+ mkdir -p "$env/etc/conda/deactivate.d"
+
+echo"unset NPM_CONFIG_USERCONFIG" > "$env/etc/conda/activate.d/conda.sh"
+echo"set -e NPM_CONFIG_USERCONFIG" > "$env/etc/conda/activate.d/conda.fish"
+echo"export NPM_CONFIG_USERCONFIG=$HOME/._npmrc" > "$env/etc/conda/deactivate.d/conda.sh"
+echo"export NPM_CONFIG_USERCONFIG=$HOME/._npmrc" > "$env/etc/conda/deactivate.d/conda.fish"
+done
+
+for env in "${CONDA_ENVS_NO_NPM}"; do
+echo"Did not found npm in $env"
+ rm -rf "$env/etc/conda/activate.d/conda.sh"||true
+ rm -rf "$env/etc/conda/activate.d/conda.fish"||true
+ rm -rf "$env/etc/conda/deactivate.d/conda.sh"||true
+ rm -rf "$env/etc/conda/deactivate.d/conda.fish"||true
+done
+
Slack
+
What a nonsense of a program.
+
I was able to launch the nix version with the following wrapper script:
Also, it requires a lsb_release in the PATH, so here is one:
+
echo"LSB Version: Hey. I spent an hour figuring out why Slack doesn't launch."
+echo"Distributor ID: It seems like it requires an lsb_release."
+echo"Description: But GNU Guix doesn't have one."
+echo"Release: 42.2"
+echo"Codename: n/a"
+
+
+
diff --git a/configs/index.xml b/configs/index.xml
new file mode 100644
index 0000000..5e005bd
--- /dev/null
+++ b/configs/index.xml
@@ -0,0 +1,53 @@
+
+
+
+ Configs on SqrtMinusOne
+ https://sqrtminusone.xyz/configs/
+ Recent content in Configs on SqrtMinusOne
+ Hugo -- gohugo.io
+ en-us
+
+
+ Console
+ https://sqrtminusone.xyz/configs/console/
+ Mon, 01 Jan 0001 00:00:00 +0000
+ https://sqrtminusone.xyz/configs/console/
+ No matter from which side you approach penguins, more always come from behind
A friend of mine Colors Noweb function to get colors.
(let ((color (or (my/color-value name)))) (if (> quote 0) (concat "\"" color "\"") color)) (let ((val (if (ct-light-p (my/color-value name)) (my/color-value 'black) (my/color-value 'white)))) (if (eq quote 1) (concat "\"" val "\"") val)) (setq-local org-confirm-babel-evaluate nil) .profile Environment export QT_QPA_PLATFORMTHEME="qt5ct" export QT_AUTO_SCREEN_SCALE_FACTOR=0 Set ripgrep config path
+
+
+ Desktop
+ https://sqrtminusone.xyz/configs/desktop/
+ Mon, 01 Jan 0001 00:00:00 +0000
+ https://sqrtminusone.xyz/configs/desktop/
+ My general desktop environment configuration.
Parts prefixed with (OFF) are not used, but kept for historic purposes. For some reason GitHub’s org renderer ignores TODO status, hence such a prefix. Round brackets instead of square ones to prevent GitHub’s org renderer from screwing up.
References:
A few cases of literate configuration. A few interesting ways in which literate configuration is used in this file. Some remarks Removed features:
Feature Last commit rofi-buku e22476b0cc6315e104e5ce4de5559a61c830c429 Global customization Colors I used to define color codes here (see previous version of the file), now I just get colors from the current Emacs theme.
+
+
+ Emacs config
+ https://sqrtminusone.xyz/configs/emacs/
+ Mon, 01 Jan 0001 00:00:00 +0000
+ https://sqrtminusone.xyz/configs/emacs/
+ One day we won’t hate one another, no young boy will march to war and I will clean up my Emacs config. But that day isn’t today.
Me, <2021-05-27 Thu 17:35> in commit 93a0573. Adapted from The Dark Element - “The Pallbearer Walks Alone”. T_T Introduction My configuration of GNU Emacs, an awesome text editor piece of software that can do almost anything.
At the moment of writing this, that “almost anything” includes:
+
+
+ Guix
+ https://sqrtminusone.xyz/configs/guix/
+ Mon, 01 Jan 0001 00:00:00 +0000
+ https://sqrtminusone.xyz/configs/guix/
+ GNU Guix is (1) a transactional package manager and (2) a GNU/Linux distribution.
My personal selling points are declarative package configuration and transactional upgrades.
References:
Official help System Crafters wiki Pjotr Prins’ Guix notes Davil Wilson’s YouTube series Profiles A profile is a way to group Guix packages. Amongst its advantages, profiles can be defined by manifests, which in turn can be stored in VCS.
References:
Guix Profiles in Practice Activate profiles A script to activate guix profiles.
+
+
+ Mail
+ https://sqrtminusone.xyz/configs/mail/
+ Mon, 01 Jan 0001 00:00:00 +0000
+ https://sqrtminusone.xyz/configs/mail/
+ :TOC: :include all :depth 3
My email configration. Currently I use lieer to fetch emails from Gmail, davmail & offlineimap to fetch emails from MS Exchange, notmuch to index, msmtp to send emails. Also using notmuch frontend from Emacs.
My problem with any particular mail setup was that I use Gmail labels quite extensively, and handling these over IMAP is rather awkward. Notmuch seems to be the only software that provides the same first-class support for labels.
+
+
+ My dotfiles
+ https://sqrtminusone.xyz/configs/readme/
+ Mon, 01 Jan 0001 00:00:00 +0000
+ https://sqrtminusone.xyz/configs/readme/
+ These are my GNU/Linux configuration files. View at GitHub.
I use the literate configuration strategy via Emacs’ Org Mode wherever possible. It has its pros and cons, but I find it pretty nice to keep the configs interweaved with comments in a handful of files.
The files themselves are managed and deployed via yadm, although I use Org Mode for things like config templating.
My current GNU/Linux distribution is GNU Guix.
+
+
+
diff --git a/configs/mail/index.html b/configs/mail/index.html
new file mode 100644
index 0000000..f984a54
--- /dev/null
+++ b/configs/mail/index.html
@@ -0,0 +1,771 @@
+
+
+
+
+
+ Mail
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mail
+
+
+
+
+ Mail
+
+
+
:TOC: :include all :depth 3
+
My email configration. Currently I use lieer to fetch emails from Gmail, davmail & offlineimap to fetch emails from MS Exchange, notmuch to index, msmtp to send emails. Also using notmuch frontend from Emacs.
+
My problem with any particular mail setup was that I use Gmail labels quite extensively, and handling these over IMAP is rather awkward. Notmuch seems to be the only software that provides the same first-class support for labels.
+
But I also have an Exchange account, with which I communicate via IMAP/SMTP adapter, and in this case, I synchronize notmuch tags and IMAP folders.
+
References:
+
+
My post about email configuration. I wrote it some time ago, but the general idea remains.
+
+
Lieer
+
+
+
+
Guix dependency
+
+
+
+
+
python-lieer
+
+
+
+
Lieer is a program to link up Gmail and notmuch. Basically, it downloads mail from Gmail via API, stores them in Maildir, and synchronizes labels with notmuch.
+
I have a separate directory in my ~/Mail for each address. To init lieer, run the following command in the directory:
+
gmi init <address>
+
After which the settings will be stored in gmailieer.json and the credentials in .credentials.gmailieer.json. The latter file is stored encrypted.
+
My preferred settings:
+
gmi set --replace-slash-with-dot
+gmi set --ignore-tags-local new
+
Running gmi sync in the required directory performs the synchronization. The first sync takes a while, the subsequent syncs are pretty fast.
+
DavMail
+
is a gateway between MS Exchange and the rest of the world, which uses IMAP/SMTP/LDAP/etc. As I have one corporate MS Exchange address, this is just the program I need. As of yet, it isn’t packaged for Guix, but it’s easy enough to download.
OfflineIMAP is a program that can synchronize IMAP mailbox and Maildir. Lieer does everything by itself, but my pirate Exchange IMAP needs this program. There is also isync, but there I had some weird issues with duplicate UIDs, which don’t occur for OfflineIMAP.
+
I have a few options for setting a username and password. First, I can run pass in remotepasswordeval, and while this will work, it will keep my keyring unlocked because I want to run offlineimap every couple of minutes.
+
Another option is to use noweb and not push the file below to the version control. Then I have a plaintext password of email on my computer, but I think it’s a lesser evil than the entire keyring.
+
I would use password-store-get from password-store.el, but I want this to be able to run without any 3rd party packages, so it’s just bash.
+
+
pass show Job/Digital/Email/pvkorytov@etu.ru | sed -n 's/username: //;2p'
+
+
pass show Job/Digital/Email/pvkorytov@etu.ru | head -n 1
+
Notmuch is an email indexer program, which handles labels in a way somewhat similar to Gmail. It also provides a frontend for Emacs, but it’s not the only one available.
+
Config
+
Not much is going on here.
+
First, the database path.
+
[database]
+path=/home/pavel/Mail
+
My name and list of emails. It’s not like it’s a secret anyhow.
Now we have to link up lieer & davmail’s maildir and with notmuch. This is done via the notmuch hook system, which allows running custom scripts before and after any command.
+
With lieer and Gmail, it is enough to simply run the program, because Gmail has first-class support for tags. Maildir does not, so I decide to synchronize notmuch tags and IMAP folders. In essence, the idea is to:
+
+
move emails to their folders by tags before the synchronization
+
tag mails by their folders after the synchronization
+
+
The problem is that with that approach one email can have only one tag, but it’s better than nothing.
+
So, here are the rules which match tags & folders:
+
+
+
+
+
tag
+
folder
+
+
+
+
+
inbox
+
INBOX
+
+
+
sent
+
Sent
+
+
+
spam
+
Junk
+
+
+
trash
+
Trash
+
+
+
job.digital
+
Job_Digital
+
+
+
job.digital.docs
+
Job_Digital.Docs
+
+
+
job.digital.support
+
Job_Digital.Support
+
+
+
job.digital.superservice
+
Job_Digital.Superservice
+
+
+
job.digital.applicants
+
Job_Digital.Applicants
+
+
+
job.moevm
+
Job_Moevm
+
+
+
etu
+
Etu
+
+
+
+
And below is a noweb function, which generates the following commands for notmuch to execute:
+
+
before sync:
+
+
notmuch search --output files "NOT path:[PATH] AND tag:[TAG] AND tag:[ROOT_TAG]" | xargs -I ! mv ! [PATH]
+Move emails with TAG but outside the matching PATH to the latter
+
notmuch search --output=files "NOT path:[ARCHIVE_PATH] AND tag:[ROOT_TAG] AND NOT tag:[TAG1] ... AND NOT tag:[TAGN]" | xargs -I ! mv ! [ARCHIVE_PATH]
+Move untagged emails to the ARCHIVE_PATH
+
+
+
after sync:
+
+
notmuch tag +[TAG] "path:[PATH] AND NOT tag:[TAG]"
+Tag emails in PATH which do not yet have the matching TAG
+
notmuch tag -[TAG] "NOT path:[PATH] AND tag:[TAG] AND tag:[ROOT_TAG]"
+Remove TAG from emails which are outside the matching PATH
+
+
+
+
These rules are getting included in the respective hooks.
notmuch tag +main "path:thexcloud/** AND tag:new"
+notmuch tag +progin "path:progin6304/** AND tag:new"
+notmuch tag +pvkorytov "path:pvkorytov_etu/** AND tag:new"
+
+echo"Running post-new filters"
+<<mail-tags(make_tag="t",remove="t")>>
+echo"Post-new filters done"
+notmuch tag -new "tag:new"
+
Sync script
+
A script to run notmuch new and push a notification if there is new mail.
+
exportDISPLAY=:0
+CHECK_FILE="/home/pavel/Mail/.last_check"
+QUERY="tag:unread"
+ALL_QUERY="tag:unread"
+if[ -f "$CHECK_FILE"]; then
+DATE=$(cat "$CHECK_FILE")
+QUERY="$QUERY and date:@$DATE.."
+fi
+
+notmuch new
+NEW_UNREAD=$(notmuch count "$QUERY")
+ALL_UNREAD=$(notmuch count "$ALL_QUERY")
+
+if[$NEW_UNREAD -gt 0]; then
+MAIN_UNREAD=$(notmuch count "tag:unread AND tag:main")
+PROGIN_UNREAD=$(notmuch count "tag:unread AND tag:progin")
+ETU_UNREAD=$(notmuch count "tag:unread AND tag:pvkorytov")
+read -r -d '' NOTIFICATION <<EOM
+$NEW_UNREAD new messages
+$MAIN_UNREAD thexcloud@gmail.com
+$PROGIN_UNREAD progin6304@gmail.com
+$ETU_UNREAD pvkorytov@etu.ru
+$ALL_UNREAD total
+EOM
+ notify-send "New Mail""$NOTIFICATION"
+fi
+
+echo"$(date +%s)" > $CHECK_FILE
+
The script is ran via GNU Mcron every 5 minutes.
+
(job"*/5 * * * * ""~/bin/scripts/check-email")
+
MSMTP
+
+
+
+
Guix dependency
+
+
+
+
+
msmtp
+
+
+
+
Sending emails can be done with MSMTP. It automatially chooses the email address and server based on the contents of the message, which is handy if there are multiple mailboxes to be managed.
+
defaults
+auth on
+tls on
+tls_trust_file /etc/ssl/certs/ca-certificates.crt
+logfile ~/.msmtp.log
+
+account main
+host smtp.gmail.com
+port 587
+from thexcloud@gmail.com
+user thexcloud@gmail.com
+passwordeval "pass show My_Online/APIs/google-main-app-password | head -n 1"
+
+account progin
+host smtp.gmail.com
+port 587
+from progin6304@gmail.com
+user progin6304@gmail.com
+passwordeval "pass show My_Online/ETU/progin6304@gmail.com | head -n 1"
+
+account pvkorytov
+tls off
+auth plain
+host localhost
+port 1025
+from pvkorytov@etu.ru
+user pvkorytov
+passwordeval "pass show Job/Digital/Email/pvkorytov@etu.ru | head -n 1"
+
Emacs
+
+
+
+
Guix dependency
+
+
+
+
+
emacs-notmuch
+
+
+
+
Finally, Emacs configuration. Let’s start with some variables:
This creates issues with certain email clients. For instance, MS Exchange often just cuts the text at Person <person@mail.org>...., so there’s no way to see the signature from the UI.
+
What’s more, MS Exchange, Gmail and other such clients add the signature before the quotation block, like that:
These are my GNU/Linux configuration files. View at GitHub.
+
I use the literate configuration strategy via Emacs’ Org Mode wherever possible. It has its pros and cons, but I find it pretty nice to keep the configs interweaved with comments in a handful of files.
+
The files themselves are managed and deployed via yadm, although I use Org Mode for things like config templating.
+
My current GNU/Linux distribution is GNU Guix. I like Guix because, among other things, it allows to declare the required software in configuration files, so I can have the same set of programs across multiple machines (look for tables with “Guix dependency” in the header).
+
The central program to all of that is, of course, GNU Emacs. At the time of this writing, it takes ~50% of my screen time and has the largest share of configuration here.
+
+
diff --git a/elfeed-summary-img/screenshot.png b/elfeed-summary-img/screenshot.png
new file mode 100644
index 0000000..08e5465
Binary files /dev/null and b/elfeed-summary-img/screenshot.png differ
diff --git a/elfeed-sync-img/screenshot.png b/elfeed-sync-img/screenshot.png
new file mode 100644
index 0000000..aefb919
Binary files /dev/null and b/elfeed-sync-img/screenshot.png differ
diff --git a/exwm-modeline-img/screenshot.png b/exwm-modeline-img/screenshot.png
new file mode 100644
index 0000000..326d2f1
Binary files /dev/null and b/exwm-modeline-img/screenshot.png differ
diff --git a/index.xml b/index.xml
index 86e4786..070d48a 100644
--- a/index.xml
+++ b/index.xml
@@ -6,7 +6,245 @@
Recent content in Index on SqrtMinusOneHugo -- gohugo.ioen-us
- Sat, 11 Nov 2023 00:00:00 +0000
+ Sun, 17 Dec 2023 00:00:00 +0000
+
+ org-clock-agg
+ https://sqrtminusone.xyz/packages/org-clock-agg/
+ Sun, 17 Dec 2023 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/org-clock-agg/
+
+ <p>Aggregate <a href="https://orgmode.org/manual/Clocking-Work-Time.html">org-clock</a> records and display the results in an interactive buffer. The records are grouped by predicates such as file name, their outline path in the file, etc. Each record is placed in a tree structure; each node of the tree shows the total time spent in that node and its children. The top-level node shows the total time spent in all records found by the query.</p>
+<figure><img src="https://sqrtminusone.xyz/org-clock-agg-img/screenshot.png"/>
+</figure>
+
+<h2 id="installation">Installation</h2>
+<p>The package isn’t yet available anywhere but in this repository. My preferred way for such cases is <a href="https://github.com/jwiegley/use-package">use-package</a> and <a href="https://github.com/radian-software/straight.el">straight.el</a>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">org-clock-agg</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> (<span style="color:#008000">:host</span> <span style="color:#19177c">github</span> <span style="color:#008000">:repo</span> <span style="color:#ba2121">"SqrtMinusOne/org-clock-agg"</span>))
+</span></span></code></pre></div><p>Alternatively, clone the repository, add it to the <code>load-path</code>, and <code>require</code> the package.</p>
+<h2 id="usage">Usage</h2>
+<p>Run <code>M-x org-clock-agg</code> to open the interactive buffer (as depicted in the screenshot above).</p>
+<p>The interactive buffer provides the following controls:</p>
+<ul>
+<li><strong>Files</strong>: Specifies the org files from which to select (defaults to <a href="https://orgmode.org/manual/Agenda-Files.html">org-agenda</a>).</li>
+<li><strong>Date from</strong> and <strong>To</strong>: Define the date range.</li>
+<li><strong>Group by</strong>: Determines how <code>org-clock</code> records are grouped.</li>
+<li><strong>Show elements</strong>: Whether to display raw <code>org-clock</code> records in each node.</li>
+<li><strong>Add “Ungrouped”</strong>: Option to include the “Ungrouped” node. This is particularly useful with <a href="#custom-grouping-predicates">custom grouping predicates</a>.</li>
+</ul>
+<p>Press <code>[Refresh]</code> to update the buffer. The initial search might take some time, but subsequent searches are generally faster due to the caching mechanism employed by <a href="https://github.com/alphapapa/org-ql">org-ql</a>.</p>
+<p>The buffer uses <a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Outline-Mode.html">outline-mode</a> to display the tree, so each node becomes an <code>outline-mode</code> header. Refer to the linked manual for available commands/keybindings, or, if you use <code>evil-mode</code>, check <a href="https://github.com/emacs-evil/evil-collection/blob/master/modes/outline/evil-collection-outline.el">the relevant evil-collection file</a>.</p>
+<h3 id="files">Files</h3>
+<p>By default, the package selects <code>org-clock</code> records from <code>(org-agenda-files)</code>. Additional options can be included by customizing the <code>org-clock-agg-files-preset</code> variable. For instance:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">org-clock-agg-files-preset</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">`</span>((<span style="color:#ba2121">"Org Agenda + Archive"</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">.</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">,</span>(<span style="color:#00f">append</span> (<span style="color:#19177c">org-agenda-files</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">cl-remove-if</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">lambda</span> (<span style="color:#19177c">f</span>) (<span style="color:#19177c">string-match-p</span> (<span style="color:#008000">rx</span> <span style="color:#ba2121">"."</span> <span style="color:#19177c">eos</span>) <span style="color:#19177c">f</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#00f">directory-files</span> (<span style="color:#00f">concat</span> <span style="color:#19177c">org-directory</span> <span style="color:#ba2121">"/archive/"</span>) <span style="color:#800">t</span>))))))
+</span></span></code></pre></div><p>Note that after updating any of these variables, you’ll need to reopen the <code>*org-clock-agg*</code> buffer to view the changes.</p>
+<p>Alternatively, you can directly specify the list of files within the buffer by selecting “Custom list” in the “Files” control.</p>
+<h3 id="date-range">Date Range</h3>
+<p>Dates can take the following values:</p>
+<ul>
+<li>A number: Represents a relative number of days from the current date. E.g. the default value of <code>-7</code> to <code>0</code> menas the previous week up to today.</li>
+<li>A date string in the format <code>YYYY-MM-DD HH:mm:ss</code>, with or without the time part.</li>
+</ul>
+<p>By default, the interval is inclusive. For instance, specifying an interval like 2023-12-12 .. 2023-12-13 includes all records from 2023-12-12 00:00:00 to 2023-12-13 23:59:59.</p>
+<h3 id="group-by">Group By</h3>
+<p>Records are grouped based on the sequence of grouping predicates.</p>
+<p>For example, with the following content in <code>tasks.org</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>* Tasks
+</span></span><span style="display:flex;"><span>** DONE Thing 1
+</span></span><span style="display:flex;"><span>:LOGBOOK:
+</span></span><span style="display:flex;"><span>CLOCK: [2023-12-13 Wed 19:01]--[2023-12-13 Wed 19:29] => 0:28
+</span></span><span style="display:flex;"><span>CLOCK: [2023-12-13 Wed 19:30]--[2023-12-13 Wed 19:40] => 0:10
+</span></span><span style="display:flex;"><span>:END:
+</span></span></code></pre></div><p>And predicates “Org file”, “Day”, and “Outline path”, the records for “Thing 1” will be processed as follows:</p>
+<ul>
+<li>“Day” -> <code>2023-12-13</code></li>
+<li>“Org file” -> <code>tasks.org</code></li>
+<li>“Outline path” -> <code>Tasks</code>, <code>Thing 1</code></li>
+</ul>
+<p>Consequently, the node will be placed at the path <code>2023-12-13</code> / <code>tasks.org</code> / <code>Tasks</code> / <code>Thing 1</code> in the resulting tree:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>* Results Root 0:38
+</span></span><span style="display:flex;"><span>** 2023-12-13 Day 0:38
+</span></span><span style="display:flex;"><span>*** tasks.org Org File 0:38
+</span></span><span style="display:flex;"><span>**** Tasks Outline path 0:38
+</span></span><span style="display:flex;"><span>***** Thing 1 Outline path 0:38
+</span></span><span style="display:flex;"><span>- [2023-12-13 Wed 19:01]--[2023-12-13 Wed 19:29] => 0:28 : DONE Thing 1
+</span></span><span style="display:flex;"><span>- [2023-12-13 Wed 19:30]--[2023-12-13 Wed 19:40] => 0:10 : DONE Thing 1
+</span></span></code></pre></div><p>The following built-in predicates are currently available:</p>
+<table>
+<thead>
+<tr>
+<th>Name</th>
+<th>Comment</th>
+<th>Customization variables</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>Category</td>
+<td></td>
+<td></td>
+</tr>
+<tr>
+<td>Org file</td>
+<td></td>
+<td></td>
+</tr>
+<tr>
+<td>Outline path</td>
+<td></td>
+<td></td>
+</tr>
+<tr>
+<td>Tags</td>
+<td>Sorted alphabetically</td>
+<td></td>
+</tr>
+<tr>
+<td>Headline</td>
+<td>Last item of the outline path</td>
+<td></td>
+</tr>
+<tr>
+<td>Day</td>
+<td></td>
+<td><code>org-clock-agg-day-format</code></td>
+</tr>
+<tr>
+<td>Week</td>
+<td></td>
+<td><code>org-clock-agg-week-format</code></td>
+</tr>
+<tr>
+<td>Month</td>
+<td></td>
+<td><code>org-clock-agg-month-format</code></td>
+</tr>
+<tr>
+<td>TODO keyword</td>
+<td></td>
+<td></td>
+</tr>
+<tr>
+<td>Is done</td>
+<td></td>
+<td></td>
+</tr>
+<tr>
+<td>Selected props</td>
+<td></td>
+<td><code>org-clock-agg-properties</code></td>
+</tr>
+</tbody>
+</table>
+<p>Ensure to use <code>setopt</code> to set the variables; otherwise, the customization logic will not be invoked:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">setopt</span> <span style="color:#19177c">org-clock-agg-properties</span> <span style="color:#666">'</span>(<span style="color:#ba2121">"PROJECT_NAME"</span>))
+</span></span></code></pre></div><p>Refer also to <a href="#custom-grouping-predicates-1">custom grouping predicates</a>.</p>
+<h2 id="customization">Customization</h2>
+<h3 id="node-formatting">Node Formatting</h3>
+<p>The <code>org-clock-agg-node-format</code> variable determines the formatting of individual tree nodes. This uses a <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Custom-Format-Strings.html">format string</a> that with the following format specifiers avaiable:</p>
+<ul>
+<li><code>%t</code>: Node title with the level prefix, truncated to <code>title-width</code> characters (refer to below)</li>
+<li><code>%c</code>: Name of the grouping function that generated the node</li>
+<li><code>%z</code>: Time spent in the node, formatted according to <code>org-clock-agg-duration-format</code>.</li>
+<li><code>%s</code>: Time share of the node against the parent node</li>
+<li><code>%S</code>: Time share of the node against the top-level node</li>
+</ul>
+<p>The default value is:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>%-%(+ title-width)t %20c %8z
+</span></span></code></pre></div><p>Where <code>%(+ title-width)</code> is <code>(- (window-width) org-clock-agg-node-title-width-delta)</code>, with the default value of the latter set to <code>40</code>.</p>
+<p>Thefore, in the default configuration, the node title is truncated to <code>title-width</code> characters, while 40 symbols are allocated for the rest of the header, i.e. " %20c %8z" (30 symbols), along with additional space for folding symbols of <code>outline-minor-mode</code>, line numbers, etc.</p>
+<h3 id="record-formatting">Record Formatting</h3>
+<p>When the “Show records” flag is enabled, associated records for each node are displayed. The formatting of these is defined by <code>org-clock-agg-elem-format</code>, which is also a format string with the following specifiers:
+Customize the formatting of these records through <code>org-clock-agg-elem-format</code>, which also utilizes a format string comprising the following specifiers:</p>
+<ul>
+<li><code>%s</code>: Start of the time range</li>
+<li><code>%e</code>: End of the time range</li>
+<li><code>%d</code>: Duration of the time range</li>
+<li><code>%t</code>: Title of the record.</li>
+</ul>
+<p>The default value is:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>- [%s]--[%e] => %d : %t
+</span></span></code></pre></div><h3 id="custom-grouping-predicates-2">Custom grouping predicates</h3>
+<p>It’s possible to define custom grouping predicates in addition to the default ones. In fact, it’s probably the only way to get grouping that is tailored to your particular org workflow; I haven’t included my predicates in the package because they aren’t general enough.</p>
+<p>To create new predicates, use <code>org-clock-agg-defgroupby</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">org-clock-agg-defgroupby</span> <span style="color:#19177c"><name></span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:key1</span> <span style="color:#19177c">value1</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:key2</span> <span style="color:#19177c">value2</span>
+</span></span><span style="display:flex;"><span> <span style="color:#19177c"><body></span>)
+</span></span></code></pre></div><p>The available keyword arguments include:</p>
+<ul>
+<li><code>:readable-name</code>: Function name for the UI.</li>
+<li><code>:default-sort</code>: Default sorting function.</li>
+</ul>
+<p>The body binds two variables - <code>elem</code> and <code>extra-params</code>, and must return a list of strings.</p>
+<p>The <code>elem</code> variable is an alist that represents one org-clock record. The keys are as follows:</p>
+<ul>
+<li><code>:start</code>: Start time in seconds since the epoch</li>
+<li><code>:end</code>: End time in seconds since the epoch</li>
+<li><code>:duration</code>: Duration in seconds</li>
+<li><code>:headline</code>: Instance of <a href="https://orgmode.org/worg/dev/org-element-api.html">org-element</a> for the headline</li>
+<li><code>:tags</code>: List of tags</li>
+<li><code>:file</code>: File name</li>
+<li><code>:outline-path</code>: titles of all headlines from the root to the current headline</li>
+<li><code>:properties</code>: List of properties; <code>org-clock-agg-properties</code> sets the selection list</li>
+<li><code>:category</code>: <a href="https://orgmode.org/manual/Categories.html">Category</a> of the current headline.</li>
+</ul>
+<p>The <code>extra-params</code> variable is an alist of global parameters controlling the function’s behavior. Additional parameters can be added by customizing <code>org-clock-agg-extra-params</code>. This alist has keys as parameter names and values as <a href="https://www.gnu.org/software/emacs/manual/html_mono/widget.html">widget.el</a> expressions (applied to <code>widget-create</code>) controlling the UI. Each widget must contain an <code>:extras-key</code> key.</p>
+<p>For instance:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">org-clock-agg-extra-params</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">'</span>((<span style="color:#ba2121">"Events: Offline / Online"</span> <span style="color:#666">.</span> (<span style="color:#19177c">checkbox</span> <span style="color:#008000">:extras-key</span> <span style="color:#008000">:events-online</span>))))
+</span></span></code></pre></div><p>This adds a checkbox to the form that appears as:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Events: Offline / Online [ ]
+</span></span></code></pre></div><p>When checked, <code>extra-params</code> takes the value <code>((:extras-keys . t))</code>.</p>
+<p>Here’s an example predicate. I store meetings the following way:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>* Some project
+</span></span><span style="display:flex;"><span>** Meetings
+</span></span><span style="display:flex;"><span>*** Some meeting 1
+</span></span><span style="display:flex;"><span>*** Some meeting 2
+</span></span><span style="display:flex;"><span>* Another project
+</span></span><span style="display:flex;"><span>** Meetings
+</span></span><span style="display:flex;"><span>*** Another meeting 1
+</span></span><span style="display:flex;"><span>*** Another meeting 2 (offline)
+</span></span></code></pre></div><p>I want to group these meetings by title, i.e. group all instances of “Some meeting”, “Another meeting”, etc. Optionally I want to group online and offline meetings.</p>
+<p>This can be done the following way:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">org-clock-agg-defgroupby</span> <span style="color:#19177c">event</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:readable-name</span> <span style="color:#ba2121">"Event"</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:default-sort</span> <span style="color:#19177c">total</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">let*</span> ((<span style="color:#19177c">title</span> (<span style="color:#19177c">org-element-property</span> <span style="color:#008000">:raw-value</span> (<span style="color:#19177c">alist-get</span> <span style="color:#008000">:headline</span> <span style="color:#19177c">elem</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">is-meeting</span> (<span style="color:#008000">or</span> (<span style="color:#19177c">string-match-p</span> <span style="color:#ba2121">"meeting"</span> (<span style="color:#00f">downcase</span> <span style="color:#19177c">title</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">seq-contains-p</span> (<span style="color:#19177c">alist-get</span> <span style="color:#008000">:tags</span> <span style="color:#19177c">elem</span>) <span style="color:#ba2121">"mt"</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">is-offline</span> (<span style="color:#008000">or</span> (<span style="color:#19177c">string-match-p</span> <span style="color:#ba2121">"offline"</span> (<span style="color:#00f">downcase</span> <span style="color:#19177c">title</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">seq-contains-p</span> (<span style="color:#19177c">alist-get</span> <span style="color:#008000">:tags</span> <span style="color:#19177c">elem</span>) <span style="color:#ba2121">"offline"</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">title-without-stuff</span> (<span style="color:#19177c">string-trim</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">replace-regexp-in-string</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">rx</span> (<span style="color:#008000">or</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#00f">+</span> (<span style="color:#008000">or</span> <span style="color:#19177c">digit</span> <span style="color:#ba2121">"."</span>)))
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"(offline)"</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">seq</span> <span style="color:#ba2121">"["</span> (<span style="color:#00f">+</span> <span style="color:#19177c">alnum</span>) <span style="color:#ba2121">"]"</span>) ))
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">""</span> <span style="color:#19177c">title</span>))))
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> <span style="color:#19177c">is-meeting</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">`</span>(<span style="color:#ba2121">"Meeting"</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">,@</span>(<span style="color:#008000">when</span> (<span style="color:#19177c">alist-get</span> <span style="color:#008000">:events-online</span> <span style="color:#19177c">extra-params</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">if</span> <span style="color:#19177c">is-offline</span> <span style="color:#666">'</span>(<span style="color:#ba2121">"Offline"</span>) <span style="color:#666">'</span>(<span style="color:#ba2121">"Online"</span>)))
+</span></span><span style="display:flex;"><span> <span style="color:#666">,</span><span style="color:#19177c">title-without-stuff</span>))))
+</span></span></code></pre></div><p>For the following result:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>* Results
+</span></span><span style="display:flex;"><span>** Meetings
+</span></span><span style="display:flex;"><span>*** Some meeting
+</span></span><span style="display:flex;"><span>*** Another meeting
+</span></span><span style="display:flex;"><span>** Ungrouped
+</span></span></code></pre></div><p>This can be coupled with a project predicate to analyze the time spent per project in a particular kind of meeting.</p>
+
+
+
+
Declarative filesystem management with Emacs & Org Mode
https://sqrtminusone.xyz/posts/2023-11-11-index/
@@ -825,6 +1063,138 @@
+
+ BIOME - Bountiful Interface to Open Meteo for Emacs
+ https://sqrtminusone.xyz/packages/biome/
+ Sat, 22 Jul 2023 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/biome/
+
+ <figure><a href="https://melpa.org/#/biome"><img src="https://melpa.org/packages/biome-badge.svg"/></a>
+</figure>
+
+<p>Interface to <a href="https://open-meteo.com/">Open Meteo</a> for Emacs. The service provides weather forecasts, historical weather data, climate change projections, and more.</p>
+<p>The service is AGPL-licensed; the hosted API is free for non-commercial use if you make less than 10000 requests per day.</p>
+<figure><img src="https://sqrtminusone.xyz/biome-img/report.png"/>
+</figure>
+
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you normally install packages, I prefer <a href="https://github.com/jwiegley/use-package">use-package</a> and <a href="https://github.com/radian-software/straight.el">straight.el</a>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">biome</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>)
+</span></span></code></pre></div><p>Or clone the repository, add it to <code>load-path</code>, and <code>require</code> the package.</p>
+<h2 id="usage">Usage</h2>
+<p>The main entry point is <code>M-x biome</code>. Each item under “Open Meteo Data” corresponds to a particular endpoint of the service. For instance, <code>M-x biome ww</code> is a generic weather forecast. Check out the <a href="https://open-meteo.com/en/docs">API docs</a> for more detailed descriptions.</p>
+<figure><img src="https://sqrtminusone.xyz/biome-img/root.png"/>
+</figure>
+
+<p>Each of these items opens a query interface. A query consists of “global” variables, such as location, units, etc., and “group variables”. Groups are usually “hourly” and “daily”.</p>
+<figure><img src="https://sqrtminusone.xyz/biome-img/query.png"/>
+</figure>
+
+<p>Global variables must always include a location (section “Select Coordinates or City”). To enter a location, you can either enter latitude and longitude (Open Meteo has an <a href="https://open-meteo.com/en/docs/geocoding-api">API for those</a> as well) or select a location from <code>biome-query-coords</code>. Example configuration:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">biome-query-coords</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">'</span>((<span style="color:#ba2121">"Helsinki, Finland"</span> <span style="color:#666">60.16952</span> <span style="color:#666">24.93545</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"Berlin, Germany"</span> <span style="color:#666">52.52437</span> <span style="color:#666">13.41053</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"Dubai, UAE"</span> <span style="color:#666">25.0657</span> <span style="color:#666">55.17128</span>)))
+</span></span></code></pre></div><p>A timezone is also often required (“Settings” > “Timezone”).</p>
+<p>The current group is switched with <code><tab></code>. Each group’s section has a set of variables that can be toggled on and off, such as temperature, precipitation, etc. Check out the <a href="https://open-meteo.com/en/docs">API docs</a> if you’re interested in the meaning of more esoteric ones.</p>
+<p>Press <code>RET</code> after you’ve configured the query to call the API. If something goes wrong, it will output an error, such as:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Open Meteo has returned an error.
+</span></span><span style="display:flex;"><span>Error: (error http 400)
+</span></span><span style="display:flex;"><span>Reason: Timezone is required
+</span></span></code></pre></div><p>Or it will open the results table (the first screenshot).</p>
+<p><code>tabulated-list</code> doesn’t support horizontal scrolling, so press <code>c</code> to toggle columns’ visibility.</p>
+<figure><img src="https://sqrtminusone.xyz/biome-img/columns.png"/>
+</figure>
+
+<h2 id="more-configuration">More configuration</h2>
+<p>To save a query for later, press <code>P</code> in the root of the query interface. This will generate a definition like this:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">biome-def-preset</span> <span style="color:#19177c">biome-query-preset-177</span>
+</span></span><span style="display:flex;"><span> ((<span style="color:#008000">:name</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Weather Forecast"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">"hourly"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:params</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"hourly"</span> <span style="color:#ba2121">"windgusts_10m"</span> <span style="color:#ba2121">"windspeed_10m"</span> <span style="color:#ba2121">"cloudcover"</span> <span style="color:#ba2121">"surface_pressure"</span> <span style="color:#ba2121">"weathercode"</span> <span style="color:#ba2121">"snowfall"</span> <span style="color:#ba2121">"showers"</span> <span style="color:#ba2121">"rain"</span> <span style="color:#ba2121">"relativehumidity_2m"</span> <span style="color:#ba2121">"temperature_2m"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"longitude"</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"latitude"</span> <span style="color:#666">.</span> <span style="color:#666">60.16952</span>))))
+</span></span></code></pre></div><p>Add this somewhere in your config after the package is loaded, e.g., in the <code>:config</code> section of the <code>use-package</code> form or wrapped in <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Hooks-for-Loading.html#index-with_002deval_002dafter_002dload">with-eval-after-load</a>. Running <code>M-x biome-query-preset-177</code> will create a query interface with this preset.</p>
+<p>Table formatting can be configured with <code>biome-grid-format</code>; check the docstring for more information. For instance, if you want to disable all gradients:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">biome-grid-format</span> (<span style="color:#19177c">seq-filter</span> (<span style="color:#008000">lambda</span> (<span style="color:#19177c">f</span>) (<span style="color:#19177c">not</span> (<span style="color:#00f">eq</span> (<span style="color:#00f">car-safe</span> (<span style="color:#00f">nth</span> <span style="color:#666">2</span> <span style="color:#19177c">f</span>))
+</span></span><span style="display:flex;"><span> <span style="color:#19177c">'gradient</span>)))
+</span></span><span style="display:flex;"><span> <span style="color:#19177c">biome-grid-format</span>))
+</span></span></code></pre></div><h2 id="composite-queries">Composite queries</h2>
+<p>The package also allows executing multiple queries at once to join their results. This can be useful for comparing weather in different locations or for viewing different reports about the same location.</p>
+<p>Run <code>M-x biome-multi</code> to invoke the-multi query dialog.</p>
+<figure><img src="https://sqrtminusone.xyz/biome-img/multi.png"/>
+</figure>
+
+<p>(<em>yes, I’ve switched to a light theme since the time of the previous screenshot</em>)</p>
+<p>Pressing <code>a</code> invokes the standard query dialog, where pressing <code>RET</code> returns to the root dialog, adding the query to the list. Pressing <code>RET</code> in the root dialog executes the queries in the list.</p>
+<p>Queries are executed concurrently. The results are shown if all queries have been successfully completed.</p>
+<p><code>P</code> generates a preset defintion for the current query:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">biome-def-multi-preset</span> <span style="color:#19177c">biome-query-preset-601</span>
+</span></span><span style="display:flex;"><span> (((<span style="color:#008000">:name</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Air Quality"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">"hourly"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:params</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"hourly"</span> <span style="color:#ba2121">"uv_index"</span> <span style="color:#ba2121">"european_aqi"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"longitude"</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"latitude"</span> <span style="color:#666">.</span> <span style="color:#666">60.16952</span>)))
+</span></span><span style="display:flex;"><span> ((<span style="color:#008000">:name</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Weather Forecast"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">"hourly"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:params</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"hourly"</span> <span style="color:#ba2121">"weathercode"</span> <span style="color:#ba2121">"snowfall"</span> <span style="color:#ba2121">"showers"</span> <span style="color:#ba2121">"rain"</span> <span style="color:#ba2121">"temperature_2m"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"longitude"</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"latitude"</span> <span style="color:#666">.</span> <span style="color:#666">60.16952</span>)))))
+</span></span></code></pre></div><p>Just note that the macro is called <code>biome-def-multi-preset</code>.</p>
+<h2 id="implementation-notes">Implementation notes</h2>
+<p>This isn’t the most complicated thing I’ve done, but it’s probably the most over-engineered one.</p>
+<p>As you may have guessed, the interfaces mirror the <a href="https://open-meteo.com/en/docs">API docs</a>. I’ve implemented <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Parsing-HTML_002fXML.html">parsing of these HTMLs</a> in <code>biome-api-parse--generate</code>, which generates the value of <code>biome-api-data</code>. Initially, it downloaded the HTML pages by itself, but - imagine that - the website was migrated to Svelte after I implemented maybe 80% of the parsing logic, and the Svelte version populates the accordions via JavaScript. So, as of now, the function requires opening the website in the browser, manually toggling all the accordions, and copying the HTML from DevTools. Fortunately, the parsing is a one-off operation.</p>
+<p>Then, the interface… I like <a href="https://github.com/magit/transient/">transient.el</a>, so I wanted to make the interface generated dynamically from <code>biome-api-data</code>, which turned out harder than I expected. I probably should’ve just used <a href="https://www.gnu.org/software/emacs/manual/html_mono/widget.html">widget.el</a>.</p>
+<p>Generating sensible keys was a challenge. I’ve made an algorithm in <code>biome-query--unique-keys</code> that sort of works well.</p>
+<p>And as for populating transient prefixes, I tried to use <code>:setup-children</code> in a few places, but it’s not general enough, namely, it doesn’t seem to support specifying <code>:class</code> for child groups… So I ended up overriding <code>transient--layout</code> in the prefix setup. This doesn’t seem to have any undesirable side effects.</p>
+<p>Also, the only way I found to use custom infix classes in these dynamic definitions was to eval <code>transient-define-infix</code> for each required place. Unfortunately, that adds a lot of stuff to the interactive functions namespace.</p>
+<p>Getting to the results display, Lars Ingebrigtsen’s <a href="https://lars.ingebrigtsen.no/2022/04/13/more-vtable-fun/">vtable</a> comes only in Emacs 29, so I used <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Tabulated-List-Mode.html">tabulated-list</a>. The only disadvantage of the latter is the lack of horizontal scroll support, which can be worked around by hiding columns with <code>biome-grid-columns</code>.</p>
+<p>Most variables are formatted with a gradient, colors for which were mostly inspired by <a href="https://www.windy.com/">Windy</a>. Formatting for things like air quality variables is probably all over the place, so take the red color with a grain of salt.</p>
+
+
+
+
+
+ micromamba.el
+ https://sqrtminusone.xyz/packages/micromamba/
+ Tue, 20 Jun 2023 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/micromamba/
+
+ <figure><a href="https://melpa.org/#/micromamba"><img src="https://melpa.org/packages/micromamba-badge.svg"/></a>
+</figure>
+
+<p>Emacs package for working with <a href="https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html">micromamba</a> environments.</p>
+<p><a href="https://mamba.readthedocs.io/en/latest/index.html">mamba</a> is a reimplementation of the <a href="https://docs.conda.io/en/latest/">conda</a> package manager in C++. <code>mamba</code> is notably much faster and essentially compatible with <code>conda</code>, so it also works with <a href="https://github.com/necaris/conda.el">conda.el</a>. <code>micromamba</code>, however, implements only a subset of <code>mamba</code> commands, and as such requires a separate integration.</p>
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you normally install packages, I prefer <code>use-package</code> and <code>straight</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">micromamba</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>)
+</span></span></code></pre></div><p>Or clone the repository, add it to the <code>load-path</code> and <code>require</code> the package.</p>
+<p>If your <code>micromamba</code> binary is located in some place unknown to <code>executable-find</code>, set the <code>micromamba-executable</code> variable.</p>
+<p>If you are running shells (e.g. <a href="https://github.com/akermu/emacs-libvterm">vterm</a>) from Emacs, you probably want to set <code>auto_activate_base</code> in your <a href="https://docs.conda.io/projects/conda/en/latest/user-guide/configuration/index.html">.condarc</a> or <a href="https://mamba.readthedocs.io/en/latest/user_guide/configuration.html">.mambarc</a>, because the shells are launched in the correct environment anyway.</p>
+<h2 id="usage">Usage</h2>
+<p>The package has two entrypoints:</p>
+<ul>
+<li><code>M-x micromamba-activate</code> - activate the environment</li>
+<li><code>M-x micromamba-deactivate</code> - deactivate the environment</li>
+</ul>
+<p><code>micromamba-activate</code> prompts for the environment (by parsing <code>micromamba env list</code>). If some environments have duplicate names, these names are replaced by full paths.</p>
+<p>I’ve noticed that <code>micromamba</code> also sees <code>conda</code> environments, so migrating from <code>conda</code> was rather painless for me.</p>
+<h2 id="implementation-notes">Implementation notes</h2>
+<p>I initially wanted to extend <a href="https://github.com/necaris/conda.el">conda.el</a>, but decided it would be counterproductive for a few reasons.</p>
+<p>First, <code>conda</code> is rather slow, so <code>conda.el</code> does various tricks to avoid calling the <code>conda</code> executable. For instance, it gets the environment list from scanning the anaconda home directory instead of running <code>conda env list</code>. This is really not necessary with <code>micromamba</code>, which is written in C++.</p>
+<p>Second, and more importantly, <code>conda.el</code> relies heavily on passing <code>shell.posix+json</code> to <code>conda</code>. <code>micromamba</code> doesn’t support that. It supports the <code>--json</code> flag in some places, but not in the <code>activate</code> command, so I have to parse the output of <code>micromamba shell -s bash activate</code> and <code>micromamba shell -s bash deactivate</code> to get the environment configuration.</p>
+<p>This also means the package most likely won’t work out-of-the-box on Windows.</p>
+
+
+
+
916 days of Emacs
https://sqrtminusone.xyz/posts/2023-04-13-emacs/
@@ -1412,6 +1782,241 @@ I’ve seen a couple of cases where people would swap their username and
+
+ reverso.el
+ https://sqrtminusone.xyz/packages/reverso/
+ Sun, 28 Aug 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/reverso/
+
+ <p>Emacs client for <a href="https://www.reverso.net/">Reverso</a>. The implemented features are:</p>
+<ul>
+<li><a href="https://www.reverso.net/text-translation">Translation</a></li>
+<li><a href="https://context.reverso.net/translation/">Context</a> (AKA bilingual concordances)</li>
+<li><a href="https://www.reverso.net/spell-checker/english-spelling-grammar/">Grammar check</a></li>
+<li><a href="https://synonyms.reverso.net/synonym/">Synonyms search</a></li>
+</ul>
+<h2 id="installation">Installation</h2>
+<p>The package isn’t yet available anywhere but in this repository. My preferred way for such cases is <a href="https://github.com/jwiegley/use-package">use-package</a> and <a href="https://github.com/radian-software/straight.el">straight.el</a>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">reverso</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> (<span style="color:#008000">:host</span> <span style="color:#19177c">github</span> <span style="color:#008000">:repo</span> <span style="color:#ba2121">"SqrtMinusOne/reverso.el"</span>))
+</span></span></code></pre></div><p>Or clone the repository, add it to the <code>load-path</code> and <code>require</code> the package.</p>
+<h2 id="usage">Usage</h2>
+<p>There’s a single entrypoint for all implemented functions: <code>M-x reverso</code>. The UI is implemented using the excellent <a href="https://github.com/magit/transient/">transient.el</a>.</p>
+<h3 id="input-handling">Input Handling</h3>
+<p>All commands handle input as follows:</p>
+<p>By default, the input string is empty. If a command is launched with a region selected, use the string of that region. If launched with the prefix argument (<code>C-u</code>), use the entire buffer.</p>
+<p>Results are displayed in <code>reverso-result-mode</code> buffers. When launched within that buffer, the command uses the input string specific to the buffer. If launched with <code>C-u</code>, it uses the output string from that buffer (if available).</p>
+<h3 id="translation">Translation</h3>
+<p>Use <code>M-x reverso t</code> or <code>M-x reverso-translate</code> to invoke the translation transient.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/translation-transient.png"/>
+</figure>
+
+<p>The “Source language” and “Target language” parameters are self-explanatory. Note that not every language is compatible with every other language in the general case. “Swap languages” attempts to swap them.</p>
+<p>Enabling “Brief translation output” will display only the translated version of the string in the output buffer.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/translation-res.png"/>
+</figure>
+
+<p>Otherwise, the result buffer may contain the following sections:</p>
+<ul>
+<li><strong>Source text</strong> and <strong>Translation</strong></li>
+<li><strong>Corrected text</strong>, if available</li>
+<li><strong>Context results</strong>, if available</li>
+</ul>
+<p>Context results typically appear for short strings, as seen in the example from the screenshot.</p>
+<h3 id="context">Context</h3>
+<p>Use <code>M-x reverso c</code> or <code>M-x reverso-context</code> to invoke context search (or <a href="https://en.wikipedia.org/w/index.php?title=Online_bilingual_concordance&redirect=no">bilingual concordances</a>, essentially a Rosetta stone generator).</p>
+<p>The input/output UI resembles that of the translation command.</p>
+<p>Interestingly, direct context search often yields different results than the “Context results” section of the translation command. Hence, checking both might provide more comprehensive data.</p>
+<h3 id="synonyms">Synonyms</h3>
+<p>Use <code>M-x reverso s</code> or <code>M-x reverso-synonyms</code> to invoke the synonyms search.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/synonyms-transient.png"/>
+</figure>
+
+<figure><img src="https://sqrtminusone.xyz/reverso-img/synonyms-res.png"/>
+</figure>
+
+<p>If necessary, results are segmented by parts of speech.</p>
+<p>Each part of speech section contains up to three subsections:</p>
+<ul>
+<li>Synonyms</li>
+<li>Examples</li>
+<li>Antonyms</li>
+</ul>
+<h3 id="grammar-check">Grammar check</h3>
+<p>Use <code>M-x reverso g</code> or <code>M-x reverso-grammar</code> to invoke the grammar check.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/grammar-transient.png"/>
+</figure>
+
+<p>Currently, only English, French, Spanish, and Italian languages are available.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/grammar-res.png"/>
+</figure>
+
+<p>The results may contain the following sections:</p>
+<ul>
+<li><strong>Source text</strong>, highlighting errors with <code>reverso-error-face</code></li>
+<li><strong>Corrected text</strong></li>
+<li><strong>Corrections</strong></li>
+</ul>
+<h3 id="grammar-check-in-buffer">Grammar check in buffer</h3>
+<p>It can be convenient to apply the grammar check directly to the current buffer without displaying results in another buffer. Use <code>M-x reverso b</code> or <code>M-x reverso-grammar-buffer</code> for this.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/grammar-buffer-transient.png"/>
+</figure>
+
+<p>Running <code>e</code> there (or <code>M-x reverso-check-buffer</code>) utilizes the current buffer as input and highlights any found errors using overlays. If a region is selected, the check is confined to that region.</p>
+<p>There are a couple of caveats there. First, the service considers each linebreak as a new line, which is incompatible with <a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Filling.html">filling text</a>, i.e. breaking it into lines of a specified width. The “Remove linebreaks” option (<code>l</code>) is a workaround for this.</p>
+<p>Secondly, the service usually freaks out with special syntax, for instance, Org Mode links.</p>
+<p>The third issue partly follows from the second one, as the service often finds “errors” within hidden parts of Org links. Either skip these errors or execute <code>M-x org-toggle-link-display</code> in Org files beforehand.</p>
+<p>Lastly (and this applies to all other methods as well), the API usually restricts input size. If the service returns an error, try running the command on a smaller region of the buffer.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/grammar-buffer-res.png"/>
+</figure>
+
+<p>When the cursor is placed on an error, the “Information” section provides details.</p>
+<p>“Fix error” (<code>f</code> or <code>M-x reverso-check-fix-at-point</code>) opens a completion interface with potential fixes. “Ignore error” (<code>i</code> or <code>M-x reverso-check-ignore-error</code>) simply removes the overlay and moves to the next error.</p>
+<p>“Previous error” (<code>p</code> or <code>M-x reverso-check-prev-error</code>), “Next error” (<code>n</code> or <code>M-x reverso-check-next-error</code>), “First error” (<code>P</code> or <code>M-x reverso-check-first-error</code>) and “Last error” (<code>L</code> or <code>M-x reverso-check-last-error</code>) serve to navigate the error list.</p>
+<p>“Clear” (<code>c</code> or <code>M-x reverso-clear</code>) removes error overlays. If a region is selected, it removes overlays only in that region; otherwise, it removes them from the entire buffer.</p>
+<h3 id="history">History</h3>
+<p>Enable <code>reverso-history-mode</code> to keep history:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">reverso-history-mode</span>)
+</span></span></code></pre></div><p>I haven’t implemented persistence yet, but I might in the future.</p>
+<p>After enabling the minor mode, <code>M-x reverso-history</code> or <code>M-x reverso h</code> will display recent commans. <code>RET</code> on shows the results of each command.</p>
+<h2 id="caveats">Caveats</h2>
+<p>Before proceeding further, here are some general caveats to be aware of.</p>
+<p>Firstly, the package uses a reverse-engineered API, so all the typical consequences apply, such as sudden irreparable breakages. Although I’ve been using it for over a year, so… maybe not.</p>
+<p>Secondly, the limit on input size has been mentioned. The obvious is executing commands on a smaller region.</p>
+<p>Thirdly, there have been reports that Reverso dispatches <strong>IP bans</strong> to particularly enthusiastic users, so be cautious if you’re sending lots of automated queries. This is also why I didn’t implement running one command for multiple consecutive regions.</p>
+<p>Lastly, exercise caution with the content sent to the service. Avoid inadvertently sharing confidential information (like passwords) or anything that could be used against you in other ways. While the service claims to be <a href="https://www.reverso.net/privacy.aspx?lang=EN">GDPR-compliant</a>, we can’t actually check that.</p>
+<h2 id="customization">Customization</h2>
+<p>Run <code>M-x customize-group reverso</code> to view the available parameters. Here are a few.</p>
+<p>If you don’t need all 17 languages, customize the <code>reverso-languages</code> variable to narrow down the list:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">reverso-languages</span> <span style="color:#666">'</span>(<span style="color:#19177c">english</span> <span style="color:#19177c">german</span> <span style="color:#19177c">russian</span>))
+</span></span></code></pre></div><p>If the length of <code>reverso-languages</code> exceeds <code>reverso-language-completing-read-threshold</code>, switching a language in transient buffers will invoke <code>completing-read</code> (minibuffer completion). Otherwise, it will simply switch to the next language available.</p>
+<p><code>reverso-max-display-lines-in-input</code> controls the maximum number of lines displayed in the input section of a transient buffer.</p>
+<p>The available faces:</p>
+<ul>
+<li><code>reverso-highlight-face</code></li>
+<li><code>reverso-error-face</code></li>
+<li><code>reverso-heading-face</code></li>
+<li><code>reverso-keyword-face</code></li>
+<li><code>reverso-definition-face</code></li>
+</ul>
+<p>are inherited from the faces of <code>transient.el</code> and <code>basic-faces</code> to look nice.</p>
+<h2 id="elisp-api">Elisp API</h2>
+<p>In Emacs Lisp, there are four primary functions that interact with the Reverso API:</p>
+<ul>
+<li><code>reverso--translate</code></li>
+<li><code>reverso--get-context</code></li>
+<li><code>reverso--get-grammar</code></li>
+<li><code>reverso--get-context</code></li>
+</ul>
+<p>Refer to the docstrings for more detailed information.</p>
+<p>Each function is asynchronous, and the results are retrieved via a callback.</p>
+<p>As Reverso sometimes modifies its available languages and compatibility matrix, so if you change that, execute <code>reverso-verify-settings</code> to check for potential errors.</p>
+<h2 id="alternatives-and-observations">Alternatives and Observations</h2>
+<p>A widely recognized translation service is <a href="https://translate.google.com/">Google Translate</a>, so of course, there’s an <a href="https://github.com/atykhonov/google-translate">Emacs client</a> for it.</p>
+<p>The <a href="https://github.com/emacs-grammarly">emacs-grammarly</a> package series provides the Elisp API for <a href="https://www.grammarly.com/">Grammarly</a> (a grammar checking service) along with multiple frontends. Unlike Reverso, Grammarly has an official API (so you don’t risk getting an IP ban), and it allows a much larger input size.</p>
+<p>Additionally, Grammarly is less bothered by Org and Markdown syntax, although it struggles with inline code blocks. It seems to do work generally better than Reverso, but it also generates a lot of false positives. For instance, it finds a lot of issues in <a href="https://www.economist.com/">The Economist</a> articles, which, I think, have beautiful English.</p>
+<p>Another notable grammar-checking solution is <a href="https://languagetool.org/">LanguageTool</a>, which can be <a href="https://dev.languagetool.org/http-server">run offline</a> and used with its <a href="https://github.com/mhayashi1120/Emacs-langtool">Emacs package</a>. This tool offers the advantage of unlimited usage and doesn’t transmit your data to a third-party server you can’t control. But it still doesn’t like markup syntaxes.</p>
+<p>Also, I’ve been pretty happy with <a href="https://github.com/valentjn/ltex-ls">LTeX LS</a>, which is a LanguageTool-based language server explicitly designed to support markup formats like Org, Markdown, LaTeX, among others.</p>
+<p>The <a href="https://www.npmjs.com/package/reverso-api">reverso-api</a> npm package implements the same commands in JavaScript. It also provided invaluable information for creating this package.</p>
+
+
+
+
+
+ elfeed-sync
+ https://sqrtminusone.xyz/packages/elfeed-sync/
+ Sun, 29 May 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/elfeed-sync/
+
+ <p>Sync read/marked status of entries between <a href="https://github.com/skeeto/elfeed">elfeed</a> and <a href="https://tt-rss.org/">tt-rss</a>. Supports <a href="https://github.com/SqrtMinusOne/elfeed-summary">elfeed-summary</a>.</p>
+<p>DISCLAIMER: It’s still an alpha version of the package, so you may want to backup your elfeed index and tt-rss database.</p>
+<figure><img src="https://sqrtminusone.xyz/elfeed-sync-img/screenshot.png"/>
+</figure>
+
+<h2 id="installation">Installation</h2>
+<p>The project consists of the tt-rss plugin and the Emacs package.</p>
+<p>If you are using the <a href="https://git.tt-rss.org/fox/ttrss-docker-compose.git/tree/README.md">tt-rss docker</a> setup, the steps are as follows. Change them accordingly if you are not.</p>
+<ol>
+<li>
+<p>Mount the <code>/var/www/html</code> directory from the container somewhere to the filesystem as described <a href="https://git.tt-rss.org/fox/ttrss-docker-compose.wiki.git/tree/Home.md#how-do-i-use-dynamic-image-for-development">here</a>.</p>
+</li>
+<li>
+<p>Put the repository to the <code>tt-rss/plugins.local/elfeed_sync</code> folder:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#008000">cd</span> ./html/tt-rss/plugins.local/
+</span></span><span style="display:flex;"><span>git clone https://github.com/SqrtMinusOne/elfeed-sync.git elfeed_sync
+</span></span></code></pre></div></li>
+<li>
+<p>Add <code>elfeed_sync</code> to the <code>TTRSS_PLUGINS</code> environment variable.</p>
+<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">TTRSS_PLUGINS=auth_internal, auth_remote, nginx_xaccel, elfeed_sync
+</code></pre></li>
+<li>
+<p>Allow larger request body sizes in nginx. Add the following to the <code>server</code> directive:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cfg" data-lang="cfg"><span style="display:flex;"><span><span style="color:#7d9029">client_max_body_size 10M;</span>
+</span></span></code></pre></div><p>For me, the sync payload is around 3M.</p>
+</li>
+<li>
+<p>Increase the read timeout in nginx. Add the following to the php location directive:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cfg" data-lang="cfg"><span style="display:flex;"><span><span style="color:#7d9029">fastcgi_read_timeout 600;</span>
+</span></span></code></pre></div><p>Syncing the entries is usually pretty fast, but the first feed sync takes a while.</p>
+</li>
+<li>
+<p>Then restart tt-rss. Check if the plugin appears in the Preferences > Plugins section.</p>
+</li>
+<li>
+<p>Enable “Allows accessing this account through the API” in the Preferences > Preferences. You also may want to disable “Purge unread articles”, because elfeed doesn’t do that.</p>
+</li>
+</ol>
+<p>Install the Emacs package however you normally install packages, I prefer use-package and straight.el. Make sure to enable <code>elfeed-sync-mode</code>.</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">elfeed-sync</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> (<span style="color:#008000">:host</span> <span style="color:#19177c">github</span> <span style="color:#008000">:repo</span> <span style="color:#ba2121">"SqrtMinusOne/elfeed-sync"</span>)
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:after</span> <span style="color:#19177c">elfeed</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:config</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">elfeed-sync-mode</span>))
+</span></span></code></pre></div><p>Then set up the following variables:</p>
+<ul>
+<li><code>elfeed-sync-tt-rss-instance</code> - point that to your tt-rss instance, e.g.
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>https://example.com/tt-rss
+</span></span></code></pre></div>(No trailing slash)</li>
+<li><code>elfeed-sync-tt-rss-login</code></li>
+<li><code>elfeed-sync-tt-rss-password</code></li>
+<li><code>elfeed-sync-unread-tag</code> - elfeed tag to map to read status in tt-rss. <code>unread</code> by default.</li>
+<li><code>elfeed-sync-marked-tag</code> - elfeed tag to map to marked status in tt-rss. <code>later</code> by default.</li>
+</ul>
+<h2 id="usage">Usage</h2>
+<h3 id="syncing-the-feed-list">Syncing the feed list</h3>
+<p>The first thing you probably want to do is to sync the feed list.</p>
+<p>It makes little sense to sync tt-rss feeds to elfeed, because people often use projects like <a href="https://github.com/remyhonig/elfeed-org">elfeed-org</a>. But it’s possible to sync elfeed feeds to tt-rss.</p>
+<p>The function <code>M-x elfeed-sync-feeds</code> does exactly that. If you have <a href="https://github.com/SqrtMinusOne/elfeed-summary">elfeed-summary</a> installed and tt-rss categories enabled, the function will recreate the elfeed-summary tree in tt-rss.</p>
+<p>The first run of the function takes a while because tt-rss has to fetch the feed at the moment of the first subscription. Which is why increasing the timeout may be necessary.</p>
+<p>However, running the function multiple times until it succeeds should also work.</p>
+<h3 id="syncing-the-entry-list">Syncing the entry list</h3>
+<p>To sync the entry list, run <code>M-x elfeed-sync</code>. The sync usually takes a couple of seconds.</p>
+<p>The sync finishes at the “Sync complete!” message. Check the <code>*elfeed-log*</code> buffer for statistics.</p>
+<p>Occasionally, some entries do not match. Here are the possible cases:</p>
+<ul>
+<li>Entry exists in the elfeed database, but not in tt-rss.
+Run <code>M-x elfeed-sync-search-missing</code> to display such entries.</li>
+<li>Entry exists in the tt-rss database, but not in elfeed:
+<ul>
+<li>Entry appeared in the feed after the last <code>elfeed-update</code>.
+Run <code>M-x elfeed-update</code> and then <code>M-x elfeed-sync</code>.</li>
+<li>Entry appeared and disappeared in the feed after the last <code>elfeed-update</code>.
+Such an entry will never get to the elfeed database. If you want to, run <code>M-x elfeed-sync</code> and then <code>M-x elfeed-sync-read-ttrss-missing</code> to mark all such entries as read.</li>
+</ul>
+</li>
+<li>Entry appeared in the feed before <code>elfeed-sync-look-back</code>.
+Such an entry will never be matched. This is an inconvenience if you have just set up tt-rss, it fetched old entries from the feeds and such entries remain permanently unread because they are untouched by the <code>M-x elfeed-sync</code>.
+To mark such entries as read, run <code>M-x elfeed-sync-read-ttrss-old</code>.</li>
+</ul>
+<h2 id="implementation-details">Implementation details</h2>
+<p>The heavy-lifting is done on the elisp side because I ran into strange performance issues with associative arrays in PHP.</p>
+<p>Check the <code>elfeed-sync--do-sync</code> function for the description of the synchronization algorithm. The tl;dr is to download all entries from tt-rss and match each entry against the elfeed database. In the case of discrepancy update whichever entry has the lower priority.</p>
+
+
+
+
Extending elfeed with PDF viewer and subtitles fetcher
https://sqrtminusone.xyz/posts/2022-05-09-pdf/
@@ -1995,6 +2600,480 @@ I’ve seen a couple of cases where people would swap their username and
+
+ avy-dired
+ https://sqrtminusone.xyz/packages/avy-dired/
+ Fri, 01 Apr 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/avy-dired/
+
+ <p>Doing some experimentation with avy & dired. Still somewhat flaky.</p>
+<figure><img src="https://sqrtminusone.xyz/avy-dired-img/screenshot.png"/>
+</figure>
+
+<p>The only available command is <code>M-x avy-dired-goto-line</code>. Use <code>K</code> and <code>J</code> to scroll up and down while in the avy state, <code>C-g</code> or <code>q</code> to quit.</p>
+
+
+
+
+
+ elfeed-summary
+ https://sqrtminusone.xyz/packages/elfeed-summary/
+ Sat, 26 Mar 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/elfeed-summary/
+
+ <figure><a href="https://melpa.org/#/elfeed-summary"><img src="https://melpa.org/packages/elfeed-summary-badge.svg"/></a>
+</figure>
+
+<p>The package provides a tree-based feed summary interface for <a href="https://github.com/skeeto/elfeed">elfeed</a>. The tree can include individual feeds, <a href="https://github.com/skeeto/elfeed#filter-syntax">searches</a>, and groups. It mainly serves as an easier “jumping point” for elfeed, so to make querying a subset of the elfeed database one action away.</p>
+<p>Inspired by <a href="https://github.com/newsboat/newsboat">newsboat</a>.</p>
+<figure><img src="https://sqrtminusone.xyz/elfeed-summary-img/screenshot.png"/>
+</figure>
+
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA, so install it however you normally install packages. My preferred way is <code>use-package</code> with <code>straight</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">elfeed-summary</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>)
+</span></span></code></pre></div><p>Of course, you have to have <a href="https://github.com/skeeto/elfeed">elfeed</a> configured.</p>
+<h2 id="usage">Usage</h2>
+<p>Running <code>M-x elfeed-summary</code> opens up the summary buffer, as shown on the screenshot.</p>
+<p>The tree consists of:</p>
+<ul>
+<li>feeds;</li>
+<li>searches;</li>
+<li>groups, that can include other groups, feeds, and searches.</li>
+</ul>
+<p>Groups can also be generated automatically.</p>
+<p>Available keybindings in the summary mode:</p>
+<table>
+<thead>
+<tr>
+<th>Keybinding</th>
+<th>Command</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><code>RET</code></td>
+<td><code>elfeed-summary--action</code></td>
+<td>Open thing under the cursor (a feed, search, or a group). If there is at least one unread item, it will show only unread items.</td>
+</tr>
+<tr>
+<td><code>M-RET</code></td>
+<td><code>elfeed-summary--action-show-read</code></td>
+<td>Open thing under the cursor, but always include read items</td>
+</tr>
+<tr>
+<td><code>q</code></td>
+<td>…</td>
+<td>Quit the summary buffer</td>
+</tr>
+<tr>
+<td><code>r</code></td>
+<td><code>elfeed-summary--refresh</code></td>
+<td>Refresh the summary buffer</td>
+</tr>
+<tr>
+<td><code>R</code></td>
+<td><code>elfeed-summary-update</code></td>
+<td>Run update for elfeed feeds</td>
+</tr>
+<tr>
+<td><code>u</code></td>
+<td><code>elfeed-summary-toggle-only-unread</code></td>
+<td>Toggle showing only unread entries</td>
+</tr>
+<tr>
+<td><code>U</code></td>
+<td><code>elfeed-summary--action-mark-read</code></td>
+<td>Mark everything in the entry under the cursor as read</td>
+</tr>
+</tbody>
+</table>
+<p>The standard keybindings from <a href="https://magit.vc/manual/magit.html#Sections">magit-section</a> are also available, for instance <code>TAB</code> toggles the visibility of the current group. <a href="https://github.com/emacs-evil/evil">evil-mode</a> is also supported.</p>
+<h2 id="configuration">Configuration</h2>
+<h3 id="tree-configuration">Tree configuration</h3>
+<p>The structure of the tree is determined by the <code>elfeed-summary-settings</code> variable.</p>
+<p>This is a list of these possible items:</p>
+<ul>
+<li>Group <code>(group . <group-params>)</code>
+Groups are used to group elements under collapsible sections.</li>
+<li>Query <code>(query . <query-params>)</code>
+Query extracts a subset of elfeed feeds based on the given criteria. Each found feed will be represented as a line.</li>
+<li>Search <code>(search . <search-params>)</code>
+Elfeed search, as defined by <code>elfeed-search-set-filter</code>.</li>
+<li>Tags tree <code>(auto-tags . <auto-tags-params>)</code>
+A tree generated automatically from the available tags.</li>
+<li>Tag groups <code>(tag-groups . <tag-group-params>)</code>
+Insert one tag as one group.</li>
+<li>a few special forms</li>
+</ul>
+<p><code><group-params></code> is an alist with the following keys:</p>
+<ul>
+<li><code>:title</code> (mandatory)</li>
+<li><code>:elements</code> (mandatory) - elements of the group. The structure is the same as in the root definition.</li>
+<li><code>:face</code> - group face. The default face is <code>elfeed-summary-group-face</code>.</li>
+<li><code>:hide</code> - if non-nil, the group is collapsed by default.</li>
+</ul>
+<p><code><query-params></code> can be:</p>
+<ul>
+<li>A symbol of a tag.
+A feed will be matched if it has that tag.</li>
+<li><code>:all</code>. Will match anything.</li>
+<li><code>(title . "string")</code> or <code>(title . <form>)</code>
+Match feed title with <code>string-match-p</code>. <form> makes sense if you
+want to pass something like <code>rx</code>.</li>
+<li><code>(author . "string")</code> or <code>(author . <form>)</code></li>
+<li><code>(url . "string")</code> or <code>(url . <form>)</code></li>
+<li><code>(and <q-1> <q-2> ... <q-n>)</code>
+Match if all the conditions 1, 2, …, n match.</li>
+<li><code>(or <q-1> <q-2> ... <q-n>)</code> or <code>(<q-1> <q-2> ... <q-n>)</code>
+Match if any of the conditions 1, 2, …, n match.</li>
+<li><code>(not <query>)</code></li>
+</ul>
+<p>Feed tags for the query are determined by the <code>elfeed-feeds</code> variable.</p>
+<p>Query examples:</p>
+<ul>
+<li><code>(emacs lisp)</code>
+Return all feeds that have either “emacs” or “lisp” tags.</li>
+<li><code>(and emacs lisp)</code>
+Return all feeds that have both “emacs” and “lisp” tags.</li>
+<li><code>(and (title . "Emacs") (not planets))</code>
+Return all feeds that have “Emacs” in their title and don’t have
+the “planets” tag.</li>
+</ul>
+<p><code><search-params></code> is an alist with the following keys:</p>
+<ul>
+<li><code>:filter</code> (mandatory) filter string, as defined by
+<code>elfeed-search-set-filter</code></li>
+<li><code>:title</code> (mandatory) title.</li>
+<li><code>:tags</code> - list of tags to get the face of the entry.</li>
+</ul>
+<p><code><auto-tags-params></code> is an alist with the following keys:</p>
+<ul>
+<li><code>:max-level</code> - maximum level of the tree (default 2)</li>
+<li><code>:source</code> - which feeds to use to build the tree.
+Can be <code>:misc</code> (default) or <code>(query . <query-params>)</code>.</li>
+<li><code>:original-order</code> - do not try to build a more concise tree by
+putting the most frequent tags closer to the root of the tree.</li>
+<li><code>:faces</code> - list of faces for groups.</li>
+</ul>
+<p><code><tag-group-params></code> is an alist with the following keys:</p>
+<ul>
+<li><code>:source</code> - which feeds to use to build the tree.
+Can be <code>:misc</code> (default) or <code>(query . <query-params>)</code>.</li>
+<li><code>:repeat-feeds</code> - allow feeds to repeat. Otherwise, each feed is
+assigned to group with the least amount of members.</li>
+<li><code>:face</code> - face for groups.</li>
+</ul>
+<p>Available special forms:</p>
+<ul>
+<li><code>:misc</code> - print out feeds, not found by any query above.</li>
+</ul>
+<p>Also keep in mind that <code>'(key . ((values)))</code> is the same as <code>'(key (values))</code>. This helps to shorten the form in many cases.</p>
+<p>Also, this variable is not validated by any means, so wrong values can produce somewhat cryptic errors. Sorry about that.</p>
+<h3 id="example">Example</h3>
+<p>Here is an excerpt from my configuration that was used to produce this screenshot:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">elfeed-summary-settings</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">'</span>((<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"GitHub"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#19177c">url</span> <span style="color:#666">.</span> <span style="color:#ba2121">"SqrtMinusOne.private.atom"</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> <span style="color:#666">.</span> ((<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Guix packages"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">github</span> <span style="color:#19177c">guix_packages</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:hide</span> <span style="color:#800">t</span>)))))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Blogs [Software]"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> <span style="color:#19177c">software_blogs</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Blogs [People]"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">blogs</span> <span style="color:#19177c">people</span> (<span style="color:#19177c">not</span> <span style="color:#19177c">emacs</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Emacs"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">blogs</span> <span style="color:#19177c">people</span> <span style="color:#19177c">emacs</span>))))))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Podcasts"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> <span style="color:#19177c">podcasts</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Videos"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Music"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">videos</span> <span style="color:#19177c">music</span>))))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Tech"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">videos</span> <span style="color:#19177c">tech</span>))))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"History"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">videos</span> <span style="color:#19177c">history</span>))))
+</span></span><span style="display:flex;"><span> <span style="color:#408080;font-style:italic">;; ...</span>
+</span></span><span style="display:flex;"><span> ))
+</span></span><span style="display:flex;"><span> <span style="color:#408080;font-style:italic">;; ...</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Miscellaneous"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Searches"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">search</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:filter</span> <span style="color:#666">.</span> <span style="color:#ba2121">"@6-months-ago sqrtminusone"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"About me"</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">search</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:filter</span> <span style="color:#666">.</span> <span style="color:#ba2121">"+later"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Check later"</span>))))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Ungrouped"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span> <span style="color:#008000">:misc</span>))))))
+</span></span></code></pre></div><h3 id="automatic-generation-of-groups">Automatic generation of groups</h3>
+<h4 id="auto-tags"><code>auto-tags</code></h4>
+<p>As described in the <a href="#tree-configuration-1">tree configuration</a> section, there are two ways to avoid defining all the relevant groups manually, <code>auto-tags</code> and <code>tag-groups</code>. Both use tags that are defined in <code>elfeed-feeds</code>.</p>
+<p><code>auto-tags</code> tries to build the most concise tree from these tags. E.g. if we have feeds:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>feed1 tag1 tag2
+</span></span><span style="display:flex;"><span>feed2 tag1 tag2
+</span></span><span style="display:flex;"><span>feed3 tag1 tag3
+</span></span><span style="display:flex;"><span>feed4 tag1 tag3
+</span></span></code></pre></div><p>It will create the following tree:</p>
+<ul>
+<li>tag1
+<ul>
+<li>tag2
+<ul>
+<li>feed1</li>
+<li>feed2</li>
+</ul>
+</li>
+<li>tag3
+<ul>
+<li>feed3</li>
+<li>feed4</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+<p>The tree is truncated by <code>:max-level</code>, which is 2 by default.</p>
+<p>If tags don’t form this kind of hierarchy in <code>elfeed-feeds</code>, the algorithm will still try to build the most “optimal” tree, where the most frequent tags are on the top.</p>
+<p>To avoid that you can set <code>(:original-order . t)</code>, in which case each feed will be placed at the path <code>tag1 tag2 ... tagN feed</code>, where the order of tags is the same as in <code>elfeed-feeds</code>. By the way, this allows reproducing the hierarchy of <a href="https://github.com/remyhonig/elfeed-org">elfeed-org</a>, e.g. this structure:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>* tag1 :tag1:
+</span></span><span style="display:flex;"><span>** feed1
+</span></span><span style="display:flex;"><span>** feed2 :tag2:
+</span></span><span style="display:flex;"><span>** feed3 :tag2:
+</span></span><span style="display:flex;"><span>* tag3 :tag3:
+</span></span><span style="display:flex;"><span>** feed4 :tag2:
+</span></span><span style="display:flex;"><span>** feed5 :tag2:
+</span></span><span style="display:flex;"><span>** feed6 :tag2:
+</span></span></code></pre></div><p>Will be converted to this:</p>
+<ul>
+<li>tag1
+<ul>
+<li>feed1</li>
+<li>tag2
+<ul>
+<li>feed2</li>
+<li>feed3</li>
+</ul>
+</li>
+</ul>
+</li>
+<li>tag3
+<ul>
+<li>tag2
+<ul>
+<li>feed4</li>
+<li>feed5</li>
+<li>feed6</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+<p>Whereas without <code>:original-order</code> the structure will be:</p>
+<ul>
+<li>tag1
+<ul>
+<li>feed1</li>
+</ul>
+</li>
+<li>tag2
+<ul>
+<li>tag1
+<ul>
+<li>feed2</li>
+<li>feed3</li>
+</ul>
+</li>
+<li>tag3
+<ul>
+<li>feed4</li>
+<li>feed5</li>
+<li>feed6</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+<h4 id="tag-groups"><code>tag-groups</code></h4>
+<p>The second option is <code>tag-groups</code>, which creates a group for each tag.</p>
+<p>By default, each feed is assigned to its less frequent tag. This can be turned off by setting <code>(:repeat-feeds . t)</code>.</p>
+<p>E.g., the elfeed-org setup from the section above will be converted to this structure:</p>
+<ul>
+<li>tag1
+<ul>
+<li>feed1</li>
+<li>feed2</li>
+<li>feed3</li>
+</ul>
+</li>
+<li>tag3
+<ul>
+<li>feed4</li>
+<li>feed5</li>
+<li>feed6</li>
+</ul>
+</li>
+</ul>
+<p>And with <code>:repeat-feeds</code>:</p>
+<ul>
+<li>tag1
+<ul>
+<li>feed1</li>
+<li>feed2</li>
+<li>feed3</li>
+</ul>
+</li>
+<li>tag2
+<ul>
+<li>feed2</li>
+<li>feed3</li>
+<li>feed4</li>
+<li>feed5</li>
+<li>feed6</li>
+</ul>
+</li>
+<li>tag3
+<ul>
+<li>feed4</li>
+<li>feed5</li>
+<li>feed6</li>
+</ul>
+</li>
+</ul>
+<h4 id="common-options">Common options</h4>
+<p>Both <code>auto-tags</code> and <code>tag-groups</code> allow setting the <code>:search</code> parameter.</p>
+<p>The default value is <code>(:search . :misc)</code>, i.e. use feeds that weren’t found by other queries.</p>
+<p>Passing <code>(:search . (query . <query-params>))</code> is another option.</p>
+<h3 id="faces">Faces</h3>
+<p>Group faces by default use the <code>elfeed-summary-group-faces</code> variable, which serves as a list of faces for each level of the tree. Individual group faces can be overridden with the <code>:face</code> attribute.</p>
+<p>Feed faces by default reuse <a href="https://github.com/skeeto/elfeed#custom-tag-faces">the existing elfeed mechanism</a>. The tags for feeds are taken from the <code>elfeed-feeds</code> variable; if a feed has at least one unread entry, the unread tag is added to the list. This can be overridden by setting the <code>elfeed-summary-feed-face-fn</code> variable.</p>
+<p>Searches are mostly the same as feeds, but tags for the search are taken from the <code>:tags</code> attribute. This also can be overridden with <code>elfeed-summary-search-face-fn</code> variable.</p>
+<h3 id="opening-elfeed-search-in-other-window">Opening <code>elfeed-search</code> in other window</h3>
+<p>If you set:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">elfeed-summary-other-window</span> <span style="color:#800">t</span>)
+</span></span></code></pre></div><p>Then <code>RET</code> and <code>M-RET</code> in the <code>elfeed-summary</code> buffer will open the search buffer in other window.</p>
+<p><code>elfeed-summary-width</code> regulates the width of the remaining summary window in this case. It is useful because the data in the search buffer is generally wider than in the summary buffer. The variable can also be set to <code>nil</code> to disable this behavior.</p>
+<h3 id="skipping-feeds">Skipping feeds</h3>
+<p><a href="https://tt-rss.org/">tt-rss</a> has a feature to disable updating a particular feed but keep it in the feed list. I also want that for elfeed.</p>
+<p>To use that, set <code>elfeed-summary-skip-sync-tag</code> to some value:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">elfeed-summary-skip-sync-tag</span> <span style="color:#19177c">'skip</span>)
+</span></span></code></pre></div><p>And tag the feeds you want to skip with this tag. Then, running <code>M-x elfeed-summary-update</code> will skip them. This won’t affect <code>M-x elfeed-update</code> unless you:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">advice-add</span> <span style="color:#00f">#'</span><span style="color:#19177c">elfeed-update</span> <span style="color:#008000">:override</span> <span style="color:#00f">#'</span><span style="color:#19177c">elfeed-summary-update</span>)
+</span></span></code></pre></div><p>Also watch out if you use <a href="https://github.com/remyhonig/elfeed-org">elfeed-org</a> and want to use the <code>ignore</code> tag, because this package omits feeds with this tag altogether (configurable by <code>rmh-elfeed-org-ignore-tag</code>).</p>
+<h3 id="other-options">Other options</h3>
+<p>Also take a look at <code>M-x customize-group elfeed-summary</code> for the rest of available options.</p>
+<h2 id="ideas-and-alternatives">Ideas and alternatives</h2>
+<p>The default interface of elfeed is just a list of all entries. Naturally, it gets hard to navigate when there are a lot of sources with varying frequencies of posts.</p>
+<p>Elfeed itself provides one solution, which is using <a href="https://github.com/skeeto/elfeed#bookmarks">bookmarks</a> to save individual <a href="https://github.com/skeeto/elfeed#filter-syntax">searches</a>. This can work, but it can be somewhat cumbersome.</p>
+<p><a href="https://github.com/sp1ff/elfeed-score">elfeed-score</a> is another solution, which introduces scoring rules for entries. Thus, with proper rules set, the most important entries should be on the top of the list. You can take a look at <a href="https://www.youtube.com/watch?v=rvWbUGx9U5E">this video by John Kitchin</a> to see how this can work.</p>
+<p>However, I mostly had <code>elfeed-score</code> to group entries to sets with equal scores, and I then processed one such set or the other. This is why I decided this package is a better fit for my workflow.</p>
+<p>Another idea I used often before that is this function:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/elfeed-search-filter-source</span> (<span style="color:#19177c">entry</span>)
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Filter elfeed search buffer by the feed under the cursor."</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">interactive</span> (<span style="color:#00f">list</span> (<span style="color:#19177c">elfeed-search-selected</span> <span style="color:#008000">:ignore-region</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> (<span style="color:#19177c">elfeed-entry-p</span> <span style="color:#19177c">entry</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">elfeed-search-set-filter</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#00f">concat</span>
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"@6-months-ago "</span>
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"+unread "</span>
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"="</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">replace-regexp-in-string</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">rx</span> <span style="color:#ba2121">"?"</span> (<span style="color:#00f">*</span> <span style="color:#19177c">not-newline</span>) <span style="color:#19177c">eos</span>)
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">""</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">elfeed-feed-url</span> (<span style="color:#19177c">elfeed-entry-feed</span> <span style="color:#19177c">entry</span>)))))))
+</span></span></code></pre></div><p>I’ve bound it to <code>o</code>, so I would open <code>elfeed</code>, press <code>o</code>, and only see unread entries from a particular feed. Then I cleaned the filter and switched to the next feed. Once again, a tree with feeds is obviously a better tool for such a workflow.</p>
+<p>The last solution I want to mention is <a href="https://github.com/manojm321/elfeed-dashboard">elfeed-dashboard</a>, although I didn’t test this one. It looks similar to this package but seems to require much more fine-tuning, for instance, it doesn’t allow to list all the feeds with a certain tag in a group.</p>
+
+
+
+
+
+ password-store-ivy
+ https://sqrtminusone.xyz/packages/password-store-ivy/
+ Sun, 13 Feb 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/password-store-ivy/
+
+ <p>A <a href="https://www.passwordstore.org/">pass</a> frontend based on <a href="https://github.com/abo-abo/swiper#ivy">Ivy</a>, made primarily to use with <a href="https://github.com/ch11ng/exwm">EXWM</a> and <a href="https://github.com/tumashu/ivy-posframe">ivy-posframe</a>. Types fields from entries.</p>
+<p>Also take a look at Nicolas Petton’s <a href="https://github.com/NicolasPetton/pass">pass</a>, <code>password-store-ivy</code> is designed as complementary to the Nicolas’ package.</p>
+<p>This package is made with Ivy because I need some fine-tuning like actions and turning off sorting in some completions, and Ivy happens to be the completion system I’m using now.</p>
+<h2 id="installation">Installation</h2>
+<p>As the package isn’t yet available anywhere but in this repository, you can clone the repository, add it to the load-path and require the package. My preferred way is <code>use-package</code> with <code>straight</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">password-store-ivy</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> (<span style="color:#008000">:host</span> <span style="color:#19177c">github</span> <span style="color:#008000">:repo</span> <span style="color:#ba2121">"SqrtMinusOne/password-store-ivy"</span>)
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:after</span> (<span style="color:#19177c">exwm</span>))
+</span></span></code></pre></div><p>This package types stuff with <code>xdotool</code>, so you need to have that available in your <code>$PATH</code>.</p>
+<h2 id="usage">Usage</h2>
+<p>Emacs’ built-in <a href="https://www.gnu.org/software/emacs/manual/html_node/auth/The-Unix-password-store.html">password store</a> integration has to be set up.</p>
+<p>The only command is <code>M-x password-store-ivy</code>, which invokes Ivy to select an entry from the pass database. Available commands in the selection buffer:</p>
+<ul>
+<li><code>M-a</code>. Perform autotype</li>
+<li><code>M-p</code>. Type password</li>
+<li><code>M-u</code>. Type username</li>
+<li><code>M-U</code>. Type url</li>
+<li><code>M-f</code>. Select a field to type</li>
+</ul>
+<h2 id="customization">Customization</h2>
+<p>There are a few parameters that control delays:</p>
+<ul>
+<li><code>password-store-ivy-initial-wait</code> controls the initial delay before starting to type a sequence (in milliseconds)</li>
+<li><code>password-store-ivy-delay</code> controls the delay between typing characters (in milliseconds)</li>
+</ul>
+<p>There is also <code>password-store-ivy-sequences</code> that determines the sequence of actions <code>password-store-ivy</code> performs.</p>
+<p>It is an alist with the following required keys (corresponding to the basic actions):</p>
+<ul>
+<li><code>autotype</code></li>
+<li><code>password</code></li>
+<li><code>username</code></li>
+<li><code>url</code></li>
+</ul>
+<p>The values are lists of the following elements:</p>
+<ul>
+<li><code>wait</code>. Wait for <code>password-store-ivy-initial-wait</code> milliseconds</li>
+<li><code>(wait <milliseconds>)</code>. Wait for <code><milliseconds></code>.</li>
+<li><code>(key <key>)</code>. Type <code><key></code>.</li>
+<li><code>(field <field>)</code>. Type <code><field></code> of entry.</li>
+</ul>
+<p>For example, the starting values:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span><span style="color:#666">'</span>((<span style="color:#19177c">autotype</span> <span style="color:#666">.</span> (<span style="color:#19177c">wait</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">field</span> <span style="color:#666">.</span> <span style="color:#ba2121">"username"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">key</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Tab"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">field</span> <span style="color:#666">.</span> <span style="color:#19177c">secret</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">key</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Return"</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">password</span> <span style="color:#666">.</span> (<span style="color:#19177c">wait</span> (<span style="color:#19177c">field</span> <span style="color:#666">.</span> <span style="color:#19177c">secret</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">username</span> <span style="color:#666">.</span> (<span style="color:#19177c">wait</span> (<span style="color:#19177c">field</span> <span style="color:#666">.</span> <span style="color:#ba2121">"username"</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">url</span> <span style="color:#666">.</span> (<span style="color:#19177c">wait</span> (<span style="color:#19177c">field</span> <span style="color:#666">.</span> <span style="color:#ba2121">"url"</span>))))
+</span></span></code></pre></div><p>In addition to the global override, sequences can be overriden per-entry with a field called <code>sequence-<name></code>, where <code><name></code> is a key of <code>password-store-ivy-sequences</code>.</p>
+<p>For example, here is an override to press <code>Tab</code> twice:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span><pass>
+</span></span><span style="display:flex;"><span>username: thexcloud@gmail.com
+</span></span><span style="display:flex;"><span>url: <url>
+</span></span><span style="display:flex;"><span>sequence-autotype: (wait (field . "username") (key . "Tab") (key . "Tab") (field . secret) (key . "Return"))
+</span></span></code></pre></div>
+
+
+
A few cases of literate configuration
https://sqrtminusone.xyz/posts/2022-02-12-literate/
@@ -2506,6 +3585,180 @@ I’ve seen a couple of cases where people would swap their username and
+
+ org-journal-tags
+ https://sqrtminusone.xyz/packages/org-journal-tags/
+ Sun, 06 Feb 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/org-journal-tags/
+
+ <figure><a href="https://melpa.org/#/org-journal-tags"><img src="https://melpa.org/packages/org-journal-tags-badge.svg"/></a>
+</figure>
+
+<p>A package to make sense of <del>my life</del> <a href="https://github.com/bastibe/org-journal">org-journal</a> records.</p>
+<p>The package adds the <code>org-journal:</code> link type to Org Mode. When placed in an org-journal file, the link serves as a “tag” that references one or many paragraphs of the journal or the entire section. These tags are aggregated in the database that can be queried in various ways.</p>
+<h2 id="rationale">Rationale</h2>
+<p>Journal files, by their very nature, are weakly structured. A single journal note can reference multiple entities (or none) and can itself be composed of multiple parts that have in common only the date and time when they were written. Needless to say, it’s hard to find anything in such records.</p>
+<p>This package attempts to improve the accessibility of the journal by:</p>
+<ul>
+<li>Taking advantage of temporal data, e.g. allowing to query entries in some date range.</li>
+<li>Allowing to extract (and reference) only certain parts of a particular journal entry.</li>
+<li>Compensating weak structure by with more advanced query engine.</li>
+</ul>
+<p>For instance, when I’m writing down the progress on a job project, I can leave a tag like <code>job.<project-name></code> in the paragraph(s) related to that project. Later, I can query only those paragraphs that are referenced by this particular tag. The query results can then be narrowed, for instance, to include the word “backend”, or extended with some other tag.</p>
+<p>If no tag matches the subject matter, the journal can be queried with a regular expression, e.g. by searching some regex within a specific time frame. Subsequent searches are also significantly faster than the built-in <code>org-journal</code> search functionality due to the to caching mechanism.</p>
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you normally install packages, my preferred way is <code>use-package</code> with <code>straight</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">org-journal-tags</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:after</span> (<span style="color:#19177c">org-journal</span>)
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:config</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">org-journal-tags-autosync-mode</span>))
+</span></span></code></pre></div><h2 id="basic-usage">Basic usage</h2>
+<h3 id="adding-tags">Adding tags</h3>
+<p>To add an inline tag, you can manually create a link of the following format:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[[org-journal:<tag-name>][<tag-description>]]
+</span></span></code></pre></div><p>Or run <code>M-x org-journal-tags-insert-tag</code> to insert a tag with a completion interface. The description is not aggregated and thus optional. Also, <code><tag-name></code> cannot contain <code>:</code>.</p>
+<p>The link will reference the current Org Mode paragraph. If you want to reference more paragraphs, you can set the number of paragraphs like this:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[[org-journal:<tag-name>::<number-of-paragraphs>][<tag-description>]]
+</span></span></code></pre></div><p>Run <code>M-x org-journal-tags-link-get-region-at-point</code> to select the referenced region of the buffer.</p>
+<p>To add a tag to the entire section, run <code>M-x org-journal-tags-prop-set</code>, which will create or update the <code>Tags</code> property in the property drawer of the current time section. This command features a notmuch-like UI, i.e. completing read for multiple entries, where <code>+<tag></code> adds a tag and <code>-<tag></code> deletes a tag.</p>
+<p>If you decide to rename a tag, there’s <code>M-x org-journal-tags-refactor</code>.</p>
+<h3 id="tag-kinds">Tag kinds</h3>
+<p>Tag kind is a predefined class of tag with some extra functionality. The link format fo such tags is as follows:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[[org-journal:<kind>:<tag-name>][<tag-description>]]
+</span></span><span style="display:flex;"><span>[[org-journal:<kind>:<tag-name>::<number-of-paragraphs>][<tag-description>]]
+</span></span></code></pre></div><p>If <code><kind></code> is omitted, a tag is considered “normal”.</p>
+<p>Running <code>C-u M-x org-journal-tags-insert-tag</code> will first prompt for the tag kind and then for the tag itself from the set of already used tags of that kind.</p>
+<p>Running <code>C-u C-u M-x org-journal-tags-insert-tag</code> will also first prompt for the tag kind, but then will try to invoke the kind-specific tag selection logic, if such is available. For instance, the <code>contact</code> kind will prompt the <code>org-contacts</code> database.</p>
+<p>For now, the only available tag kind is <a href="https://repo.or.cz/org-contacts.git">org-contacts</a>.</p>
+<h3 id="adding-timestamps">Adding timestamps</h3>
+<p>In addition to tags, the package also aggregates inline timestamps, i.e. timestamps that are left in the text like this:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>This is a text. This is a text with <2022-04-07 Thu> a timestamp. This is a text again.
+</span></span></code></pre></div><p>A timestamp will reference just the current paragraph.</p>
+<p>Other forms of timestamps (<code>SCHEDULED</code>, <code>DEADLINE</code>, etc.) are not supported at the moment, because this functionality is implemented well enough by <a href="https://orgmode.org/manual/Agenda-Views.html">org-agenda</a>.</p>
+<p>The envisioned use case for this functionality to leave references for the future to be seen at a particular date.</p>
+<h3 id="database">Database</h3>
+<p>The package stores tags and references to these tags in a database.</p>
+<p><code>org-journal-tags-autosync-mode</code> enables synchronizing the database at the moment of saving of the org-journal buffer. You can also run the synchronization manually:</p>
+<ul>
+<li><code>M-x org-journal-tags-process-buffer</code> to process the current buffer.</li>
+<li><code>M-x org-journal-tags-db-sync</code> to sync changed org-journal files in the filesystem.</li>
+</ul>
+<p>The same mode enables saving the database on killing Emacs, but you can always run <code>M-x org-journal-tags-db-save</code> manually.</p>
+<p><code>M-x org-journal-tags-db-unload</code> saves and unloads the database from the memory, <code>M-x org-journal-tags-db-reset</code> creates a new database.</p>
+<h3 id="status-buffer">Status buffer</h3>
+<figure><img src="https://sqrtminusone.xyz/org-journal-tags-img/status.png"/>
+</figure>
+
+<p><em>(I replaced tag names with “X” just for the screenshot)</em></p>
+<p><code>M-x org-journal-tags-status</code> opens the status buffer with some statistics about the journal and tags. Press <code>?</code> to see the available keybindings.</p>
+<p>Pressing <code>RET</code> on a tag name in the “All tags” section should open a query buffer set to return all references for this tag.</p>
+<h3 id="query-constructor">Query constructor</h3>
+<figure><img src="https://sqrtminusone.xyz/org-journal-tags-img/query.png"/>
+</figure>
+
+<p>Pressing <code>s</code> in the status buffer or running <code>M-x org-journal-tags-transient-query</code> opens a <a href="https://magit.vc/manual/transient/">transient.el</a> buffer with query settings.</p>
+<p>The options are as follows:</p>
+<ul>
+<li><strong>Include tags</strong> filters the references so that each reference had at least one of these tags.</li>
+<li><strong>Exclude tags</strong> filters the references so that each reference didn’t have any of these tags.</li>
+<li><strong>Include children</strong> includes child tags to the previous two lists.</li>
+<li><strong>Tag location</strong> can filter only section tags on inline tags.</li>
+<li><strong>Start date</strong> and <strong>End date</strong> filter the references by date.</li>
+<li><strong>Filter timestamps</strong> filters the references so that they include a timestamp.</li>
+<li><strong>Timestamp start date</strong> and <strong>Timestamp end date</strong> filter
+timestamps by their date.</li>
+<li><strong>Regex</strong> filter the references by a regular expression. It can be a string or <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Rx-Notation.html">rx</a> expression (it just has to start with <code>(rx</code> in this case).</li>
+<li><strong>Narrow to regex</strong> makes it so that each reference had only paragraphs that have a regex match.</li>
+<li><strong>Sort</strong> sorts the result in ascending order. It’s descending by default.</li>
+</ul>
+<p>Pressing <code>RET</code> or <code>e</code> executes the query. Journal files are cached, so subsequent queries within one session are much faster.</p>
+<h3 id="query-results">Query results</h3>
+<figure><img src="https://sqrtminusone.xyz/org-journal-tags-img/query-results.png"/>
+</figure>
+
+<p>After the query completes, the package opens the results buffer. Press <code>?</code> to see the available keybindings there.</p>
+<p>Pressing <code>RET</code> opens the corresponding org-journal entry.</p>
+<p>Pressing <code>s</code> opens the query constructor buffer. If opened from inside the query results, the query constructor has 4 additional options:</p>
+<table>
+<thead>
+<tr>
+<th>Command</th>
+<th>Set operation</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><strong>Union</strong></td>
+<td>old ∪ new</td>
+<td>Add records of the new query to the displayed records</td>
+</tr>
+<tr>
+<td><strong>Intersection</strong></td>
+<td>old ∩ new</td>
+<td>Leave only those records that are both displayed and in the new query</td>
+</tr>
+<tr>
+<td><strong>Difference from current</strong></td>
+<td>old \ new</td>
+<td>Exclude records of the new query from the displayed records</td>
+</tr>
+<tr>
+<td><strong>Difference to current</strong></td>
+<td>new \ old</td>
+<td>Exclude displayed records from ones of the new query</td>
+</tr>
+</tbody>
+</table>
+<p>Thus it is possible to make any query that can be described as a sequence of such set operations.</p>
+<h2 id="advanced-usage">Advanced usage</h2>
+<h3 id="automatic-tagging">Automatic tagging</h3>
+<p>org-journal provides a hook to automatically add information to the journal entries.</p>
+<p>It can be used to automatically assign tags, for instance, based on hostname. Here’s an excerpt from my configuration:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/set-journal-header</span> ()
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">org-journal-tags-prop-apply-delta</span> <span style="color:#008000">:add</span> (<span style="color:#00f">list</span> (<span style="color:#00f">format</span> <span style="color:#ba2121">"host.%s"</span> (<span style="color:#00f">system-name</span>))))
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> (<span style="color:#00f">boundp</span> <span style="color:#19177c">'my/loc-tag</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">org-journal-tags-prop-apply-delta</span> <span style="color:#008000">:add</span> (<span style="color:#00f">list</span> <span style="color:#19177c">my/loc-tag</span>))))
+</span></span><span style="display:flex;"><span>
+</span></span><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'org-journal-after-entry-create-hook</span>
+</span></span><span style="display:flex;"><span> <span style="color:#00f">#'</span><span style="color:#19177c">my/set-journal-header</span>)
+</span></span></code></pre></div><h3 id="encryption">Encryption</h3>
+<p>There are two ways how org-journal can be encrypted:</p>
+<ul>
+<li>With <a href="https://orgmode.org/manual/Org-Crypt.html">org-crypt</a>, by setting <code>org-journal-enable-encryption</code>.</li>
+<li>With <a href="https://www.gnu.org/software/emacs/manual/html_node/epa/Encrypting_002fdecrypting-gpg-files.html">epa</a>, by setting <code>org-journal-encrypt-journal</code>.</li>
+</ul>
+<p>Both ways are supported by this package (I use the first). The decryption of entries takes some time, but this is alleviated by caching.</p>
+<p>The cache is stored in the <code>org-journal-tags--files-cache</code> variable, so in principle, someone could come to your computer and inspect the value of this variable (who would ever do that?). If that’s an issue, you can do something like:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">run-with-idle-timer</span> (<span style="color:#00f">*</span> <span style="color:#666">60</span> <span style="color:#666">15</span>) <span style="color:#800">t</span> <span style="color:#00f">#'</span><span style="color:#19177c">org-journal-tags-cache-reset</span>)
+</span></span></code></pre></div><p>To clear the cache on Emacs being idle after 15 minutes.</p>
+<p>Also, as said above, <code>org-journal-tags</code> uses its own database, which is more like persistent cache for tags and references. You can encrypt it as well with <a href="https://www.gnu.org/software/emacs/manual/html_node/epa/Encrypting_002fdecrypting-gpg-files.html">epa</a> by adding <code>.gpg</code> to the <code>org-journal-tags-db-file</code> variable:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">org-journal-tags-db-file</span> (<span style="color:#00f">concat</span> <span style="color:#19177c">user-emacs-directory</span> <span style="color:#ba2121">"var/org-journal-tags/index.gpg"</span>))
+</span></span></code></pre></div><p>The database is also stored in memory in <code>org-journal-tags-db</code> variable, so once again, someone could inspect the value of the variable or just run <code>M-x org-journal-tags-status</code>.</p>
+<p>To avoid that, you can manually run <code>M-x org-journal-tags-db-unload</code> or add it to <code>run-with-idle-timer</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">run-with-idle-timer</span> (<span style="color:#00f">*</span> <span style="color:#666">60</span> <span style="color:#666">15</span>) <span style="color:#800">t</span> <span style="color:#00f">#'</span><span style="color:#19177c">org-journal-tags-db-unload</span>)
+</span></span></code></pre></div><p>If you have everything set up correctly, encrypting a file shouldn’t ask for a passphrase, so this function can be run automatically.</p>
+<h3 id="advanced-querying">Advanced querying</h3>
+<p>This package provides an API for doing queries from the Lisp code.</p>
+<p>The central function there <code>org-journal-tags-query</code>, which has an interface corresponding to the flags in the query constructor. Take a look at its docstring for more info.</p>
+<p>Also, you can use some of the following operations on the set of journal references:</p>
+<ul>
+<li><code>org-journal-tags--query-union-refs</code> - union</li>
+<li><code>org-journal-tags--query-diff-refs</code> - difference</li>
+<li><code>org-journal-tags--query-intersect-refs</code> - intersection</li>
+<li><code>org-journal-tags--query-merge-refs</code> - merge intersecting references within one set</li>
+<li><code>org-journal-tags--query-sort-refs</code> - order references by date</li>
+<li><code>org-journal-tags--string-extract-refs</code> - collect strings corresponding to references</li>
+</ul>
+<h2 id="final-notes">Final notes</h2>
+<p>This package turned out to be almost as long and complex as <a href="https://github.com/bastibe/org-journal">org-journal</a> itself, and it also introduces some new dependencies. Hence I decided it would be better off as a separate package.</p>
+<p>Also, I want to list some sources of inspiration. The database logic is heavily inspired by <a href="https://github.com/skeeto/elfeed">elfeed</a>. The UI with <a href="https://www.gnu.org/software/emacs/manual/html_mono/widget.html">Emacs widgets</a> for tags & <code>completing-read-multiple</code> and the tagging system in general is inspired by <a href="https://notmuchmail.org/">notmuch</a>. Finally, <a href="https://github.com/magit/transient">transient.el</a> and <a href="https://magit.vc/manual/magit-section.html">magit-section</a> are the UI packages that made this one possible, or at least much easier to implement.</p>
+
+
+
+
Using EXWM and perspective.el on multi-monitor setup
https://sqrtminusone.xyz/posts/2022-01-03-exwm/
@@ -2893,6 +4146,333 @@ I’ve seen a couple of cases where people would swap their username and
+
+ exwm-modeline
+ https://sqrtminusone.xyz/packages/exwm-modeline/
+ Wed, 22 Dec 2021 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/exwm-modeline/
+
+ <figure><a href="https://melpa.org/#/exwm-modeline"><img src="https://melpa.org/packages/exwm-modeline-badge.svg"/></a>
+</figure>
+
+<p>A modeline segment to display exwm workspaces.</p>
+<p>Here’s how it looks near the list of <a href="https://github.com/nex3/perspective-el">perspectives</a> (the segment of the current package is to the left):
+<img src="https://sqrtminusone.xyz/exwm-modeline-img/screenshot.png" alt=""></p>
+<ul>
+<li>workspaces 0 and 5 do not have any X windows</li>
+<li>workspace 1 is the current workspace</li>
+<li>workspace 2 has at least one X window.</li>
+</ul>
+<p>Features:</p>
+<ul>
+<li>Supports <code>exwm-randr</code> to display only workspaces related to the current monitor.</li>
+<li>Numbers are clickable.</li>
+</ul>
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you usually install packages, I use <a href="https://github.com/jwiegley/use-package">use-package</a> and <a href="https://github.com/raxod502/straight.el">straight.el</a>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">exwm-modeline</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:after</span> (<span style="color:#19177c">exwm</span>))
+</span></span></code></pre></div><p>Then put a call to <code>exwm-modeline-mode</code> somewhere after the moment when EXWM has been initialized, for instance:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'exwm-init-hook</span> <span style="color:#00f">#'</span><span style="color:#19177c">exwm-modeline-mode</span>)
+</span></span></code></pre></div><h2 id="customization">Customization</h2>
+<p>Set <code>exwm-modeline-randr</code> to nil to turn off filtering of workspaces by monitor.</p>
+<p>Set <code>exwm-modeline-short</code> to <code>t</code> display only the current workspace in the modeline.</p>
+<p>Set <code>exwm-modeline-display-urgent</code> to nil to turn off displaying whether a workspace has an urgent window. This will significantly decrease the number of modeline updates, which may help with performance issues.</p>
+<h2 id="credits">Credits</h2>
+<p><a href="https://github.com/nex3/perspective-el">perspective.el</a> by <a href="https://github.com/nex3">@nex3</a> was extremely instructive on how to make a modeline segment individual to a particular frame and avoid recalculating it too often.</p>
+<p><a href="https://github.com/elken/doom-modeline-exwm">doom-modeline-exwm</a> by <a href="https://github.com/elken">@elken</a> also was a source of inspiration.</p>
+
+
+
+
+
+ perspective-exwm
+ https://sqrtminusone.xyz/packages/perspective-exwm/
+ Wed, 01 Dec 2021 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/perspective-exwm/
+
+ <figure><a href="https://melpa.org/#/perspective-exwm"><img src="https://melpa.org/packages/perspective-exwm-badge.svg"/></a>
+</figure>
+
+<p>A couple of tricks and fixes to make using <a href="https://github.com/ch11ng/exwm">EXWM</a> and <a href="https://github.com/nex3/perspective-el">perspective.el</a> a better experience.</p>
+<h2 id="installation">Installation</h2>
+<p>This package is available on MELPA. Install it however you usually install packages, I use <a href="https://github.com/jwiegley/use-package">use-package</a> and <a href="https://github.com/raxod502/straight.el">straight.el</a>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">perspective-exwm</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>)
+</span></span></code></pre></div><p>Or clone the repository, add the package to the <code>load-path</code> and load it with <code>require</code>.</p>
+<p>The package provides a minor mode, <code>perspective-exwm-mode</code>, which is meant to be loaded before <code>exwm-init</code>. For instance, if you use <code>use-package</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">exwm</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:config</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">...</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">perspective-exwm-mode</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">exwm-init</span>))
+</span></span></code></pre></div><h2 id="usage-and-details">Usage and details</h2>
+<ul>
+<li>
+<p><code>perspective-exwm-mode</code><br />
+The mode does a couple of things:</p>
+<ul>
+<li>advises away a bug with half-killing the current perspective when closing a floating window. <del>I haven’t tested this as thoroughly</del> I haven’t run into this issue for nearly a month, so it seems to be fixed. But there’s <code>M-x perspective-exwm-revive-perspectives</code> if the problem arises anyway.</li>
+<li>fixes a bug with running <code>persp-set-buffer</code> on an EXWM buffer that was moved between workspaces by advising <code>persp-buffer-in-other-p</code>.</li>
+<li>fixes a bug with <code>persp-set-buffer</code> copying all the perspectives from other workspaces to the current one.</li>
+<li>adjusts the name of the initial perspective in the new workspace. It tries to get the name from the <code>perspective-exwm-override-initial-name</code> variable and fallbacks to <code>main-<index></code>.</li>
+</ul>
+<p>For the last point, I have the following in my configuration:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">perspective-exwm-override-initial-name</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">'</span>((<span style="color:#666">0</span> <span style="color:#666">.</span> <span style="color:#ba2121">"misc"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#666">1</span> <span style="color:#666">.</span> <span style="color:#ba2121">"core"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#666">2</span> <span style="color:#666">.</span> <span style="color:#ba2121">"browser"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#666">3</span> <span style="color:#666">.</span> <span style="color:#ba2121">"comms"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#666">4</span> <span style="color:#666">.</span> <span style="color:#ba2121">"dev"</span>)))
+</span></span></code></pre></div><p>Having distinct perspective names between frames also serves a purpose, because otherwise there are issues with multiple perspectives sharing the same scratch buffer.</p>
+</li>
+<li>
+<p><code>M-x perspective-exwm-cycle-exwm-buffers-forward</code>, <code>perspective-exwm-cycle-exwm-buffers-backward</code><br />
+Cycle EXWM buffers in the current perspective.</p>
+<figure><img src="https://sqrtminusone.xyz/perspective-exwm-img/cycle-buffers.png"/>
+ </figure>
+
+<p>The buffer highlighted in yellow is the current one, the buffer highlighted in blue is shown in another window of the perspective so it will be omitted from the cycle.</p>
+<p>Set <code>perspective-exwm-get-exwm-buffer-name</code> to customize the displayed name, by default it’s <code>exwm-class-name</code>.</p>
+</li>
+<li>
+<p><code>M-x perspective-exwm-cycle-all-buffers-forward</code>, <code>perspective-exwm-cycle-exwm-all-backward</code><br />
+The same as above, but not restricted to EXWM buffers.</p>
+</li>
+<li>
+<p><code>M-x perspective-exwm-switch-perspective</code><br />
+Select a perspective from the list of all perspectives on all workspaces.</p>
+<figure><img src="https://sqrtminusone.xyz/perspective-exwm-img/switch-perspective.png"/>
+ </figure>
+
+</li>
+<li>
+<p><code>M-x perspective-exwm-copy-to-workspace</code><br />
+Copy the current perspective to another EXWM workspace.</p>
+</li>
+<li>
+<p><code>M-x perspective-exwm-move-to-workspace</code><br />
+Move the current perspective to another EXWM workspace.</p>
+</li>
+<li>
+<p><code>perspective-exwm-assign-windows</code><br />
+A handy function to move the current window to a given workspace and/or perspective. Example usage:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-configure-window</span> ()
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">interactive</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">pcase</span> <span style="color:#19177c">exwm-class-name</span>
+</span></span><span style="display:flex;"><span> ((<span style="color:#008000">or</span> <span style="color:#ba2121">"Firefox"</span> <span style="color:#ba2121">"Nightly"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">perspective-exwm-assign-window</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:workspace-index</span> <span style="color:#666">2</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:persp-name</span> <span style="color:#ba2121">"browser"</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"Alacritty"</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">perspective-exwm-assign-window</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:persp-name</span> <span style="color:#ba2121">"term"</span>))
+</span></span><span style="display:flex;"><span> ((<span style="color:#008000">or</span> <span style="color:#ba2121">"VK"</span> <span style="color:#ba2121">"Slack"</span> <span style="color:#ba2121">"Discord"</span> <span style="color:#ba2121">"TelegramDesktop"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">perspective-exwm-assign-window</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:workspace-index</span> <span style="color:#666">3</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:persp-name</span> <span style="color:#ba2121">"comms"</span>))))
+</span></span><span style="display:flex;"><span>
+</span></span><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'exwm-manage-finish-hook</span> <span style="color:#00f">#'</span><span style="color:#19177c">my/exwm-configure-window</span>)
+</span></span></code></pre></div></li>
+</ul>
+<h2 id="known-issues">Known issues</h2>
+<ul>
+<li><code>perspective-exwm-move-to-workspace</code> kills X windows in the perspective it tries to move. Have no idea how to fix this at the moment.</li>
+</ul>
+
+
+
+
+
+ pomm.el
+ https://sqrtminusone.xyz/packages/pomm/
+ Fri, 05 Nov 2021 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/pomm/
+
+ <figure><a href="https://melpa.org/#/pomm"><img src="https://melpa.org/packages/pomm-badge.svg"/></a>
+</figure>
+
+<p>Implementation of <a href="https://en.wikipedia.org/wiki/Pomodoro_Technique">Pomodoro</a> and <a href="https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work">Third Time</a> techniques for Emacs.</p>
+<figure><img src="https://sqrtminusone.xyz/pomm-img/screenshot.png"/>
+</figure>
+
+<p>Features:</p>
+<ul>
+<li>Managing the timer with the excellent <a href="https://github.com/magit/transient/blob/master/lisp/transient.el">transient.el</a>.</li>
+<li>Persistent state between Emacs sessions.
+The timer state isn’t reset if you close Emacs. If necessary, the state file can be synchronized between machines.</li>
+<li>History.
+History of the timer can be stored in a CSV file. Eventually, I want to join this with <a href="https://activitywatch.net/">other activity data</a> to see if the state of the timer changes how I use the computer.</li>
+</ul>
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you usually install Emacs packages, e.g.</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>M-x package-install pomm
+</span></span></code></pre></div><p>My preferred way is <code>use-package</code> with <code>straight.el</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">pomm</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:commands</span> (<span style="color:#19177c">pomm</span> <span style="color:#19177c">pomm-third-time</span>))
+</span></span></code></pre></div><p>Or you can clone the repository, add the package to the <code>load-path</code> and load it with <code>require</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">require</span> <span style="color:#19177c">'pomm</span>)
+</span></span></code></pre></div><p>The package requires Emacs 27.1 because the time API of the previous versions is kinda crazy and 27.1 has <code>time-convert</code>.</p>
+<h2 id="usage">Usage</h2>
+<h3 id="pomodoro">Pomodoro</h3>
+<p>Run <code>M-x pomm</code> to open the transient buffer.</p>
+<p>The listed commands are rather self-descriptive and match the Pomodoro ideology.</p>
+<p>The timer can have 3 states:</p>
+<ul>
+<li><strong>Stopped</strong>. Can be started with “s” or <code>M-x pomm-start</code>. A new iteration of the timer will be started.</li>
+<li><strong>Paused</strong>. Can be continuted with “s” / <code>M-x pomm-start</code> or stopped competely with “S” / <code>M-x pomm-stop</code>.</li>
+<li><strong>Running</strong>. Can be paused with “p” / <code>M-x pomm-pause</code> or stopped with “S” / <code>M-x pomm-stop</code>.</li>
+</ul>
+<p>The state of the timer can be reset with “R” or <code>M-x pomm-reset</code>.</p>
+<p>“u” updates the transient buffer. The update is manual because I didn’t figure out how to automate this, and I think this is not <em>really</em> necessary.</p>
+<p>With “r” or <code>M-x pomm-set-context</code> you can set the current “context”, that is some description of the task you are currently working on. This description will show up in history and in the csv file. Also, <code>M-x pomm-start-with-context</code> will prompt for the context and then start the timer.</p>
+<h3 id="third-time">Third Time</h3>
+<p>Run <code>M-x pomm-third-time</code> to open the transient buffer for the Third Time technique.</p>
+<figure><img src="https://sqrtminusone.xyz/pomm-img/screenshot-tt.png"/>
+</figure>
+
+<p>Essentially, the techique is designed aroud the formula:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Time of break = 1/3 x Time of work.
+</span></span></code></pre></div><p>I.e. you work as long as you want or need, and then take a break with the maximum duration <code>1/3</code> of the time worked. If you take a shorter break, the remaining break time is saved and added to the next break within the same session. <a href="https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work">Here is a more detailed explanation</a>.</p>
+<p>The Third Time timer can have 2 states:</p>
+<ul>
+<li><strong>Stopped</strong>. Can be started with “s” or <code>M-x pomm-third-time-start</code>.</li>
+<li><strong>Running</strong>. Can be stopped with “S” or <code>M-x pomm-third-time-stop</code>. This resets the accumulated break time.</li>
+</ul>
+<p>Use “b” or <code>M-x pomm-third-time-switch</code> to switch the current period type (work or break). If the break time runs out, the timer automatically switches to work.</p>
+<h2 id="customization">Customization</h2>
+<p>Some settings are available in the transient buffer, but you can customize the relevant variables to make them permanent. Check <code>M-x customize-group</code> <code>pomm</code> and <code>M-x customize-group pomm-third-time</code> for more information.</p>
+<h3 id="alerts">Alerts</h3>
+<p>The package sends alerts via <code>alert.el</code>. The default style of alert is a plain <code>message</code>, but if you want an actual notification, set <code>alert-default-style</code> accordingly:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">alert-default-style</span> <span style="color:#19177c">'libnotify</span>)
+</span></span></code></pre></div><h3 id="sounds">Sounds</h3>
+<p>By default sounds are disabled. Set <code>pomm-audio-enabled</code> to <code>t</code> to toggle them. Set <code>pomm-audio-tick-enabled</code> to <code>t</code> if you want the ticking sound.</p>
+<p>This functionality needs <code>pomm-audio-player-executable</code> to be set so that the program could be invoked like: <code><executable> /path/to/sound.wav</code>.</p>
+<p>The package ships with some built-it sounds, which you can replace by customizing the <code>pomm-audio-files</code> variable.</p>
+<h3 id="modeline">Modeline</h3>
+<p>If you want the timer to display in the modeline, activate the <code>pomm-mode-line-mode</code> minor mode.</p>
+<h3 id="polybar-module">Polybar module</h3>
+<p>If you want to display the Pomodoro status in something like polybar, you can add the following lines to your config:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'pomm-on-tick-hook</span> <span style="color:#19177c">'pomm-update-mode-line-string</span>)
+</span></span><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'pomm-on-status-changed-hook</span> <span style="color:#19177c">'pomm-update-mode-line-string</span>)
+</span></span></code></pre></div><p>Create a script like this:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#008000;font-weight:bold">if</span> ps -e | grep emacs >> /dev/null; <span style="color:#008000;font-weight:bold">then</span>
+</span></span><span style="display:flex;"><span> emacsclient --eval <span style="color:#ba2121">"(if (boundp 'pomm-current-mode-line-string) pomm-current-mode-line-string \"\") "</span> | xargs <span style="color:#008000">echo</span> -e
+</span></span><span style="display:flex;"><span><span style="color:#008000;font-weight:bold">fi</span>
+</span></span></code></pre></div><p>And add a polybar module definition to your polybar config:</p>
+<pre tabindex="0"><code class="language-conf-windows" data-lang="conf-windows">[module/pomm]
+type = custom/script
+exec = /home/pavel/bin/polybar/pomm.sh
+interval = 1
+</code></pre><h3 id="state-file-location">State file location</h3>
+<p>To implement pesistence between Emacs sessions, the package stores its state in the following files:</p>
+<ul>
+<li><code>pomm-state-file-location</code>, <code>.emacs.d/pomm</code> by default</li>
+<li><code>pomm-third-time-state-file-location</code>, <code>/.emacs.d/pomm-third-time</code> by default</li>
+</ul>
+<p>Set these paths however like.</p>
+<h3 id="history">History</h3>
+<p>If you set the <code>pomm-csv-history-file</code> (and/or <code>pomm-third-time-csv-history-file</code>) variable, the package will log its history in CSV format. Just keep in mind that the parent directory has to exist.</p>
+<p>The file for the Pomodoro technique has the following columns:</p>
+<ul>
+<li><code>timestamp</code></li>
+<li><code>status</code> (<code>stopped</code>, <code>paused</code> or <code>running</code>, according to the <a href="#usage-1">usage</a> section)</li>
+<li><code>kind</code> (<code>work</code>, <code>short-break</code>, <code>long-break</code> or <code>nil</code>)</li>
+<li><code>iteration</code></li>
+<li><code>context</code></li>
+</ul>
+<p>One for the Third Time technique has an extra column called <code>break-time-remaining</code>.</p>
+<p>A new entry is written after a particular state of the timer comes into being.</p>
+<p>To customize timestamp, set the <code>pomm-csv-history-file-timestamp-format</code> variable. For example, for traditional <code>YYYY-MM-DD HH:mm:ss</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">pomm-csv-history-file-timestamp-format</span> <span style="color:#ba2121">"%F %T"</span>)
+</span></span></code></pre></div><p>The format is the same as in <code>format-time-string</code>.</p>
+<h2 id="alternatives">Alternatives</h2>
+<p>There is a number of packages with a similar purpose, here is a rough comparison of features:</p>
+<table>
+<thead>
+<tr>
+<th>Package</th>
+<th>3rd party integrations</th>
+<th>Control method (1)</th>
+<th>Persistent history</th>
+<th>Persistent state</th>
+<th>Notifications</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><a href="https://github.com/SqrtMinusOne/pomm.el">pomm.el</a></td>
+<td>-</td>
+<td>transient.el</td>
+<td>CSV</td>
+<td>+</td>
+<td>alert.el + sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/marcinkoziej/org-pomodoro/tree/master">org-pomodoro</a></td>
+<td>Org Mode!</td>
+<td>via Org commands</td>
+<td>via Org mode</td>
+<td>-</td>
+<td>alert.el + sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/TatriX/pomidor/">pomidor</a></td>
+<td>-</td>
+<td>self-cooked interactive buffer</td>
+<td>custom delimited format?</td>
+<td>+, but saving on-demand</td>
+<td>alert.el + sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/baudtack/pomodoro.el/">pomodoro.el</a></td>
+<td>-</td>
+<td>-</td>
+<td>-</td>
+<td>-</td>
+<td>notifications.el + sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/konr/tomatinho/">tomatinho</a></td>
+<td>-</td>
+<td>self-cooked interactive buffer</td>
+<td>-</td>
+<td>-</td>
+<td>message + sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/ferfebles/redtick">redtick</a></td>
+<td>-</td>
+<td>mode-line icon</td>
+<td>+</td>
+<td>-</td>
+<td>sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/abo-abo/gtk-pomodoro-indicator">gtk-pomodoro-indicator</a></td>
+<td>GTK panel</td>
+<td>CLI</td>
+<td>-</td>
+<td>-, but the program is independent from Emacs</td>
+<td>GTK notifications</td>
+</tr>
+</tbody>
+</table>
+<p>Be sure to check those out if this one doesn’t quite fit your workflow!</p>
+<p>(1) Means of timer control with exception of Emacs interactive commands</p>
+<p>Also take a look at <a href="https://github.com/telotortium/org-pomodoro-third-time">org-pomodoro-third-time</a>, which adapts <code>org-pomodoro</code> for the Third Time technique.</p>
+<h2 id="p-dot-s-dot">P.S.</h2>
+<p>The package name is not an abbreviation. I just hope it doesn’t mean something horrible in some language I don’t know.</p>
+<p>The sounds are made by Mike Koening under <a href="https://creativecommons.org/licenses/by/3.0/legalcode">CC BY 3.0</a>.</p>
+
+
+
+
Getting a consistent set of keybindings between i3 and Emacs
https://sqrtminusone.xyz/posts/2021-10-04-emacs-i3/
@@ -3334,6 +4914,117 @@ I’ve seen a couple of cases where people would swap their username and
+
+ lyrics-fetcher.el
+ https://sqrtminusone.xyz/packages/lyrics-fetcher/
+ Sat, 14 Aug 2021 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/lyrics-fetcher/
+
+ <figure><a href="https://melpa.org/#/lyrics-fetcher"><img src="https://melpa.org/packages/lyrics-fetcher-badge.svg"/></a>
+</figure>
+
+<p>A package to fetch song lyrics and album covers. Integrates with EMMS.</p>
+<figure><img src="https://sqrtminusone.xyz/lyrics-fetcher-img/screenshot.png"/>
+</figure>
+
+<p>The available backends are <a href="https://genius.com">genius.com</a> and <a href="https://music.163.com/">music.163.com</a>.</p>
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you normally install packages, I prefer <code>use-package</code> with <code>straight</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">lyrics-fetcher</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:after</span> (<span style="color:#19177c">emms</span>))
+</span></span></code></pre></div><p>Install <a href="https://imagemagick.org/index.php">imagemagick</a> if you want to download covers.</p>
+<p>If you want to use the genius backend, you have to set <a href="https://docs.genius.com/">genius.com</a> client access token. To do that, <a href="https://genius.com/api-clients/new">create a new client,</a> click “Generate Access Token” and put the result to the <code>lyrics-fetcher-genius-access-token</code> variable. I do this with password-store:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">lyrics-fetcher-genius-access-token</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">password-store-get</span> <span style="color:#ba2121">"My_Online/APIs/genius.com"</span>))
+</span></span></code></pre></div><p>But of course, you can just hardcode the string.</p>
+<h2 id="usage">Usage</h2>
+<p>Available commands:</p>
+<ul>
+<li>
+<p><code>M-x lyrics-fetcher-show-lyrics</code> - show lyrics for the current playing track.</p>
+<p>The resulting lyric files are saved to the <code>lyrics-fetcher-lyrics-folder</code> and have the <code>lyrics-fetcher-lyrics-file-extension</code> extension. The folder will be created if it doesn’t exist.</p>
+<p>By default, the function opens an already saved lyrics file if one exists, otherwise tries to fetch the lyrics.</p>
+<p>If called with <code>C-u</code>, then tries to fetch the text regardless of the latter.</p>
+<p>If called with <code>C-u C-u</code>, prompts the user to select a matching song. That is helpful when there are multiple songs with similar names, and the top one isn’t the right one.</p>
+<p>If called with <code>C-u C-u C-u</code>, edit the search query in minibuffer before sending. This is helpful when there is extra information in the song title which prevents the API from finding the song.</p>
+</li>
+<li>
+<p><code>M-x lyrics-fetcher-show-lyrics-query</code> - fetch lyrics by a text query.</p>
+<p>Modified by <code>C-u</code> the same way as <code>lyrics-fetcher-show-lyrics</code>.</p>
+</li>
+<li>
+<p><code>M-x lyrics-fetcher-use-backend</code> - select a backend to use.</p>
+</li>
+</ul>
+<p>EMMS integration:</p>
+<ul>
+<li>
+<p><code>M-x lyrics-fetcher-emms-browser-show-at-point</code> - fetch data for the current point in EMMS browser.</p>
+<p>If the point contains just one song, it will be fetched the usual way and lyrics will be shown upon successful completion.</p>
+<p>If the point contains many songs (e.g. it’s an album), the lyrics will be fetched consequentially for every song. The process then will stop at the first failure.</p>
+<p>Modified by <code>C-u</code> the same way as <code>lyrics-fetcher-show-lyrics</code>.</p>
+</li>
+<li>
+<p><code>M-x lyrics-fetcher-emms-browser-fetch-covers-at-point</code> - fetch album covers for the current point in the EMMS browser.</p>
+<p>This functionality requires songs’ directories to be grouped by albums, i.e. one album per one folder.</p>
+<p>The files will be saved to the folder with names like “cover_small.jpg”, “cover_med.jpg”, “cover_large.jpg”.</p>
+<p>You can customize the sizes via the <code>lyrics-fetcher-small-cover-size</code> and <code>lyrics-fetcher-medium-cover-size</code> variables.</p>
+<p>Modified by <code>C-u</code> the same way as <code>lyrics-fetcher-show-lyrics</code>.</p>
+</li>
+<li>
+<p><code>M-x lyrics-fetcher-emms-browser-open-large-cover-at-point</code> - open large_cover for the current point in EMMS browser.</p>
+</li>
+<li>
+<p><code>M-x lyrics-fetcher-lyrics-catchup</code> - feed the LRC file for the current track to EMMS.</p>
+</li>
+</ul>
+<p>Lyric view mode keybindings:</p>
+<ul>
+<li><code>q</code> - close the lyrics buffer</li>
+<li><code>r</code> - refetch the lyrics in the buffer</li>
+</ul>
+<h2 id="available-backends">Available backends</h2>
+<p>As of now, the available backends are <code>genius</code> and <code>neteasecloud</code> (thanks <a href="https://github.com/Elilif">@Elilif</a>). Backends can be switched with <code>M-x lyrics-fetcher-use-backend</code>, or from the Lisp code:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">lyrics-fetcher-use-backend</span> <span style="color:#19177c">'neteasecloud</span>)
+</span></span></code></pre></div><p>The <code>genius</code> backend fetches lyrics in a simple text format.</p>
+<p><code>neteasecloud</code> fetches in the <a href="https://en.wikipedia.org/wiki/LRC_(file_format)">LRC</a> format, which contains timestamps for each line of the lyrics text.</p>
+<p>LRC files can also be read by <code>emms-lyrics</code>. <code>lyrics-fetcher-use-backend</code> sets up <code>lyrics-fetcher</code> and EMMS variables so that EMMS could see the lyrics, downloaded by <code>lyrics-fetcher</code>. Running <code>M-x emms-lyrics</code> then should enable lyric display for newly played tracks, or you can run <code>M-x lyrics-fetcher-lyrics-catchup</code> to manually feed the current LRC file to EMMS.</p>
+<h2 id="customization-and-extension">Customization and extension</h2>
+<h3 id="lyrics-file-naming-and-location">Lyrics file naming and location</h3>
+<p>As was outlined above, lyrics files are saved to <code>lyrics-fetcher-lyrics-folder</code> and have an extension set in <code>lyrics-fetcher-lyrics-file-extension</code>.</p>
+<p>Take a look at the <code>lyrics-fetcher-format-song-name-method</code> and <code>lyrics-fetcher-format-file-name-method</code> variables if you want to customize the lyrics buffer and file naming.</p>
+<p>Also note that integration with <code>emms-lyrics</code> requires these variables to be set with <code>lyrics-fetcher-use-backend</code></p>
+<h3 id="using-other-player-than-emms">Using other player than EMMS</h3>
+<p>To use another player, customize <code>lyrics-fetcher-current-track-method</code>.</p>
+<p>This variable contains a function that returns the current playing track. The return format has to be either a string or (recommended) an EMMS-like alist, which has to have the following fields:</p>
+<ul>
+<li><code>info-artist</code> or <code>info-albumartist</code></li>
+<li><code>info-title</code></li>
+</ul>
+<h3 id="adding-another-backend">Adding another backend</h3>
+<p>A function to perform the lyric fetching is set in <code>lyrics-fetcher-fetch-method</code>.</p>
+<p>The function has to receive 3 arguments:</p>
+<ul>
+<li><code>track</code> - a string or alist, as outlined <a href="#using-other-player-than-emms-1">above</a>.</li>
+<li><code>callback</code> - the function which has to be called with the resulting lyrics string</li>
+<li><code>sync</code> - if non-nil, inquire the user about the possible choices. This is called <code>sync</code> because then it is reasonable to perform the request synchronously, as otherwise, it won’t be nice to suddenly throw a prompt at the user.</li>
+</ul>
+<p>The album cover fetching is similar. The corresponding function is set in <code>lyrics-fetcher-download-cover-method</code> and has to receive the following parameters:</p>
+<ul>
+<li><code>track</code> - as above</li>
+<li><code>callback</code> - has to be called with the path to the resulting file. This file should be named <code>cover_large.<extension></code>.</li>
+<li><code>folder</code> - where the file has to be put</li>
+<li><code>sync</code> - as above.</li>
+</ul>
+<p>The first argument is <code>track</code> because in EMMS all the required information is stored in tracks, and album data is deduced from tracks. So this package just takes a sample track in the album.</p>
+<h2 id="troubleshooting">Troubleshooting</h2>
+<p>I’ve noticed that Genius can give pages with different DOMs to different people. If you have an empty buffer instead of lyrics, please attach the <code>curl-cookie-jar</code> file to the issue. It usually resides in <code>.emacs.d/request</code>.</p>
+
+
+
+
Replacing Jupyter Notebook with Org Mode
https://sqrtminusone.xyz/posts/2021-05-01-org-python/
diff --git a/lyrics-fetcher-img/screenshot.png b/lyrics-fetcher-img/screenshot.png
new file mode 100644
index 0000000..af97c7f
Binary files /dev/null and b/lyrics-fetcher-img/screenshot.png differ
diff --git a/org-clock-agg-img/screenshot.png b/org-clock-agg-img/screenshot.png
new file mode 100644
index 0000000..f10cce6
Binary files /dev/null and b/org-clock-agg-img/screenshot.png differ
diff --git a/org-journal-tags-img/query-results.png b/org-journal-tags-img/query-results.png
new file mode 100644
index 0000000..86b6047
Binary files /dev/null and b/org-journal-tags-img/query-results.png differ
diff --git a/org-journal-tags-img/query.png b/org-journal-tags-img/query.png
new file mode 100644
index 0000000..46aec40
Binary files /dev/null and b/org-journal-tags-img/query.png differ
diff --git a/org-journal-tags-img/status.png b/org-journal-tags-img/status.png
new file mode 100644
index 0000000..364d589
Binary files /dev/null and b/org-journal-tags-img/status.png differ
diff --git a/ox-hugo/works-on-my-machine.svg b/ox-hugo/works-on-my-machine.svg
new file mode 100644
index 0000000..81b404c
--- /dev/null
+++ b/ox-hugo/works-on-my-machine.svg
@@ -0,0 +1,48 @@
+
+
+
diff --git a/packages/avy-dired/index.html b/packages/avy-dired/index.html
new file mode 100644
index 0000000..8711451
--- /dev/null
+++ b/packages/avy-dired/index.html
@@ -0,0 +1,127 @@
+
+
+
+
+
+ avy-dired
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ avy-dired
+
+
+
+
+
+
+ avy-dired
+
+
+
+
+
Doing some experimentation with avy & dired. Still somewhat flaky.
+
+
+
+
The only available command is M-x avy-dired-goto-line. Use K and J to scroll up and down while in the avy state, C-g or q to quit.
+ BIOME - Bountiful Interface to Open Meteo for Emacs
+
+
+
+
+
+
+ BIOME - Bountiful Interface to Open Meteo for Emacs
+
+
+
+
+
+
+
+
Interface to Open Meteo for Emacs. The service provides weather forecasts, historical weather data, climate change projections, and more.
+
The service is AGPL-licensed; the hosted API is free for non-commercial use if you make less than 10000 requests per day.
+
+
+
+
Installation
+
The package is available on MELPA. Install it however you normally install packages, I prefer use-package and straight.el:
+
(use-packagebiome
+:straightt)
+
Or clone the repository, add it to load-path, and require the package.
+
Usage
+
The main entry point is M-x biome. Each item under “Open Meteo Data” corresponds to a particular endpoint of the service. For instance, M-x biome ww is a generic weather forecast. Check out the API docs for more detailed descriptions.
+
+
+
+
Each of these items opens a query interface. A query consists of “global” variables, such as location, units, etc., and “group variables”. Groups are usually “hourly” and “daily”.
+
+
+
+
Global variables must always include a location (section “Select Coordinates or City”). To enter a location, you can either enter latitude and longitude (Open Meteo has an API for those as well) or select a location from biome-query-coords. Example configuration:
A timezone is also often required (“Settings” > “Timezone”).
+
The current group is switched with <tab>. Each group’s section has a set of variables that can be toggled on and off, such as temperature, precipitation, etc. Check out the API docs if you’re interested in the meaning of more esoteric ones.
+
Press RET after you’ve configured the query to call the API. If something goes wrong, it will output an error, such as:
+
Open Meteo has returned an error.
+Error: (error http 400)
+Reason: Timezone is required
+
Or it will open the results table (the first screenshot).
+
tabulated-list doesn’t support horizontal scrolling, so press c to toggle columns’ visibility.
+
+
+
+
More configuration
+
To save a query for later, press P in the root of the query interface. This will generate a definition like this:
Add this somewhere in your config after the package is loaded, e.g., in the :config section of the use-package form or wrapped in with-eval-after-load. Running M-x biome-query-preset-177 will create a query interface with this preset.
+
Table formatting can be configured with biome-grid-format; check the docstring for more information. For instance, if you want to disable all gradients:
The package also allows executing multiple queries at once to join their results. This can be useful for comparing weather in different locations or for viewing different reports about the same location.
+
Run M-x biome-multi to invoke the-multi query dialog.
+
+
+
+
(yes, I’ve switched to a light theme since the time of the previous screenshot)
+
Pressing a invokes the standard query dialog, where pressing RET returns to the root dialog, adding the query to the list. Pressing RET in the root dialog executes the queries in the list.
+
Queries are executed concurrently. The results are shown if all queries have been successfully completed.
+
P generates a preset defintion for the current query:
Just note that the macro is called biome-def-multi-preset.
+
Implementation notes
+
This isn’t the most complicated thing I’ve done, but it’s probably the most over-engineered one.
+
As you may have guessed, the interfaces mirror the API docs. I’ve implemented parsing of these HTMLs in biome-api-parse--generate, which generates the value of biome-api-data. Initially, it downloaded the HTML pages by itself, but - imagine that - the website was migrated to Svelte after I implemented maybe 80% of the parsing logic, and the Svelte version populates the accordions via JavaScript. So, as of now, the function requires opening the website in the browser, manually toggling all the accordions, and copying the HTML from DevTools. Fortunately, the parsing is a one-off operation.
+
Then, the interface… I like transient.el, so I wanted to make the interface generated dynamically from biome-api-data, which turned out harder than I expected. I probably should’ve just used widget.el.
+
Generating sensible keys was a challenge. I’ve made an algorithm in biome-query--unique-keys that sort of works well.
+
And as for populating transient prefixes, I tried to use :setup-children in a few places, but it’s not general enough, namely, it doesn’t seem to support specifying :class for child groups… So I ended up overriding transient--layout in the prefix setup. This doesn’t seem to have any undesirable side effects.
+
Also, the only way I found to use custom infix classes in these dynamic definitions was to eval transient-define-infix for each required place. Unfortunately, that adds a lot of stuff to the interactive functions namespace.
+
Getting to the results display, Lars Ingebrigtsen’s vtable comes only in Emacs 29, so I used tabulated-list. The only disadvantage of the latter is the lack of horizontal scroll support, which can be worked around by hiding columns with biome-grid-columns.
+
Most variables are formatted with a gradient, colors for which were mostly inspired by Windy. Formatting for things like air quality variables is probably all over the place, so take the red color with a grain of salt.
The package provides a tree-based feed summary interface for elfeed. The tree can include individual feeds, searches, and groups. It mainly serves as an easier “jumping point” for elfeed, so to make querying a subset of the elfeed database one action away.
Running M-x elfeed-summary opens up the summary buffer, as shown on the screenshot.
+
The tree consists of:
+
+
feeds;
+
searches;
+
groups, that can include other groups, feeds, and searches.
+
+
Groups can also be generated automatically.
+
Available keybindings in the summary mode:
+
+
+
+
Keybinding
+
Command
+
Description
+
+
+
+
+
RET
+
elfeed-summary--action
+
Open thing under the cursor (a feed, search, or a group). If there is at least one unread item, it will show only unread items.
+
+
+
M-RET
+
elfeed-summary--action-show-read
+
Open thing under the cursor, but always include read items
+
+
+
q
+
…
+
Quit the summary buffer
+
+
+
r
+
elfeed-summary--refresh
+
Refresh the summary buffer
+
+
+
R
+
elfeed-summary-update
+
Run update for elfeed feeds
+
+
+
u
+
elfeed-summary-toggle-only-unread
+
Toggle showing only unread entries
+
+
+
U
+
elfeed-summary--action-mark-read
+
Mark everything in the entry under the cursor as read
+
+
+
+
The standard keybindings from magit-section are also available, for instance TAB toggles the visibility of the current group. evil-mode is also supported.
+
Configuration
+
Tree configuration
+
The structure of the tree is determined by the elfeed-summary-settings variable.
+
This is a list of these possible items:
+
+
Group (group . <group-params>)
+Groups are used to group elements under collapsible sections.
+
Query (query . <query-params>)
+Query extracts a subset of elfeed feeds based on the given criteria. Each found feed will be represented as a line.
+
Search (search . <search-params>)
+Elfeed search, as defined by elfeed-search-set-filter.
+
Tags tree (auto-tags . <auto-tags-params>)
+A tree generated automatically from the available tags.
+
Tag groups (tag-groups . <tag-group-params>)
+Insert one tag as one group.
+
a few special forms
+
+
<group-params> is an alist with the following keys:
+
+
:title (mandatory)
+
:elements (mandatory) - elements of the group. The structure is the same as in the root definition.
+
:face - group face. The default face is elfeed-summary-group-face.
+
:hide - if non-nil, the group is collapsed by default.
+
+
<query-params> can be:
+
+
A symbol of a tag.
+A feed will be matched if it has that tag.
+
:all. Will match anything.
+
(title . "string") or (title . <form>)
+Match feed title with string-match-p. <form> makes sense if you
+want to pass something like rx.
+
(author . "string") or (author . <form>)
+
(url . "string") or (url . <form>)
+
(and <q-1> <q-2> ... <q-n>)
+Match if all the conditions 1, 2, …, n match.
+
(or <q-1> <q-2> ... <q-n>) or (<q-1> <q-2> ... <q-n>)
+Match if any of the conditions 1, 2, …, n match.
+
(not <query>)
+
+
Feed tags for the query are determined by the elfeed-feeds variable.
+
Query examples:
+
+
(emacs lisp)
+Return all feeds that have either “emacs” or “lisp” tags.
+
(and emacs lisp)
+Return all feeds that have both “emacs” and “lisp” tags.
+
(and (title . "Emacs") (not planets))
+Return all feeds that have “Emacs” in their title and don’t have
+the “planets” tag.
+
+
<search-params> is an alist with the following keys:
+
+
:filter (mandatory) filter string, as defined by
+elfeed-search-set-filter
+
:title (mandatory) title.
+
:tags - list of tags to get the face of the entry.
+
+
<auto-tags-params> is an alist with the following keys:
+
+
:max-level - maximum level of the tree (default 2)
+
:source - which feeds to use to build the tree.
+Can be :misc (default) or (query . <query-params>).
+
:original-order - do not try to build a more concise tree by
+putting the most frequent tags closer to the root of the tree.
+
:faces - list of faces for groups.
+
+
<tag-group-params> is an alist with the following keys:
+
+
:source - which feeds to use to build the tree.
+Can be :misc (default) or (query . <query-params>).
+
:repeat-feeds - allow feeds to repeat. Otherwise, each feed is
+assigned to group with the least amount of members.
+
:face - face for groups.
+
+
Available special forms:
+
+
:misc - print out feeds, not found by any query above.
+
+
Also keep in mind that '(key . ((values))) is the same as '(key (values)). This helps to shorten the form in many cases.
+
Also, this variable is not validated by any means, so wrong values can produce somewhat cryptic errors. Sorry about that.
+
Example
+
Here is an excerpt from my configuration that was used to produce this screenshot:
As described in the tree configuration section, there are two ways to avoid defining all the relevant groups manually, auto-tags and tag-groups. Both use tags that are defined in elfeed-feeds.
+
auto-tags tries to build the most concise tree from these tags. E.g. if we have feeds:
The tree is truncated by :max-level, which is 2 by default.
+
If tags don’t form this kind of hierarchy in elfeed-feeds, the algorithm will still try to build the most “optimal” tree, where the most frequent tags are on the top.
+
To avoid that you can set (:original-order . t), in which case each feed will be placed at the path tag1 tag2 ... tagN feed, where the order of tags is the same as in elfeed-feeds. By the way, this allows reproducing the hierarchy of elfeed-org, e.g. this structure:
Whereas without :original-order the structure will be:
+
+
tag1
+
+
feed1
+
+
+
tag2
+
+
tag1
+
+
feed2
+
feed3
+
+
+
tag3
+
+
feed4
+
feed5
+
feed6
+
+
+
+
+
+
tag-groups
+
The second option is tag-groups, which creates a group for each tag.
+
By default, each feed is assigned to its less frequent tag. This can be turned off by setting (:repeat-feeds . t).
+
E.g., the elfeed-org setup from the section above will be converted to this structure:
+
+
tag1
+
+
feed1
+
feed2
+
feed3
+
+
+
tag3
+
+
feed4
+
feed5
+
feed6
+
+
+
+
And with :repeat-feeds:
+
+
tag1
+
+
feed1
+
feed2
+
feed3
+
+
+
tag2
+
+
feed2
+
feed3
+
feed4
+
feed5
+
feed6
+
+
+
tag3
+
+
feed4
+
feed5
+
feed6
+
+
+
+
Common options
+
Both auto-tags and tag-groups allow setting the :search parameter.
+
The default value is (:search . :misc), i.e. use feeds that weren’t found by other queries.
+
Passing (:search . (query . <query-params>)) is another option.
+
Faces
+
Group faces by default use the elfeed-summary-group-faces variable, which serves as a list of faces for each level of the tree. Individual group faces can be overridden with the :face attribute.
+
Feed faces by default reuse the existing elfeed mechanism. The tags for feeds are taken from the elfeed-feeds variable; if a feed has at least one unread entry, the unread tag is added to the list. This can be overridden by setting the elfeed-summary-feed-face-fn variable.
+
Searches are mostly the same as feeds, but tags for the search are taken from the :tags attribute. This also can be overridden with elfeed-summary-search-face-fn variable.
+
Opening elfeed-search in other window
+
If you set:
+
(setqelfeed-summary-other-windowt)
+
Then RET and M-RET in the elfeed-summary buffer will open the search buffer in other window.
+
elfeed-summary-width regulates the width of the remaining summary window in this case. It is useful because the data in the search buffer is generally wider than in the summary buffer. The variable can also be set to nil to disable this behavior.
+
Skipping feeds
+
tt-rss has a feature to disable updating a particular feed but keep it in the feed list. I also want that for elfeed.
+
To use that, set elfeed-summary-skip-sync-tag to some value:
+
(setqelfeed-summary-skip-sync-tag'skip)
+
And tag the feeds you want to skip with this tag. Then, running M-x elfeed-summary-update will skip them. This won’t affect M-x elfeed-update unless you:
Also watch out if you use elfeed-org and want to use the ignore tag, because this package omits feeds with this tag altogether (configurable by rmh-elfeed-org-ignore-tag).
+
Other options
+
Also take a look at M-x customize-group elfeed-summary for the rest of available options.
+
Ideas and alternatives
+
The default interface of elfeed is just a list of all entries. Naturally, it gets hard to navigate when there are a lot of sources with varying frequencies of posts.
+
Elfeed itself provides one solution, which is using bookmarks to save individual searches. This can work, but it can be somewhat cumbersome.
+
elfeed-score is another solution, which introduces scoring rules for entries. Thus, with proper rules set, the most important entries should be on the top of the list. You can take a look at this video by John Kitchin to see how this can work.
+
However, I mostly had elfeed-score to group entries to sets with equal scores, and I then processed one such set or the other. This is why I decided this package is a better fit for my workflow.
+
Another idea I used often before that is this function:
+
(defunmy/elfeed-search-filter-source (entry)
+"Filter elfeed search buffer by the feed under the cursor."
+ (interactive (list (elfeed-search-selected:ignore-region)))
+ (when (elfeed-entry-pentry)
+ (elfeed-search-set-filter
+ (concat
+"@6-months-ago "
+"+unread "
+"="
+ (replace-regexp-in-string
+ (rx"?" (*not-newline) eos)
+""
+ (elfeed-feed-url (elfeed-entry-feedentry)))))))
+
I’ve bound it to o, so I would open elfeed, press o, and only see unread entries from a particular feed. Then I cleaned the filter and switched to the next feed. Once again, a tree with feeds is obviously a better tool for such a workflow.
+
The last solution I want to mention is elfeed-dashboard, although I didn’t test this one. It looks similar to this package but seems to require much more fine-tuning, for instance, it doesn’t allow to list all the feeds with a certain tag in a group.
Allow larger request body sizes in nginx. Add the following to the server directive:
+
client_max_body_size 10M;
+
For me, the sync payload is around 3M.
+
+
+
Increase the read timeout in nginx. Add the following to the php location directive:
+
fastcgi_read_timeout 600;
+
Syncing the entries is usually pretty fast, but the first feed sync takes a while.
+
+
+
Then restart tt-rss. Check if the plugin appears in the Preferences > Plugins section.
+
+
+
Enable “Allows accessing this account through the API” in the Preferences > Preferences. You also may want to disable “Purge unread articles”, because elfeed doesn’t do that.
+
+
+
Install the Emacs package however you normally install packages, I prefer use-package and straight.el. Make sure to enable elfeed-sync-mode.
elfeed-sync-tt-rss-instance - point that to your tt-rss instance, e.g.
+
https://example.com/tt-rss
+
(No trailing slash)
+
elfeed-sync-tt-rss-login
+
elfeed-sync-tt-rss-password
+
elfeed-sync-unread-tag - elfeed tag to map to read status in tt-rss. unread by default.
+
elfeed-sync-marked-tag - elfeed tag to map to marked status in tt-rss. later by default.
+
+
Usage
+
Syncing the feed list
+
The first thing you probably want to do is to sync the feed list.
+
It makes little sense to sync tt-rss feeds to elfeed, because people often use projects like elfeed-org. But it’s possible to sync elfeed feeds to tt-rss.
+
The function M-x elfeed-sync-feeds does exactly that. If you have elfeed-summary installed and tt-rss categories enabled, the function will recreate the elfeed-summary tree in tt-rss.
+
The first run of the function takes a while because tt-rss has to fetch the feed at the moment of the first subscription. Which is why increasing the timeout may be necessary.
+
However, running the function multiple times until it succeeds should also work.
+
Syncing the entry list
+
To sync the entry list, run M-x elfeed-sync. The sync usually takes a couple of seconds.
+
The sync finishes at the “Sync complete!” message. Check the *elfeed-log* buffer for statistics.
+
Occasionally, some entries do not match. Here are the possible cases:
+
+
Entry exists in the elfeed database, but not in tt-rss.
+Run M-x elfeed-sync-search-missing to display such entries.
+
Entry exists in the tt-rss database, but not in elfeed:
+
+
Entry appeared in the feed after the last elfeed-update.
+Run M-x elfeed-update and then M-x elfeed-sync.
+
Entry appeared and disappeared in the feed after the last elfeed-update.
+Such an entry will never get to the elfeed database. If you want to, run M-x elfeed-sync and then M-x elfeed-sync-read-ttrss-missing to mark all such entries as read.
+
+
+
Entry appeared in the feed before elfeed-sync-look-back.
+Such an entry will never be matched. This is an inconvenience if you have just set up tt-rss, it fetched old entries from the feeds and such entries remain permanently unread because they are untouched by the M-x elfeed-sync.
+To mark such entries as read, run M-x elfeed-sync-read-ttrss-old.
+
+
Implementation details
+
The heavy-lifting is done on the elisp side because I ran into strange performance issues with associative arrays in PHP.
+
Check the elfeed-sync--do-sync function for the description of the synchronization algorithm. The tl;dr is to download all entries from tt-rss and match each entry against the elfeed database. In the case of discrepancy update whichever entry has the lower priority.
Then put a call to exwm-modeline-mode somewhere after the moment when EXWM has been initialized, for instance:
+
(add-hook'exwm-init-hook#'exwm-modeline-mode)
+
Customization
+
Set exwm-modeline-randr to nil to turn off filtering of workspaces by monitor.
+
Set exwm-modeline-short to t display only the current workspace in the modeline.
+
Set exwm-modeline-display-urgent to nil to turn off displaying whether a workspace has an urgent window. This will significantly decrease the number of modeline updates, which may help with performance issues.
+
Credits
+
perspective.el by @nex3 was extremely instructive on how to make a modeline segment individual to a particular frame and avoid recalculating it too often.
+
+
diff --git a/packages/index.xml b/packages/index.xml
new file mode 100644
index 0000000..69b3e28
--- /dev/null
+++ b/packages/index.xml
@@ -0,0 +1,1702 @@
+
+
+
+ Packages on SqrtMinusOne
+ https://sqrtminusone.xyz/packages/
+ Recent content in Packages on SqrtMinusOne
+ Hugo -- gohugo.io
+ en-us
+ Sun, 17 Dec 2023 00:00:00 +0000
+
+ org-clock-agg
+ https://sqrtminusone.xyz/packages/org-clock-agg/
+ Sun, 17 Dec 2023 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/org-clock-agg/
+
+ <p>Aggregate <a href="https://orgmode.org/manual/Clocking-Work-Time.html">org-clock</a> records and display the results in an interactive buffer. The records are grouped by predicates such as file name, their outline path in the file, etc. Each record is placed in a tree structure; each node of the tree shows the total time spent in that node and its children. The top-level node shows the total time spent in all records found by the query.</p>
+<figure><img src="https://sqrtminusone.xyz/org-clock-agg-img/screenshot.png"/>
+</figure>
+
+<h2 id="installation">Installation</h2>
+<p>The package isn’t yet available anywhere but in this repository. My preferred way for such cases is <a href="https://github.com/jwiegley/use-package">use-package</a> and <a href="https://github.com/radian-software/straight.el">straight.el</a>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">org-clock-agg</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> (<span style="color:#008000">:host</span> <span style="color:#19177c">github</span> <span style="color:#008000">:repo</span> <span style="color:#ba2121">"SqrtMinusOne/org-clock-agg"</span>))
+</span></span></code></pre></div><p>Alternatively, clone the repository, add it to the <code>load-path</code>, and <code>require</code> the package.</p>
+<h2 id="usage">Usage</h2>
+<p>Run <code>M-x org-clock-agg</code> to open the interactive buffer (as depicted in the screenshot above).</p>
+<p>The interactive buffer provides the following controls:</p>
+<ul>
+<li><strong>Files</strong>: Specifies the org files from which to select (defaults to <a href="https://orgmode.org/manual/Agenda-Files.html">org-agenda</a>).</li>
+<li><strong>Date from</strong> and <strong>To</strong>: Define the date range.</li>
+<li><strong>Group by</strong>: Determines how <code>org-clock</code> records are grouped.</li>
+<li><strong>Show elements</strong>: Whether to display raw <code>org-clock</code> records in each node.</li>
+<li><strong>Add “Ungrouped”</strong>: Option to include the “Ungrouped” node. This is particularly useful with <a href="#custom-grouping-predicates">custom grouping predicates</a>.</li>
+</ul>
+<p>Press <code>[Refresh]</code> to update the buffer. The initial search might take some time, but subsequent searches are generally faster due to the caching mechanism employed by <a href="https://github.com/alphapapa/org-ql">org-ql</a>.</p>
+<p>The buffer uses <a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Outline-Mode.html">outline-mode</a> to display the tree, so each node becomes an <code>outline-mode</code> header. Refer to the linked manual for available commands/keybindings, or, if you use <code>evil-mode</code>, check <a href="https://github.com/emacs-evil/evil-collection/blob/master/modes/outline/evil-collection-outline.el">the relevant evil-collection file</a>.</p>
+<h3 id="files">Files</h3>
+<p>By default, the package selects <code>org-clock</code> records from <code>(org-agenda-files)</code>. Additional options can be included by customizing the <code>org-clock-agg-files-preset</code> variable. For instance:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">org-clock-agg-files-preset</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">`</span>((<span style="color:#ba2121">"Org Agenda + Archive"</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">.</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">,</span>(<span style="color:#00f">append</span> (<span style="color:#19177c">org-agenda-files</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">cl-remove-if</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">lambda</span> (<span style="color:#19177c">f</span>) (<span style="color:#19177c">string-match-p</span> (<span style="color:#008000">rx</span> <span style="color:#ba2121">"."</span> <span style="color:#19177c">eos</span>) <span style="color:#19177c">f</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#00f">directory-files</span> (<span style="color:#00f">concat</span> <span style="color:#19177c">org-directory</span> <span style="color:#ba2121">"/archive/"</span>) <span style="color:#800">t</span>))))))
+</span></span></code></pre></div><p>Note that after updating any of these variables, you’ll need to reopen the <code>*org-clock-agg*</code> buffer to view the changes.</p>
+<p>Alternatively, you can directly specify the list of files within the buffer by selecting “Custom list” in the “Files” control.</p>
+<h3 id="date-range">Date Range</h3>
+<p>Dates can take the following values:</p>
+<ul>
+<li>A number: Represents a relative number of days from the current date. E.g. the default value of <code>-7</code> to <code>0</code> menas the previous week up to today.</li>
+<li>A date string in the format <code>YYYY-MM-DD HH:mm:ss</code>, with or without the time part.</li>
+</ul>
+<p>By default, the interval is inclusive. For instance, specifying an interval like 2023-12-12 .. 2023-12-13 includes all records from 2023-12-12 00:00:00 to 2023-12-13 23:59:59.</p>
+<h3 id="group-by">Group By</h3>
+<p>Records are grouped based on the sequence of grouping predicates.</p>
+<p>For example, with the following content in <code>tasks.org</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>* Tasks
+</span></span><span style="display:flex;"><span>** DONE Thing 1
+</span></span><span style="display:flex;"><span>:LOGBOOK:
+</span></span><span style="display:flex;"><span>CLOCK: [2023-12-13 Wed 19:01]--[2023-12-13 Wed 19:29] => 0:28
+</span></span><span style="display:flex;"><span>CLOCK: [2023-12-13 Wed 19:30]--[2023-12-13 Wed 19:40] => 0:10
+</span></span><span style="display:flex;"><span>:END:
+</span></span></code></pre></div><p>And predicates “Org file”, “Day”, and “Outline path”, the records for “Thing 1” will be processed as follows:</p>
+<ul>
+<li>“Day” -> <code>2023-12-13</code></li>
+<li>“Org file” -> <code>tasks.org</code></li>
+<li>“Outline path” -> <code>Tasks</code>, <code>Thing 1</code></li>
+</ul>
+<p>Consequently, the node will be placed at the path <code>2023-12-13</code> / <code>tasks.org</code> / <code>Tasks</code> / <code>Thing 1</code> in the resulting tree:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>* Results Root 0:38
+</span></span><span style="display:flex;"><span>** 2023-12-13 Day 0:38
+</span></span><span style="display:flex;"><span>*** tasks.org Org File 0:38
+</span></span><span style="display:flex;"><span>**** Tasks Outline path 0:38
+</span></span><span style="display:flex;"><span>***** Thing 1 Outline path 0:38
+</span></span><span style="display:flex;"><span>- [2023-12-13 Wed 19:01]--[2023-12-13 Wed 19:29] => 0:28 : DONE Thing 1
+</span></span><span style="display:flex;"><span>- [2023-12-13 Wed 19:30]--[2023-12-13 Wed 19:40] => 0:10 : DONE Thing 1
+</span></span></code></pre></div><p>The following built-in predicates are currently available:</p>
+<table>
+<thead>
+<tr>
+<th>Name</th>
+<th>Comment</th>
+<th>Customization variables</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>Category</td>
+<td></td>
+<td></td>
+</tr>
+<tr>
+<td>Org file</td>
+<td></td>
+<td></td>
+</tr>
+<tr>
+<td>Outline path</td>
+<td></td>
+<td></td>
+</tr>
+<tr>
+<td>Tags</td>
+<td>Sorted alphabetically</td>
+<td></td>
+</tr>
+<tr>
+<td>Headline</td>
+<td>Last item of the outline path</td>
+<td></td>
+</tr>
+<tr>
+<td>Day</td>
+<td></td>
+<td><code>org-clock-agg-day-format</code></td>
+</tr>
+<tr>
+<td>Week</td>
+<td></td>
+<td><code>org-clock-agg-week-format</code></td>
+</tr>
+<tr>
+<td>Month</td>
+<td></td>
+<td><code>org-clock-agg-month-format</code></td>
+</tr>
+<tr>
+<td>TODO keyword</td>
+<td></td>
+<td></td>
+</tr>
+<tr>
+<td>Is done</td>
+<td></td>
+<td></td>
+</tr>
+<tr>
+<td>Selected props</td>
+<td></td>
+<td><code>org-clock-agg-properties</code></td>
+</tr>
+</tbody>
+</table>
+<p>Ensure to use <code>setopt</code> to set the variables; otherwise, the customization logic will not be invoked:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">setopt</span> <span style="color:#19177c">org-clock-agg-properties</span> <span style="color:#666">'</span>(<span style="color:#ba2121">"PROJECT_NAME"</span>))
+</span></span></code></pre></div><p>Refer also to <a href="#custom-grouping-predicates-1">custom grouping predicates</a>.</p>
+<h2 id="customization">Customization</h2>
+<h3 id="node-formatting">Node Formatting</h3>
+<p>The <code>org-clock-agg-node-format</code> variable determines the formatting of individual tree nodes. This uses a <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Custom-Format-Strings.html">format string</a> that with the following format specifiers avaiable:</p>
+<ul>
+<li><code>%t</code>: Node title with the level prefix, truncated to <code>title-width</code> characters (refer to below)</li>
+<li><code>%c</code>: Name of the grouping function that generated the node</li>
+<li><code>%z</code>: Time spent in the node, formatted according to <code>org-clock-agg-duration-format</code>.</li>
+<li><code>%s</code>: Time share of the node against the parent node</li>
+<li><code>%S</code>: Time share of the node against the top-level node</li>
+</ul>
+<p>The default value is:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>%-%(+ title-width)t %20c %8z
+</span></span></code></pre></div><p>Where <code>%(+ title-width)</code> is <code>(- (window-width) org-clock-agg-node-title-width-delta)</code>, with the default value of the latter set to <code>40</code>.</p>
+<p>Thefore, in the default configuration, the node title is truncated to <code>title-width</code> characters, while 40 symbols are allocated for the rest of the header, i.e. " %20c %8z" (30 symbols), along with additional space for folding symbols of <code>outline-minor-mode</code>, line numbers, etc.</p>
+<h3 id="record-formatting">Record Formatting</h3>
+<p>When the “Show records” flag is enabled, associated records for each node are displayed. The formatting of these is defined by <code>org-clock-agg-elem-format</code>, which is also a format string with the following specifiers:
+Customize the formatting of these records through <code>org-clock-agg-elem-format</code>, which also utilizes a format string comprising the following specifiers:</p>
+<ul>
+<li><code>%s</code>: Start of the time range</li>
+<li><code>%e</code>: End of the time range</li>
+<li><code>%d</code>: Duration of the time range</li>
+<li><code>%t</code>: Title of the record.</li>
+</ul>
+<p>The default value is:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>- [%s]--[%e] => %d : %t
+</span></span></code></pre></div><h3 id="custom-grouping-predicates-2">Custom grouping predicates</h3>
+<p>It’s possible to define custom grouping predicates in addition to the default ones. In fact, it’s probably the only way to get grouping that is tailored to your particular org workflow; I haven’t included my predicates in the package because they aren’t general enough.</p>
+<p>To create new predicates, use <code>org-clock-agg-defgroupby</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">org-clock-agg-defgroupby</span> <span style="color:#19177c"><name></span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:key1</span> <span style="color:#19177c">value1</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:key2</span> <span style="color:#19177c">value2</span>
+</span></span><span style="display:flex;"><span> <span style="color:#19177c"><body></span>)
+</span></span></code></pre></div><p>The available keyword arguments include:</p>
+<ul>
+<li><code>:readable-name</code>: Function name for the UI.</li>
+<li><code>:default-sort</code>: Default sorting function.</li>
+</ul>
+<p>The body binds two variables - <code>elem</code> and <code>extra-params</code>, and must return a list of strings.</p>
+<p>The <code>elem</code> variable is an alist that represents one org-clock record. The keys are as follows:</p>
+<ul>
+<li><code>:start</code>: Start time in seconds since the epoch</li>
+<li><code>:end</code>: End time in seconds since the epoch</li>
+<li><code>:duration</code>: Duration in seconds</li>
+<li><code>:headline</code>: Instance of <a href="https://orgmode.org/worg/dev/org-element-api.html">org-element</a> for the headline</li>
+<li><code>:tags</code>: List of tags</li>
+<li><code>:file</code>: File name</li>
+<li><code>:outline-path</code>: titles of all headlines from the root to the current headline</li>
+<li><code>:properties</code>: List of properties; <code>org-clock-agg-properties</code> sets the selection list</li>
+<li><code>:category</code>: <a href="https://orgmode.org/manual/Categories.html">Category</a> of the current headline.</li>
+</ul>
+<p>The <code>extra-params</code> variable is an alist of global parameters controlling the function’s behavior. Additional parameters can be added by customizing <code>org-clock-agg-extra-params</code>. This alist has keys as parameter names and values as <a href="https://www.gnu.org/software/emacs/manual/html_mono/widget.html">widget.el</a> expressions (applied to <code>widget-create</code>) controlling the UI. Each widget must contain an <code>:extras-key</code> key.</p>
+<p>For instance:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">org-clock-agg-extra-params</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">'</span>((<span style="color:#ba2121">"Events: Offline / Online"</span> <span style="color:#666">.</span> (<span style="color:#19177c">checkbox</span> <span style="color:#008000">:extras-key</span> <span style="color:#008000">:events-online</span>))))
+</span></span></code></pre></div><p>This adds a checkbox to the form that appears as:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Events: Offline / Online [ ]
+</span></span></code></pre></div><p>When checked, <code>extra-params</code> takes the value <code>((:extras-keys . t))</code>.</p>
+<p>Here’s an example predicate. I store meetings the following way:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>* Some project
+</span></span><span style="display:flex;"><span>** Meetings
+</span></span><span style="display:flex;"><span>*** Some meeting 1
+</span></span><span style="display:flex;"><span>*** Some meeting 2
+</span></span><span style="display:flex;"><span>* Another project
+</span></span><span style="display:flex;"><span>** Meetings
+</span></span><span style="display:flex;"><span>*** Another meeting 1
+</span></span><span style="display:flex;"><span>*** Another meeting 2 (offline)
+</span></span></code></pre></div><p>I want to group these meetings by title, i.e. group all instances of “Some meeting”, “Another meeting”, etc. Optionally I want to group online and offline meetings.</p>
+<p>This can be done the following way:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">org-clock-agg-defgroupby</span> <span style="color:#19177c">event</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:readable-name</span> <span style="color:#ba2121">"Event"</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:default-sort</span> <span style="color:#19177c">total</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">let*</span> ((<span style="color:#19177c">title</span> (<span style="color:#19177c">org-element-property</span> <span style="color:#008000">:raw-value</span> (<span style="color:#19177c">alist-get</span> <span style="color:#008000">:headline</span> <span style="color:#19177c">elem</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">is-meeting</span> (<span style="color:#008000">or</span> (<span style="color:#19177c">string-match-p</span> <span style="color:#ba2121">"meeting"</span> (<span style="color:#00f">downcase</span> <span style="color:#19177c">title</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">seq-contains-p</span> (<span style="color:#19177c">alist-get</span> <span style="color:#008000">:tags</span> <span style="color:#19177c">elem</span>) <span style="color:#ba2121">"mt"</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">is-offline</span> (<span style="color:#008000">or</span> (<span style="color:#19177c">string-match-p</span> <span style="color:#ba2121">"offline"</span> (<span style="color:#00f">downcase</span> <span style="color:#19177c">title</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">seq-contains-p</span> (<span style="color:#19177c">alist-get</span> <span style="color:#008000">:tags</span> <span style="color:#19177c">elem</span>) <span style="color:#ba2121">"offline"</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">title-without-stuff</span> (<span style="color:#19177c">string-trim</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">replace-regexp-in-string</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">rx</span> (<span style="color:#008000">or</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#00f">+</span> (<span style="color:#008000">or</span> <span style="color:#19177c">digit</span> <span style="color:#ba2121">"."</span>)))
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"(offline)"</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">seq</span> <span style="color:#ba2121">"["</span> (<span style="color:#00f">+</span> <span style="color:#19177c">alnum</span>) <span style="color:#ba2121">"]"</span>) ))
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">""</span> <span style="color:#19177c">title</span>))))
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> <span style="color:#19177c">is-meeting</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">`</span>(<span style="color:#ba2121">"Meeting"</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">,@</span>(<span style="color:#008000">when</span> (<span style="color:#19177c">alist-get</span> <span style="color:#008000">:events-online</span> <span style="color:#19177c">extra-params</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">if</span> <span style="color:#19177c">is-offline</span> <span style="color:#666">'</span>(<span style="color:#ba2121">"Offline"</span>) <span style="color:#666">'</span>(<span style="color:#ba2121">"Online"</span>)))
+</span></span><span style="display:flex;"><span> <span style="color:#666">,</span><span style="color:#19177c">title-without-stuff</span>))))
+</span></span></code></pre></div><p>For the following result:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>* Results
+</span></span><span style="display:flex;"><span>** Meetings
+</span></span><span style="display:flex;"><span>*** Some meeting
+</span></span><span style="display:flex;"><span>*** Another meeting
+</span></span><span style="display:flex;"><span>** Ungrouped
+</span></span></code></pre></div><p>This can be coupled with a project predicate to analyze the time spent per project in a particular kind of meeting.</p>
+
+
+
+
+
+ BIOME - Bountiful Interface to Open Meteo for Emacs
+ https://sqrtminusone.xyz/packages/biome/
+ Sat, 22 Jul 2023 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/biome/
+
+ <figure><a href="https://melpa.org/#/biome"><img src="https://melpa.org/packages/biome-badge.svg"/></a>
+</figure>
+
+<p>Interface to <a href="https://open-meteo.com/">Open Meteo</a> for Emacs. The service provides weather forecasts, historical weather data, climate change projections, and more.</p>
+<p>The service is AGPL-licensed; the hosted API is free for non-commercial use if you make less than 10000 requests per day.</p>
+<figure><img src="https://sqrtminusone.xyz/biome-img/report.png"/>
+</figure>
+
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you normally install packages, I prefer <a href="https://github.com/jwiegley/use-package">use-package</a> and <a href="https://github.com/radian-software/straight.el">straight.el</a>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">biome</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>)
+</span></span></code></pre></div><p>Or clone the repository, add it to <code>load-path</code>, and <code>require</code> the package.</p>
+<h2 id="usage">Usage</h2>
+<p>The main entry point is <code>M-x biome</code>. Each item under “Open Meteo Data” corresponds to a particular endpoint of the service. For instance, <code>M-x biome ww</code> is a generic weather forecast. Check out the <a href="https://open-meteo.com/en/docs">API docs</a> for more detailed descriptions.</p>
+<figure><img src="https://sqrtminusone.xyz/biome-img/root.png"/>
+</figure>
+
+<p>Each of these items opens a query interface. A query consists of “global” variables, such as location, units, etc., and “group variables”. Groups are usually “hourly” and “daily”.</p>
+<figure><img src="https://sqrtminusone.xyz/biome-img/query.png"/>
+</figure>
+
+<p>Global variables must always include a location (section “Select Coordinates or City”). To enter a location, you can either enter latitude and longitude (Open Meteo has an <a href="https://open-meteo.com/en/docs/geocoding-api">API for those</a> as well) or select a location from <code>biome-query-coords</code>. Example configuration:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">biome-query-coords</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">'</span>((<span style="color:#ba2121">"Helsinki, Finland"</span> <span style="color:#666">60.16952</span> <span style="color:#666">24.93545</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"Berlin, Germany"</span> <span style="color:#666">52.52437</span> <span style="color:#666">13.41053</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"Dubai, UAE"</span> <span style="color:#666">25.0657</span> <span style="color:#666">55.17128</span>)))
+</span></span></code></pre></div><p>A timezone is also often required (“Settings” > “Timezone”).</p>
+<p>The current group is switched with <code><tab></code>. Each group’s section has a set of variables that can be toggled on and off, such as temperature, precipitation, etc. Check out the <a href="https://open-meteo.com/en/docs">API docs</a> if you’re interested in the meaning of more esoteric ones.</p>
+<p>Press <code>RET</code> after you’ve configured the query to call the API. If something goes wrong, it will output an error, such as:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Open Meteo has returned an error.
+</span></span><span style="display:flex;"><span>Error: (error http 400)
+</span></span><span style="display:flex;"><span>Reason: Timezone is required
+</span></span></code></pre></div><p>Or it will open the results table (the first screenshot).</p>
+<p><code>tabulated-list</code> doesn’t support horizontal scrolling, so press <code>c</code> to toggle columns’ visibility.</p>
+<figure><img src="https://sqrtminusone.xyz/biome-img/columns.png"/>
+</figure>
+
+<h2 id="more-configuration">More configuration</h2>
+<p>To save a query for later, press <code>P</code> in the root of the query interface. This will generate a definition like this:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">biome-def-preset</span> <span style="color:#19177c">biome-query-preset-177</span>
+</span></span><span style="display:flex;"><span> ((<span style="color:#008000">:name</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Weather Forecast"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">"hourly"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:params</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"hourly"</span> <span style="color:#ba2121">"windgusts_10m"</span> <span style="color:#ba2121">"windspeed_10m"</span> <span style="color:#ba2121">"cloudcover"</span> <span style="color:#ba2121">"surface_pressure"</span> <span style="color:#ba2121">"weathercode"</span> <span style="color:#ba2121">"snowfall"</span> <span style="color:#ba2121">"showers"</span> <span style="color:#ba2121">"rain"</span> <span style="color:#ba2121">"relativehumidity_2m"</span> <span style="color:#ba2121">"temperature_2m"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"longitude"</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"latitude"</span> <span style="color:#666">.</span> <span style="color:#666">60.16952</span>))))
+</span></span></code></pre></div><p>Add this somewhere in your config after the package is loaded, e.g., in the <code>:config</code> section of the <code>use-package</code> form or wrapped in <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Hooks-for-Loading.html#index-with_002deval_002dafter_002dload">with-eval-after-load</a>. Running <code>M-x biome-query-preset-177</code> will create a query interface with this preset.</p>
+<p>Table formatting can be configured with <code>biome-grid-format</code>; check the docstring for more information. For instance, if you want to disable all gradients:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">biome-grid-format</span> (<span style="color:#19177c">seq-filter</span> (<span style="color:#008000">lambda</span> (<span style="color:#19177c">f</span>) (<span style="color:#19177c">not</span> (<span style="color:#00f">eq</span> (<span style="color:#00f">car-safe</span> (<span style="color:#00f">nth</span> <span style="color:#666">2</span> <span style="color:#19177c">f</span>))
+</span></span><span style="display:flex;"><span> <span style="color:#19177c">'gradient</span>)))
+</span></span><span style="display:flex;"><span> <span style="color:#19177c">biome-grid-format</span>))
+</span></span></code></pre></div><h2 id="composite-queries">Composite queries</h2>
+<p>The package also allows executing multiple queries at once to join their results. This can be useful for comparing weather in different locations or for viewing different reports about the same location.</p>
+<p>Run <code>M-x biome-multi</code> to invoke the-multi query dialog.</p>
+<figure><img src="https://sqrtminusone.xyz/biome-img/multi.png"/>
+</figure>
+
+<p>(<em>yes, I’ve switched to a light theme since the time of the previous screenshot</em>)</p>
+<p>Pressing <code>a</code> invokes the standard query dialog, where pressing <code>RET</code> returns to the root dialog, adding the query to the list. Pressing <code>RET</code> in the root dialog executes the queries in the list.</p>
+<p>Queries are executed concurrently. The results are shown if all queries have been successfully completed.</p>
+<p><code>P</code> generates a preset defintion for the current query:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">biome-def-multi-preset</span> <span style="color:#19177c">biome-query-preset-601</span>
+</span></span><span style="display:flex;"><span> (((<span style="color:#008000">:name</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Air Quality"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">"hourly"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:params</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"hourly"</span> <span style="color:#ba2121">"uv_index"</span> <span style="color:#ba2121">"european_aqi"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"longitude"</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"latitude"</span> <span style="color:#666">.</span> <span style="color:#666">60.16952</span>)))
+</span></span><span style="display:flex;"><span> ((<span style="color:#008000">:name</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Weather Forecast"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:group</span> <span style="color:#666">.</span> <span style="color:#ba2121">"hourly"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:params</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"hourly"</span> <span style="color:#ba2121">"weathercode"</span> <span style="color:#ba2121">"snowfall"</span> <span style="color:#ba2121">"showers"</span> <span style="color:#ba2121">"rain"</span> <span style="color:#ba2121">"temperature_2m"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"longitude"</span> <span style="color:#666">.</span> <span style="color:#666">24.93545</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"latitude"</span> <span style="color:#666">.</span> <span style="color:#666">60.16952</span>)))))
+</span></span></code></pre></div><p>Just note that the macro is called <code>biome-def-multi-preset</code>.</p>
+<h2 id="implementation-notes">Implementation notes</h2>
+<p>This isn’t the most complicated thing I’ve done, but it’s probably the most over-engineered one.</p>
+<p>As you may have guessed, the interfaces mirror the <a href="https://open-meteo.com/en/docs">API docs</a>. I’ve implemented <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Parsing-HTML_002fXML.html">parsing of these HTMLs</a> in <code>biome-api-parse--generate</code>, which generates the value of <code>biome-api-data</code>. Initially, it downloaded the HTML pages by itself, but - imagine that - the website was migrated to Svelte after I implemented maybe 80% of the parsing logic, and the Svelte version populates the accordions via JavaScript. So, as of now, the function requires opening the website in the browser, manually toggling all the accordions, and copying the HTML from DevTools. Fortunately, the parsing is a one-off operation.</p>
+<p>Then, the interface… I like <a href="https://github.com/magit/transient/">transient.el</a>, so I wanted to make the interface generated dynamically from <code>biome-api-data</code>, which turned out harder than I expected. I probably should’ve just used <a href="https://www.gnu.org/software/emacs/manual/html_mono/widget.html">widget.el</a>.</p>
+<p>Generating sensible keys was a challenge. I’ve made an algorithm in <code>biome-query--unique-keys</code> that sort of works well.</p>
+<p>And as for populating transient prefixes, I tried to use <code>:setup-children</code> in a few places, but it’s not general enough, namely, it doesn’t seem to support specifying <code>:class</code> for child groups… So I ended up overriding <code>transient--layout</code> in the prefix setup. This doesn’t seem to have any undesirable side effects.</p>
+<p>Also, the only way I found to use custom infix classes in these dynamic definitions was to eval <code>transient-define-infix</code> for each required place. Unfortunately, that adds a lot of stuff to the interactive functions namespace.</p>
+<p>Getting to the results display, Lars Ingebrigtsen’s <a href="https://lars.ingebrigtsen.no/2022/04/13/more-vtable-fun/">vtable</a> comes only in Emacs 29, so I used <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Tabulated-List-Mode.html">tabulated-list</a>. The only disadvantage of the latter is the lack of horizontal scroll support, which can be worked around by hiding columns with <code>biome-grid-columns</code>.</p>
+<p>Most variables are formatted with a gradient, colors for which were mostly inspired by <a href="https://www.windy.com/">Windy</a>. Formatting for things like air quality variables is probably all over the place, so take the red color with a grain of salt.</p>
+
+
+
+
+
+ micromamba.el
+ https://sqrtminusone.xyz/packages/micromamba/
+ Tue, 20 Jun 2023 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/micromamba/
+
+ <figure><a href="https://melpa.org/#/micromamba"><img src="https://melpa.org/packages/micromamba-badge.svg"/></a>
+</figure>
+
+<p>Emacs package for working with <a href="https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html">micromamba</a> environments.</p>
+<p><a href="https://mamba.readthedocs.io/en/latest/index.html">mamba</a> is a reimplementation of the <a href="https://docs.conda.io/en/latest/">conda</a> package manager in C++. <code>mamba</code> is notably much faster and essentially compatible with <code>conda</code>, so it also works with <a href="https://github.com/necaris/conda.el">conda.el</a>. <code>micromamba</code>, however, implements only a subset of <code>mamba</code> commands, and as such requires a separate integration.</p>
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you normally install packages, I prefer <code>use-package</code> and <code>straight</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">micromamba</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>)
+</span></span></code></pre></div><p>Or clone the repository, add it to the <code>load-path</code> and <code>require</code> the package.</p>
+<p>If your <code>micromamba</code> binary is located in some place unknown to <code>executable-find</code>, set the <code>micromamba-executable</code> variable.</p>
+<p>If you are running shells (e.g. <a href="https://github.com/akermu/emacs-libvterm">vterm</a>) from Emacs, you probably want to set <code>auto_activate_base</code> in your <a href="https://docs.conda.io/projects/conda/en/latest/user-guide/configuration/index.html">.condarc</a> or <a href="https://mamba.readthedocs.io/en/latest/user_guide/configuration.html">.mambarc</a>, because the shells are launched in the correct environment anyway.</p>
+<h2 id="usage">Usage</h2>
+<p>The package has two entrypoints:</p>
+<ul>
+<li><code>M-x micromamba-activate</code> - activate the environment</li>
+<li><code>M-x micromamba-deactivate</code> - deactivate the environment</li>
+</ul>
+<p><code>micromamba-activate</code> prompts for the environment (by parsing <code>micromamba env list</code>). If some environments have duplicate names, these names are replaced by full paths.</p>
+<p>I’ve noticed that <code>micromamba</code> also sees <code>conda</code> environments, so migrating from <code>conda</code> was rather painless for me.</p>
+<h2 id="implementation-notes">Implementation notes</h2>
+<p>I initially wanted to extend <a href="https://github.com/necaris/conda.el">conda.el</a>, but decided it would be counterproductive for a few reasons.</p>
+<p>First, <code>conda</code> is rather slow, so <code>conda.el</code> does various tricks to avoid calling the <code>conda</code> executable. For instance, it gets the environment list from scanning the anaconda home directory instead of running <code>conda env list</code>. This is really not necessary with <code>micromamba</code>, which is written in C++.</p>
+<p>Second, and more importantly, <code>conda.el</code> relies heavily on passing <code>shell.posix+json</code> to <code>conda</code>. <code>micromamba</code> doesn’t support that. It supports the <code>--json</code> flag in some places, but not in the <code>activate</code> command, so I have to parse the output of <code>micromamba shell -s bash activate</code> and <code>micromamba shell -s bash deactivate</code> to get the environment configuration.</p>
+<p>This also means the package most likely won’t work out-of-the-box on Windows.</p>
+
+
+
+
+
+ reverso.el
+ https://sqrtminusone.xyz/packages/reverso/
+ Sun, 28 Aug 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/reverso/
+
+ <p>Emacs client for <a href="https://www.reverso.net/">Reverso</a>. The implemented features are:</p>
+<ul>
+<li><a href="https://www.reverso.net/text-translation">Translation</a></li>
+<li><a href="https://context.reverso.net/translation/">Context</a> (AKA bilingual concordances)</li>
+<li><a href="https://www.reverso.net/spell-checker/english-spelling-grammar/">Grammar check</a></li>
+<li><a href="https://synonyms.reverso.net/synonym/">Synonyms search</a></li>
+</ul>
+<h2 id="installation">Installation</h2>
+<p>The package isn’t yet available anywhere but in this repository. My preferred way for such cases is <a href="https://github.com/jwiegley/use-package">use-package</a> and <a href="https://github.com/radian-software/straight.el">straight.el</a>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">reverso</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> (<span style="color:#008000">:host</span> <span style="color:#19177c">github</span> <span style="color:#008000">:repo</span> <span style="color:#ba2121">"SqrtMinusOne/reverso.el"</span>))
+</span></span></code></pre></div><p>Or clone the repository, add it to the <code>load-path</code> and <code>require</code> the package.</p>
+<h2 id="usage">Usage</h2>
+<p>There’s a single entrypoint for all implemented functions: <code>M-x reverso</code>. The UI is implemented using the excellent <a href="https://github.com/magit/transient/">transient.el</a>.</p>
+<h3 id="input-handling">Input Handling</h3>
+<p>All commands handle input as follows:</p>
+<p>By default, the input string is empty. If a command is launched with a region selected, use the string of that region. If launched with the prefix argument (<code>C-u</code>), use the entire buffer.</p>
+<p>Results are displayed in <code>reverso-result-mode</code> buffers. When launched within that buffer, the command uses the input string specific to the buffer. If launched with <code>C-u</code>, it uses the output string from that buffer (if available).</p>
+<h3 id="translation">Translation</h3>
+<p>Use <code>M-x reverso t</code> or <code>M-x reverso-translate</code> to invoke the translation transient.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/translation-transient.png"/>
+</figure>
+
+<p>The “Source language” and “Target language” parameters are self-explanatory. Note that not every language is compatible with every other language in the general case. “Swap languages” attempts to swap them.</p>
+<p>Enabling “Brief translation output” will display only the translated version of the string in the output buffer.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/translation-res.png"/>
+</figure>
+
+<p>Otherwise, the result buffer may contain the following sections:</p>
+<ul>
+<li><strong>Source text</strong> and <strong>Translation</strong></li>
+<li><strong>Corrected text</strong>, if available</li>
+<li><strong>Context results</strong>, if available</li>
+</ul>
+<p>Context results typically appear for short strings, as seen in the example from the screenshot.</p>
+<h3 id="context">Context</h3>
+<p>Use <code>M-x reverso c</code> or <code>M-x reverso-context</code> to invoke context search (or <a href="https://en.wikipedia.org/w/index.php?title=Online_bilingual_concordance&redirect=no">bilingual concordances</a>, essentially a Rosetta stone generator).</p>
+<p>The input/output UI resembles that of the translation command.</p>
+<p>Interestingly, direct context search often yields different results than the “Context results” section of the translation command. Hence, checking both might provide more comprehensive data.</p>
+<h3 id="synonyms">Synonyms</h3>
+<p>Use <code>M-x reverso s</code> or <code>M-x reverso-synonyms</code> to invoke the synonyms search.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/synonyms-transient.png"/>
+</figure>
+
+<figure><img src="https://sqrtminusone.xyz/reverso-img/synonyms-res.png"/>
+</figure>
+
+<p>If necessary, results are segmented by parts of speech.</p>
+<p>Each part of speech section contains up to three subsections:</p>
+<ul>
+<li>Synonyms</li>
+<li>Examples</li>
+<li>Antonyms</li>
+</ul>
+<h3 id="grammar-check">Grammar check</h3>
+<p>Use <code>M-x reverso g</code> or <code>M-x reverso-grammar</code> to invoke the grammar check.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/grammar-transient.png"/>
+</figure>
+
+<p>Currently, only English, French, Spanish, and Italian languages are available.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/grammar-res.png"/>
+</figure>
+
+<p>The results may contain the following sections:</p>
+<ul>
+<li><strong>Source text</strong>, highlighting errors with <code>reverso-error-face</code></li>
+<li><strong>Corrected text</strong></li>
+<li><strong>Corrections</strong></li>
+</ul>
+<h3 id="grammar-check-in-buffer">Grammar check in buffer</h3>
+<p>It can be convenient to apply the grammar check directly to the current buffer without displaying results in another buffer. Use <code>M-x reverso b</code> or <code>M-x reverso-grammar-buffer</code> for this.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/grammar-buffer-transient.png"/>
+</figure>
+
+<p>Running <code>e</code> there (or <code>M-x reverso-check-buffer</code>) utilizes the current buffer as input and highlights any found errors using overlays. If a region is selected, the check is confined to that region.</p>
+<p>There are a couple of caveats there. First, the service considers each linebreak as a new line, which is incompatible with <a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Filling.html">filling text</a>, i.e. breaking it into lines of a specified width. The “Remove linebreaks” option (<code>l</code>) is a workaround for this.</p>
+<p>Secondly, the service usually freaks out with special syntax, for instance, Org Mode links.</p>
+<p>The third issue partly follows from the second one, as the service often finds “errors” within hidden parts of Org links. Either skip these errors or execute <code>M-x org-toggle-link-display</code> in Org files beforehand.</p>
+<p>Lastly (and this applies to all other methods as well), the API usually restricts input size. If the service returns an error, try running the command on a smaller region of the buffer.</p>
+<figure><img src="https://sqrtminusone.xyz/reverso-img/grammar-buffer-res.png"/>
+</figure>
+
+<p>When the cursor is placed on an error, the “Information” section provides details.</p>
+<p>“Fix error” (<code>f</code> or <code>M-x reverso-check-fix-at-point</code>) opens a completion interface with potential fixes. “Ignore error” (<code>i</code> or <code>M-x reverso-check-ignore-error</code>) simply removes the overlay and moves to the next error.</p>
+<p>“Previous error” (<code>p</code> or <code>M-x reverso-check-prev-error</code>), “Next error” (<code>n</code> or <code>M-x reverso-check-next-error</code>), “First error” (<code>P</code> or <code>M-x reverso-check-first-error</code>) and “Last error” (<code>L</code> or <code>M-x reverso-check-last-error</code>) serve to navigate the error list.</p>
+<p>“Clear” (<code>c</code> or <code>M-x reverso-clear</code>) removes error overlays. If a region is selected, it removes overlays only in that region; otherwise, it removes them from the entire buffer.</p>
+<h3 id="history">History</h3>
+<p>Enable <code>reverso-history-mode</code> to keep history:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">reverso-history-mode</span>)
+</span></span></code></pre></div><p>I haven’t implemented persistence yet, but I might in the future.</p>
+<p>After enabling the minor mode, <code>M-x reverso-history</code> or <code>M-x reverso h</code> will display recent commans. <code>RET</code> on shows the results of each command.</p>
+<h2 id="caveats">Caveats</h2>
+<p>Before proceeding further, here are some general caveats to be aware of.</p>
+<p>Firstly, the package uses a reverse-engineered API, so all the typical consequences apply, such as sudden irreparable breakages. Although I’ve been using it for over a year, so… maybe not.</p>
+<p>Secondly, the limit on input size has been mentioned. The obvious is executing commands on a smaller region.</p>
+<p>Thirdly, there have been reports that Reverso dispatches <strong>IP bans</strong> to particularly enthusiastic users, so be cautious if you’re sending lots of automated queries. This is also why I didn’t implement running one command for multiple consecutive regions.</p>
+<p>Lastly, exercise caution with the content sent to the service. Avoid inadvertently sharing confidential information (like passwords) or anything that could be used against you in other ways. While the service claims to be <a href="https://www.reverso.net/privacy.aspx?lang=EN">GDPR-compliant</a>, we can’t actually check that.</p>
+<h2 id="customization">Customization</h2>
+<p>Run <code>M-x customize-group reverso</code> to view the available parameters. Here are a few.</p>
+<p>If you don’t need all 17 languages, customize the <code>reverso-languages</code> variable to narrow down the list:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">reverso-languages</span> <span style="color:#666">'</span>(<span style="color:#19177c">english</span> <span style="color:#19177c">german</span> <span style="color:#19177c">russian</span>))
+</span></span></code></pre></div><p>If the length of <code>reverso-languages</code> exceeds <code>reverso-language-completing-read-threshold</code>, switching a language in transient buffers will invoke <code>completing-read</code> (minibuffer completion). Otherwise, it will simply switch to the next language available.</p>
+<p><code>reverso-max-display-lines-in-input</code> controls the maximum number of lines displayed in the input section of a transient buffer.</p>
+<p>The available faces:</p>
+<ul>
+<li><code>reverso-highlight-face</code></li>
+<li><code>reverso-error-face</code></li>
+<li><code>reverso-heading-face</code></li>
+<li><code>reverso-keyword-face</code></li>
+<li><code>reverso-definition-face</code></li>
+</ul>
+<p>are inherited from the faces of <code>transient.el</code> and <code>basic-faces</code> to look nice.</p>
+<h2 id="elisp-api">Elisp API</h2>
+<p>In Emacs Lisp, there are four primary functions that interact with the Reverso API:</p>
+<ul>
+<li><code>reverso--translate</code></li>
+<li><code>reverso--get-context</code></li>
+<li><code>reverso--get-grammar</code></li>
+<li><code>reverso--get-context</code></li>
+</ul>
+<p>Refer to the docstrings for more detailed information.</p>
+<p>Each function is asynchronous, and the results are retrieved via a callback.</p>
+<p>As Reverso sometimes modifies its available languages and compatibility matrix, so if you change that, execute <code>reverso-verify-settings</code> to check for potential errors.</p>
+<h2 id="alternatives-and-observations">Alternatives and Observations</h2>
+<p>A widely recognized translation service is <a href="https://translate.google.com/">Google Translate</a>, so of course, there’s an <a href="https://github.com/atykhonov/google-translate">Emacs client</a> for it.</p>
+<p>The <a href="https://github.com/emacs-grammarly">emacs-grammarly</a> package series provides the Elisp API for <a href="https://www.grammarly.com/">Grammarly</a> (a grammar checking service) along with multiple frontends. Unlike Reverso, Grammarly has an official API (so you don’t risk getting an IP ban), and it allows a much larger input size.</p>
+<p>Additionally, Grammarly is less bothered by Org and Markdown syntax, although it struggles with inline code blocks. It seems to do work generally better than Reverso, but it also generates a lot of false positives. For instance, it finds a lot of issues in <a href="https://www.economist.com/">The Economist</a> articles, which, I think, have beautiful English.</p>
+<p>Another notable grammar-checking solution is <a href="https://languagetool.org/">LanguageTool</a>, which can be <a href="https://dev.languagetool.org/http-server">run offline</a> and used with its <a href="https://github.com/mhayashi1120/Emacs-langtool">Emacs package</a>. This tool offers the advantage of unlimited usage and doesn’t transmit your data to a third-party server you can’t control. But it still doesn’t like markup syntaxes.</p>
+<p>Also, I’ve been pretty happy with <a href="https://github.com/valentjn/ltex-ls">LTeX LS</a>, which is a LanguageTool-based language server explicitly designed to support markup formats like Org, Markdown, LaTeX, among others.</p>
+<p>The <a href="https://www.npmjs.com/package/reverso-api">reverso-api</a> npm package implements the same commands in JavaScript. It also provided invaluable information for creating this package.</p>
+
+
+
+
+
+ elfeed-sync
+ https://sqrtminusone.xyz/packages/elfeed-sync/
+ Sun, 29 May 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/elfeed-sync/
+
+ <p>Sync read/marked status of entries between <a href="https://github.com/skeeto/elfeed">elfeed</a> and <a href="https://tt-rss.org/">tt-rss</a>. Supports <a href="https://github.com/SqrtMinusOne/elfeed-summary">elfeed-summary</a>.</p>
+<p>DISCLAIMER: It’s still an alpha version of the package, so you may want to backup your elfeed index and tt-rss database.</p>
+<figure><img src="https://sqrtminusone.xyz/elfeed-sync-img/screenshot.png"/>
+</figure>
+
+<h2 id="installation">Installation</h2>
+<p>The project consists of the tt-rss plugin and the Emacs package.</p>
+<p>If you are using the <a href="https://git.tt-rss.org/fox/ttrss-docker-compose.git/tree/README.md">tt-rss docker</a> setup, the steps are as follows. Change them accordingly if you are not.</p>
+<ol>
+<li>
+<p>Mount the <code>/var/www/html</code> directory from the container somewhere to the filesystem as described <a href="https://git.tt-rss.org/fox/ttrss-docker-compose.wiki.git/tree/Home.md#how-do-i-use-dynamic-image-for-development">here</a>.</p>
+</li>
+<li>
+<p>Put the repository to the <code>tt-rss/plugins.local/elfeed_sync</code> folder:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#008000">cd</span> ./html/tt-rss/plugins.local/
+</span></span><span style="display:flex;"><span>git clone https://github.com/SqrtMinusOne/elfeed-sync.git elfeed_sync
+</span></span></code></pre></div></li>
+<li>
+<p>Add <code>elfeed_sync</code> to the <code>TTRSS_PLUGINS</code> environment variable.</p>
+<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">TTRSS_PLUGINS=auth_internal, auth_remote, nginx_xaccel, elfeed_sync
+</code></pre></li>
+<li>
+<p>Allow larger request body sizes in nginx. Add the following to the <code>server</code> directive:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cfg" data-lang="cfg"><span style="display:flex;"><span><span style="color:#7d9029">client_max_body_size 10M;</span>
+</span></span></code></pre></div><p>For me, the sync payload is around 3M.</p>
+</li>
+<li>
+<p>Increase the read timeout in nginx. Add the following to the php location directive:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cfg" data-lang="cfg"><span style="display:flex;"><span><span style="color:#7d9029">fastcgi_read_timeout 600;</span>
+</span></span></code></pre></div><p>Syncing the entries is usually pretty fast, but the first feed sync takes a while.</p>
+</li>
+<li>
+<p>Then restart tt-rss. Check if the plugin appears in the Preferences > Plugins section.</p>
+</li>
+<li>
+<p>Enable “Allows accessing this account through the API” in the Preferences > Preferences. You also may want to disable “Purge unread articles”, because elfeed doesn’t do that.</p>
+</li>
+</ol>
+<p>Install the Emacs package however you normally install packages, I prefer use-package and straight.el. Make sure to enable <code>elfeed-sync-mode</code>.</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">elfeed-sync</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> (<span style="color:#008000">:host</span> <span style="color:#19177c">github</span> <span style="color:#008000">:repo</span> <span style="color:#ba2121">"SqrtMinusOne/elfeed-sync"</span>)
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:after</span> <span style="color:#19177c">elfeed</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:config</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">elfeed-sync-mode</span>))
+</span></span></code></pre></div><p>Then set up the following variables:</p>
+<ul>
+<li><code>elfeed-sync-tt-rss-instance</code> - point that to your tt-rss instance, e.g.
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>https://example.com/tt-rss
+</span></span></code></pre></div>(No trailing slash)</li>
+<li><code>elfeed-sync-tt-rss-login</code></li>
+<li><code>elfeed-sync-tt-rss-password</code></li>
+<li><code>elfeed-sync-unread-tag</code> - elfeed tag to map to read status in tt-rss. <code>unread</code> by default.</li>
+<li><code>elfeed-sync-marked-tag</code> - elfeed tag to map to marked status in tt-rss. <code>later</code> by default.</li>
+</ul>
+<h2 id="usage">Usage</h2>
+<h3 id="syncing-the-feed-list">Syncing the feed list</h3>
+<p>The first thing you probably want to do is to sync the feed list.</p>
+<p>It makes little sense to sync tt-rss feeds to elfeed, because people often use projects like <a href="https://github.com/remyhonig/elfeed-org">elfeed-org</a>. But it’s possible to sync elfeed feeds to tt-rss.</p>
+<p>The function <code>M-x elfeed-sync-feeds</code> does exactly that. If you have <a href="https://github.com/SqrtMinusOne/elfeed-summary">elfeed-summary</a> installed and tt-rss categories enabled, the function will recreate the elfeed-summary tree in tt-rss.</p>
+<p>The first run of the function takes a while because tt-rss has to fetch the feed at the moment of the first subscription. Which is why increasing the timeout may be necessary.</p>
+<p>However, running the function multiple times until it succeeds should also work.</p>
+<h3 id="syncing-the-entry-list">Syncing the entry list</h3>
+<p>To sync the entry list, run <code>M-x elfeed-sync</code>. The sync usually takes a couple of seconds.</p>
+<p>The sync finishes at the “Sync complete!” message. Check the <code>*elfeed-log*</code> buffer for statistics.</p>
+<p>Occasionally, some entries do not match. Here are the possible cases:</p>
+<ul>
+<li>Entry exists in the elfeed database, but not in tt-rss.
+Run <code>M-x elfeed-sync-search-missing</code> to display such entries.</li>
+<li>Entry exists in the tt-rss database, but not in elfeed:
+<ul>
+<li>Entry appeared in the feed after the last <code>elfeed-update</code>.
+Run <code>M-x elfeed-update</code> and then <code>M-x elfeed-sync</code>.</li>
+<li>Entry appeared and disappeared in the feed after the last <code>elfeed-update</code>.
+Such an entry will never get to the elfeed database. If you want to, run <code>M-x elfeed-sync</code> and then <code>M-x elfeed-sync-read-ttrss-missing</code> to mark all such entries as read.</li>
+</ul>
+</li>
+<li>Entry appeared in the feed before <code>elfeed-sync-look-back</code>.
+Such an entry will never be matched. This is an inconvenience if you have just set up tt-rss, it fetched old entries from the feeds and such entries remain permanently unread because they are untouched by the <code>M-x elfeed-sync</code>.
+To mark such entries as read, run <code>M-x elfeed-sync-read-ttrss-old</code>.</li>
+</ul>
+<h2 id="implementation-details">Implementation details</h2>
+<p>The heavy-lifting is done on the elisp side because I ran into strange performance issues with associative arrays in PHP.</p>
+<p>Check the <code>elfeed-sync--do-sync</code> function for the description of the synchronization algorithm. The tl;dr is to download all entries from tt-rss and match each entry against the elfeed database. In the case of discrepancy update whichever entry has the lower priority.</p>
+
+
+
+
+
+ avy-dired
+ https://sqrtminusone.xyz/packages/avy-dired/
+ Fri, 01 Apr 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/avy-dired/
+
+ <p>Doing some experimentation with avy & dired. Still somewhat flaky.</p>
+<figure><img src="https://sqrtminusone.xyz/avy-dired-img/screenshot.png"/>
+</figure>
+
+<p>The only available command is <code>M-x avy-dired-goto-line</code>. Use <code>K</code> and <code>J</code> to scroll up and down while in the avy state, <code>C-g</code> or <code>q</code> to quit.</p>
+
+
+
+
+
+ elfeed-summary
+ https://sqrtminusone.xyz/packages/elfeed-summary/
+ Sat, 26 Mar 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/elfeed-summary/
+
+ <figure><a href="https://melpa.org/#/elfeed-summary"><img src="https://melpa.org/packages/elfeed-summary-badge.svg"/></a>
+</figure>
+
+<p>The package provides a tree-based feed summary interface for <a href="https://github.com/skeeto/elfeed">elfeed</a>. The tree can include individual feeds, <a href="https://github.com/skeeto/elfeed#filter-syntax">searches</a>, and groups. It mainly serves as an easier “jumping point” for elfeed, so to make querying a subset of the elfeed database one action away.</p>
+<p>Inspired by <a href="https://github.com/newsboat/newsboat">newsboat</a>.</p>
+<figure><img src="https://sqrtminusone.xyz/elfeed-summary-img/screenshot.png"/>
+</figure>
+
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA, so install it however you normally install packages. My preferred way is <code>use-package</code> with <code>straight</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">elfeed-summary</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>)
+</span></span></code></pre></div><p>Of course, you have to have <a href="https://github.com/skeeto/elfeed">elfeed</a> configured.</p>
+<h2 id="usage">Usage</h2>
+<p>Running <code>M-x elfeed-summary</code> opens up the summary buffer, as shown on the screenshot.</p>
+<p>The tree consists of:</p>
+<ul>
+<li>feeds;</li>
+<li>searches;</li>
+<li>groups, that can include other groups, feeds, and searches.</li>
+</ul>
+<p>Groups can also be generated automatically.</p>
+<p>Available keybindings in the summary mode:</p>
+<table>
+<thead>
+<tr>
+<th>Keybinding</th>
+<th>Command</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><code>RET</code></td>
+<td><code>elfeed-summary--action</code></td>
+<td>Open thing under the cursor (a feed, search, or a group). If there is at least one unread item, it will show only unread items.</td>
+</tr>
+<tr>
+<td><code>M-RET</code></td>
+<td><code>elfeed-summary--action-show-read</code></td>
+<td>Open thing under the cursor, but always include read items</td>
+</tr>
+<tr>
+<td><code>q</code></td>
+<td>…</td>
+<td>Quit the summary buffer</td>
+</tr>
+<tr>
+<td><code>r</code></td>
+<td><code>elfeed-summary--refresh</code></td>
+<td>Refresh the summary buffer</td>
+</tr>
+<tr>
+<td><code>R</code></td>
+<td><code>elfeed-summary-update</code></td>
+<td>Run update for elfeed feeds</td>
+</tr>
+<tr>
+<td><code>u</code></td>
+<td><code>elfeed-summary-toggle-only-unread</code></td>
+<td>Toggle showing only unread entries</td>
+</tr>
+<tr>
+<td><code>U</code></td>
+<td><code>elfeed-summary--action-mark-read</code></td>
+<td>Mark everything in the entry under the cursor as read</td>
+</tr>
+</tbody>
+</table>
+<p>The standard keybindings from <a href="https://magit.vc/manual/magit.html#Sections">magit-section</a> are also available, for instance <code>TAB</code> toggles the visibility of the current group. <a href="https://github.com/emacs-evil/evil">evil-mode</a> is also supported.</p>
+<h2 id="configuration">Configuration</h2>
+<h3 id="tree-configuration">Tree configuration</h3>
+<p>The structure of the tree is determined by the <code>elfeed-summary-settings</code> variable.</p>
+<p>This is a list of these possible items:</p>
+<ul>
+<li>Group <code>(group . <group-params>)</code>
+Groups are used to group elements under collapsible sections.</li>
+<li>Query <code>(query . <query-params>)</code>
+Query extracts a subset of elfeed feeds based on the given criteria. Each found feed will be represented as a line.</li>
+<li>Search <code>(search . <search-params>)</code>
+Elfeed search, as defined by <code>elfeed-search-set-filter</code>.</li>
+<li>Tags tree <code>(auto-tags . <auto-tags-params>)</code>
+A tree generated automatically from the available tags.</li>
+<li>Tag groups <code>(tag-groups . <tag-group-params>)</code>
+Insert one tag as one group.</li>
+<li>a few special forms</li>
+</ul>
+<p><code><group-params></code> is an alist with the following keys:</p>
+<ul>
+<li><code>:title</code> (mandatory)</li>
+<li><code>:elements</code> (mandatory) - elements of the group. The structure is the same as in the root definition.</li>
+<li><code>:face</code> - group face. The default face is <code>elfeed-summary-group-face</code>.</li>
+<li><code>:hide</code> - if non-nil, the group is collapsed by default.</li>
+</ul>
+<p><code><query-params></code> can be:</p>
+<ul>
+<li>A symbol of a tag.
+A feed will be matched if it has that tag.</li>
+<li><code>:all</code>. Will match anything.</li>
+<li><code>(title . "string")</code> or <code>(title . <form>)</code>
+Match feed title with <code>string-match-p</code>. <form> makes sense if you
+want to pass something like <code>rx</code>.</li>
+<li><code>(author . "string")</code> or <code>(author . <form>)</code></li>
+<li><code>(url . "string")</code> or <code>(url . <form>)</code></li>
+<li><code>(and <q-1> <q-2> ... <q-n>)</code>
+Match if all the conditions 1, 2, …, n match.</li>
+<li><code>(or <q-1> <q-2> ... <q-n>)</code> or <code>(<q-1> <q-2> ... <q-n>)</code>
+Match if any of the conditions 1, 2, …, n match.</li>
+<li><code>(not <query>)</code></li>
+</ul>
+<p>Feed tags for the query are determined by the <code>elfeed-feeds</code> variable.</p>
+<p>Query examples:</p>
+<ul>
+<li><code>(emacs lisp)</code>
+Return all feeds that have either “emacs” or “lisp” tags.</li>
+<li><code>(and emacs lisp)</code>
+Return all feeds that have both “emacs” and “lisp” tags.</li>
+<li><code>(and (title . "Emacs") (not planets))</code>
+Return all feeds that have “Emacs” in their title and don’t have
+the “planets” tag.</li>
+</ul>
+<p><code><search-params></code> is an alist with the following keys:</p>
+<ul>
+<li><code>:filter</code> (mandatory) filter string, as defined by
+<code>elfeed-search-set-filter</code></li>
+<li><code>:title</code> (mandatory) title.</li>
+<li><code>:tags</code> - list of tags to get the face of the entry.</li>
+</ul>
+<p><code><auto-tags-params></code> is an alist with the following keys:</p>
+<ul>
+<li><code>:max-level</code> - maximum level of the tree (default 2)</li>
+<li><code>:source</code> - which feeds to use to build the tree.
+Can be <code>:misc</code> (default) or <code>(query . <query-params>)</code>.</li>
+<li><code>:original-order</code> - do not try to build a more concise tree by
+putting the most frequent tags closer to the root of the tree.</li>
+<li><code>:faces</code> - list of faces for groups.</li>
+</ul>
+<p><code><tag-group-params></code> is an alist with the following keys:</p>
+<ul>
+<li><code>:source</code> - which feeds to use to build the tree.
+Can be <code>:misc</code> (default) or <code>(query . <query-params>)</code>.</li>
+<li><code>:repeat-feeds</code> - allow feeds to repeat. Otherwise, each feed is
+assigned to group with the least amount of members.</li>
+<li><code>:face</code> - face for groups.</li>
+</ul>
+<p>Available special forms:</p>
+<ul>
+<li><code>:misc</code> - print out feeds, not found by any query above.</li>
+</ul>
+<p>Also keep in mind that <code>'(key . ((values)))</code> is the same as <code>'(key (values))</code>. This helps to shorten the form in many cases.</p>
+<p>Also, this variable is not validated by any means, so wrong values can produce somewhat cryptic errors. Sorry about that.</p>
+<h3 id="example">Example</h3>
+<p>Here is an excerpt from my configuration that was used to produce this screenshot:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">elfeed-summary-settings</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">'</span>((<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"GitHub"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#19177c">url</span> <span style="color:#666">.</span> <span style="color:#ba2121">"SqrtMinusOne.private.atom"</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> <span style="color:#666">.</span> ((<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Guix packages"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">github</span> <span style="color:#19177c">guix_packages</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:hide</span> <span style="color:#800">t</span>)))))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Blogs [Software]"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> <span style="color:#19177c">software_blogs</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Blogs [People]"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">blogs</span> <span style="color:#19177c">people</span> (<span style="color:#19177c">not</span> <span style="color:#19177c">emacs</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Emacs"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">blogs</span> <span style="color:#19177c">people</span> <span style="color:#19177c">emacs</span>))))))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Podcasts"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> <span style="color:#19177c">podcasts</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Videos"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Music"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">videos</span> <span style="color:#19177c">music</span>))))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Tech"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">videos</span> <span style="color:#19177c">tech</span>))))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"History"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">query</span> <span style="color:#666">.</span> (<span style="color:#008000">and</span> <span style="color:#19177c">videos</span> <span style="color:#19177c">history</span>))))
+</span></span><span style="display:flex;"><span> <span style="color:#408080;font-style:italic">;; ...</span>
+</span></span><span style="display:flex;"><span> ))
+</span></span><span style="display:flex;"><span> <span style="color:#408080;font-style:italic">;; ...</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Miscellaneous"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Searches"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">search</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:filter</span> <span style="color:#666">.</span> <span style="color:#ba2121">"@6-months-ago sqrtminusone"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"About me"</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">search</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:filter</span> <span style="color:#666">.</span> <span style="color:#ba2121">"+later"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Check later"</span>))))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">group</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:title</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Ungrouped"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">:elements</span> <span style="color:#008000">:misc</span>))))))
+</span></span></code></pre></div><h3 id="automatic-generation-of-groups">Automatic generation of groups</h3>
+<h4 id="auto-tags"><code>auto-tags</code></h4>
+<p>As described in the <a href="#tree-configuration-1">tree configuration</a> section, there are two ways to avoid defining all the relevant groups manually, <code>auto-tags</code> and <code>tag-groups</code>. Both use tags that are defined in <code>elfeed-feeds</code>.</p>
+<p><code>auto-tags</code> tries to build the most concise tree from these tags. E.g. if we have feeds:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>feed1 tag1 tag2
+</span></span><span style="display:flex;"><span>feed2 tag1 tag2
+</span></span><span style="display:flex;"><span>feed3 tag1 tag3
+</span></span><span style="display:flex;"><span>feed4 tag1 tag3
+</span></span></code></pre></div><p>It will create the following tree:</p>
+<ul>
+<li>tag1
+<ul>
+<li>tag2
+<ul>
+<li>feed1</li>
+<li>feed2</li>
+</ul>
+</li>
+<li>tag3
+<ul>
+<li>feed3</li>
+<li>feed4</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+<p>The tree is truncated by <code>:max-level</code>, which is 2 by default.</p>
+<p>If tags don’t form this kind of hierarchy in <code>elfeed-feeds</code>, the algorithm will still try to build the most “optimal” tree, where the most frequent tags are on the top.</p>
+<p>To avoid that you can set <code>(:original-order . t)</code>, in which case each feed will be placed at the path <code>tag1 tag2 ... tagN feed</code>, where the order of tags is the same as in <code>elfeed-feeds</code>. By the way, this allows reproducing the hierarchy of <a href="https://github.com/remyhonig/elfeed-org">elfeed-org</a>, e.g. this structure:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>* tag1 :tag1:
+</span></span><span style="display:flex;"><span>** feed1
+</span></span><span style="display:flex;"><span>** feed2 :tag2:
+</span></span><span style="display:flex;"><span>** feed3 :tag2:
+</span></span><span style="display:flex;"><span>* tag3 :tag3:
+</span></span><span style="display:flex;"><span>** feed4 :tag2:
+</span></span><span style="display:flex;"><span>** feed5 :tag2:
+</span></span><span style="display:flex;"><span>** feed6 :tag2:
+</span></span></code></pre></div><p>Will be converted to this:</p>
+<ul>
+<li>tag1
+<ul>
+<li>feed1</li>
+<li>tag2
+<ul>
+<li>feed2</li>
+<li>feed3</li>
+</ul>
+</li>
+</ul>
+</li>
+<li>tag3
+<ul>
+<li>tag2
+<ul>
+<li>feed4</li>
+<li>feed5</li>
+<li>feed6</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+<p>Whereas without <code>:original-order</code> the structure will be:</p>
+<ul>
+<li>tag1
+<ul>
+<li>feed1</li>
+</ul>
+</li>
+<li>tag2
+<ul>
+<li>tag1
+<ul>
+<li>feed2</li>
+<li>feed3</li>
+</ul>
+</li>
+<li>tag3
+<ul>
+<li>feed4</li>
+<li>feed5</li>
+<li>feed6</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+<h4 id="tag-groups"><code>tag-groups</code></h4>
+<p>The second option is <code>tag-groups</code>, which creates a group for each tag.</p>
+<p>By default, each feed is assigned to its less frequent tag. This can be turned off by setting <code>(:repeat-feeds . t)</code>.</p>
+<p>E.g., the elfeed-org setup from the section above will be converted to this structure:</p>
+<ul>
+<li>tag1
+<ul>
+<li>feed1</li>
+<li>feed2</li>
+<li>feed3</li>
+</ul>
+</li>
+<li>tag3
+<ul>
+<li>feed4</li>
+<li>feed5</li>
+<li>feed6</li>
+</ul>
+</li>
+</ul>
+<p>And with <code>:repeat-feeds</code>:</p>
+<ul>
+<li>tag1
+<ul>
+<li>feed1</li>
+<li>feed2</li>
+<li>feed3</li>
+</ul>
+</li>
+<li>tag2
+<ul>
+<li>feed2</li>
+<li>feed3</li>
+<li>feed4</li>
+<li>feed5</li>
+<li>feed6</li>
+</ul>
+</li>
+<li>tag3
+<ul>
+<li>feed4</li>
+<li>feed5</li>
+<li>feed6</li>
+</ul>
+</li>
+</ul>
+<h4 id="common-options">Common options</h4>
+<p>Both <code>auto-tags</code> and <code>tag-groups</code> allow setting the <code>:search</code> parameter.</p>
+<p>The default value is <code>(:search . :misc)</code>, i.e. use feeds that weren’t found by other queries.</p>
+<p>Passing <code>(:search . (query . <query-params>))</code> is another option.</p>
+<h3 id="faces">Faces</h3>
+<p>Group faces by default use the <code>elfeed-summary-group-faces</code> variable, which serves as a list of faces for each level of the tree. Individual group faces can be overridden with the <code>:face</code> attribute.</p>
+<p>Feed faces by default reuse <a href="https://github.com/skeeto/elfeed#custom-tag-faces">the existing elfeed mechanism</a>. The tags for feeds are taken from the <code>elfeed-feeds</code> variable; if a feed has at least one unread entry, the unread tag is added to the list. This can be overridden by setting the <code>elfeed-summary-feed-face-fn</code> variable.</p>
+<p>Searches are mostly the same as feeds, but tags for the search are taken from the <code>:tags</code> attribute. This also can be overridden with <code>elfeed-summary-search-face-fn</code> variable.</p>
+<h3 id="opening-elfeed-search-in-other-window">Opening <code>elfeed-search</code> in other window</h3>
+<p>If you set:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">elfeed-summary-other-window</span> <span style="color:#800">t</span>)
+</span></span></code></pre></div><p>Then <code>RET</code> and <code>M-RET</code> in the <code>elfeed-summary</code> buffer will open the search buffer in other window.</p>
+<p><code>elfeed-summary-width</code> regulates the width of the remaining summary window in this case. It is useful because the data in the search buffer is generally wider than in the summary buffer. The variable can also be set to <code>nil</code> to disable this behavior.</p>
+<h3 id="skipping-feeds">Skipping feeds</h3>
+<p><a href="https://tt-rss.org/">tt-rss</a> has a feature to disable updating a particular feed but keep it in the feed list. I also want that for elfeed.</p>
+<p>To use that, set <code>elfeed-summary-skip-sync-tag</code> to some value:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">elfeed-summary-skip-sync-tag</span> <span style="color:#19177c">'skip</span>)
+</span></span></code></pre></div><p>And tag the feeds you want to skip with this tag. Then, running <code>M-x elfeed-summary-update</code> will skip them. This won’t affect <code>M-x elfeed-update</code> unless you:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">advice-add</span> <span style="color:#00f">#'</span><span style="color:#19177c">elfeed-update</span> <span style="color:#008000">:override</span> <span style="color:#00f">#'</span><span style="color:#19177c">elfeed-summary-update</span>)
+</span></span></code></pre></div><p>Also watch out if you use <a href="https://github.com/remyhonig/elfeed-org">elfeed-org</a> and want to use the <code>ignore</code> tag, because this package omits feeds with this tag altogether (configurable by <code>rmh-elfeed-org-ignore-tag</code>).</p>
+<h3 id="other-options">Other options</h3>
+<p>Also take a look at <code>M-x customize-group elfeed-summary</code> for the rest of available options.</p>
+<h2 id="ideas-and-alternatives">Ideas and alternatives</h2>
+<p>The default interface of elfeed is just a list of all entries. Naturally, it gets hard to navigate when there are a lot of sources with varying frequencies of posts.</p>
+<p>Elfeed itself provides one solution, which is using <a href="https://github.com/skeeto/elfeed#bookmarks">bookmarks</a> to save individual <a href="https://github.com/skeeto/elfeed#filter-syntax">searches</a>. This can work, but it can be somewhat cumbersome.</p>
+<p><a href="https://github.com/sp1ff/elfeed-score">elfeed-score</a> is another solution, which introduces scoring rules for entries. Thus, with proper rules set, the most important entries should be on the top of the list. You can take a look at <a href="https://www.youtube.com/watch?v=rvWbUGx9U5E">this video by John Kitchin</a> to see how this can work.</p>
+<p>However, I mostly had <code>elfeed-score</code> to group entries to sets with equal scores, and I then processed one such set or the other. This is why I decided this package is a better fit for my workflow.</p>
+<p>Another idea I used often before that is this function:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/elfeed-search-filter-source</span> (<span style="color:#19177c">entry</span>)
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"Filter elfeed search buffer by the feed under the cursor."</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">interactive</span> (<span style="color:#00f">list</span> (<span style="color:#19177c">elfeed-search-selected</span> <span style="color:#008000">:ignore-region</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> (<span style="color:#19177c">elfeed-entry-p</span> <span style="color:#19177c">entry</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">elfeed-search-set-filter</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#00f">concat</span>
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"@6-months-ago "</span>
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"+unread "</span>
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">"="</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">replace-regexp-in-string</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">rx</span> <span style="color:#ba2121">"?"</span> (<span style="color:#00f">*</span> <span style="color:#19177c">not-newline</span>) <span style="color:#19177c">eos</span>)
+</span></span><span style="display:flex;"><span> <span style="color:#ba2121">""</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">elfeed-feed-url</span> (<span style="color:#19177c">elfeed-entry-feed</span> <span style="color:#19177c">entry</span>)))))))
+</span></span></code></pre></div><p>I’ve bound it to <code>o</code>, so I would open <code>elfeed</code>, press <code>o</code>, and only see unread entries from a particular feed. Then I cleaned the filter and switched to the next feed. Once again, a tree with feeds is obviously a better tool for such a workflow.</p>
+<p>The last solution I want to mention is <a href="https://github.com/manojm321/elfeed-dashboard">elfeed-dashboard</a>, although I didn’t test this one. It looks similar to this package but seems to require much more fine-tuning, for instance, it doesn’t allow to list all the feeds with a certain tag in a group.</p>
+
+
+
+
+
+ password-store-ivy
+ https://sqrtminusone.xyz/packages/password-store-ivy/
+ Sun, 13 Feb 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/password-store-ivy/
+
+ <p>A <a href="https://www.passwordstore.org/">pass</a> frontend based on <a href="https://github.com/abo-abo/swiper#ivy">Ivy</a>, made primarily to use with <a href="https://github.com/ch11ng/exwm">EXWM</a> and <a href="https://github.com/tumashu/ivy-posframe">ivy-posframe</a>. Types fields from entries.</p>
+<p>Also take a look at Nicolas Petton’s <a href="https://github.com/NicolasPetton/pass">pass</a>, <code>password-store-ivy</code> is designed as complementary to the Nicolas’ package.</p>
+<p>This package is made with Ivy because I need some fine-tuning like actions and turning off sorting in some completions, and Ivy happens to be the completion system I’m using now.</p>
+<h2 id="installation">Installation</h2>
+<p>As the package isn’t yet available anywhere but in this repository, you can clone the repository, add it to the load-path and require the package. My preferred way is <code>use-package</code> with <code>straight</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">password-store-ivy</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> (<span style="color:#008000">:host</span> <span style="color:#19177c">github</span> <span style="color:#008000">:repo</span> <span style="color:#ba2121">"SqrtMinusOne/password-store-ivy"</span>)
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:after</span> (<span style="color:#19177c">exwm</span>))
+</span></span></code></pre></div><p>This package types stuff with <code>xdotool</code>, so you need to have that available in your <code>$PATH</code>.</p>
+<h2 id="usage">Usage</h2>
+<p>Emacs’ built-in <a href="https://www.gnu.org/software/emacs/manual/html_node/auth/The-Unix-password-store.html">password store</a> integration has to be set up.</p>
+<p>The only command is <code>M-x password-store-ivy</code>, which invokes Ivy to select an entry from the pass database. Available commands in the selection buffer:</p>
+<ul>
+<li><code>M-a</code>. Perform autotype</li>
+<li><code>M-p</code>. Type password</li>
+<li><code>M-u</code>. Type username</li>
+<li><code>M-U</code>. Type url</li>
+<li><code>M-f</code>. Select a field to type</li>
+</ul>
+<h2 id="customization">Customization</h2>
+<p>There are a few parameters that control delays:</p>
+<ul>
+<li><code>password-store-ivy-initial-wait</code> controls the initial delay before starting to type a sequence (in milliseconds)</li>
+<li><code>password-store-ivy-delay</code> controls the delay between typing characters (in milliseconds)</li>
+</ul>
+<p>There is also <code>password-store-ivy-sequences</code> that determines the sequence of actions <code>password-store-ivy</code> performs.</p>
+<p>It is an alist with the following required keys (corresponding to the basic actions):</p>
+<ul>
+<li><code>autotype</code></li>
+<li><code>password</code></li>
+<li><code>username</code></li>
+<li><code>url</code></li>
+</ul>
+<p>The values are lists of the following elements:</p>
+<ul>
+<li><code>wait</code>. Wait for <code>password-store-ivy-initial-wait</code> milliseconds</li>
+<li><code>(wait <milliseconds>)</code>. Wait for <code><milliseconds></code>.</li>
+<li><code>(key <key>)</code>. Type <code><key></code>.</li>
+<li><code>(field <field>)</code>. Type <code><field></code> of entry.</li>
+</ul>
+<p>For example, the starting values:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span><span style="color:#666">'</span>((<span style="color:#19177c">autotype</span> <span style="color:#666">.</span> (<span style="color:#19177c">wait</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">field</span> <span style="color:#666">.</span> <span style="color:#ba2121">"username"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">key</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Tab"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">field</span> <span style="color:#666">.</span> <span style="color:#19177c">secret</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">key</span> <span style="color:#666">.</span> <span style="color:#ba2121">"Return"</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">password</span> <span style="color:#666">.</span> (<span style="color:#19177c">wait</span> (<span style="color:#19177c">field</span> <span style="color:#666">.</span> <span style="color:#19177c">secret</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">username</span> <span style="color:#666">.</span> (<span style="color:#19177c">wait</span> (<span style="color:#19177c">field</span> <span style="color:#666">.</span> <span style="color:#ba2121">"username"</span>)))
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">url</span> <span style="color:#666">.</span> (<span style="color:#19177c">wait</span> (<span style="color:#19177c">field</span> <span style="color:#666">.</span> <span style="color:#ba2121">"url"</span>))))
+</span></span></code></pre></div><p>In addition to the global override, sequences can be overriden per-entry with a field called <code>sequence-<name></code>, where <code><name></code> is a key of <code>password-store-ivy-sequences</code>.</p>
+<p>For example, here is an override to press <code>Tab</code> twice:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span><pass>
+</span></span><span style="display:flex;"><span>username: thexcloud@gmail.com
+</span></span><span style="display:flex;"><span>url: <url>
+</span></span><span style="display:flex;"><span>sequence-autotype: (wait (field . "username") (key . "Tab") (key . "Tab") (field . secret) (key . "Return"))
+</span></span></code></pre></div>
+
+
+
+
+ org-journal-tags
+ https://sqrtminusone.xyz/packages/org-journal-tags/
+ Sun, 06 Feb 2022 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/org-journal-tags/
+
+ <figure><a href="https://melpa.org/#/org-journal-tags"><img src="https://melpa.org/packages/org-journal-tags-badge.svg"/></a>
+</figure>
+
+<p>A package to make sense of <del>my life</del> <a href="https://github.com/bastibe/org-journal">org-journal</a> records.</p>
+<p>The package adds the <code>org-journal:</code> link type to Org Mode. When placed in an org-journal file, the link serves as a “tag” that references one or many paragraphs of the journal or the entire section. These tags are aggregated in the database that can be queried in various ways.</p>
+<h2 id="rationale">Rationale</h2>
+<p>Journal files, by their very nature, are weakly structured. A single journal note can reference multiple entities (or none) and can itself be composed of multiple parts that have in common only the date and time when they were written. Needless to say, it’s hard to find anything in such records.</p>
+<p>This package attempts to improve the accessibility of the journal by:</p>
+<ul>
+<li>Taking advantage of temporal data, e.g. allowing to query entries in some date range.</li>
+<li>Allowing to extract (and reference) only certain parts of a particular journal entry.</li>
+<li>Compensating weak structure by with more advanced query engine.</li>
+</ul>
+<p>For instance, when I’m writing down the progress on a job project, I can leave a tag like <code>job.<project-name></code> in the paragraph(s) related to that project. Later, I can query only those paragraphs that are referenced by this particular tag. The query results can then be narrowed, for instance, to include the word “backend”, or extended with some other tag.</p>
+<p>If no tag matches the subject matter, the journal can be queried with a regular expression, e.g. by searching some regex within a specific time frame. Subsequent searches are also significantly faster than the built-in <code>org-journal</code> search functionality due to the to caching mechanism.</p>
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you normally install packages, my preferred way is <code>use-package</code> with <code>straight</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">org-journal-tags</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:after</span> (<span style="color:#19177c">org-journal</span>)
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:config</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">org-journal-tags-autosync-mode</span>))
+</span></span></code></pre></div><h2 id="basic-usage">Basic usage</h2>
+<h3 id="adding-tags">Adding tags</h3>
+<p>To add an inline tag, you can manually create a link of the following format:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[[org-journal:<tag-name>][<tag-description>]]
+</span></span></code></pre></div><p>Or run <code>M-x org-journal-tags-insert-tag</code> to insert a tag with a completion interface. The description is not aggregated and thus optional. Also, <code><tag-name></code> cannot contain <code>:</code>.</p>
+<p>The link will reference the current Org Mode paragraph. If you want to reference more paragraphs, you can set the number of paragraphs like this:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[[org-journal:<tag-name>::<number-of-paragraphs>][<tag-description>]]
+</span></span></code></pre></div><p>Run <code>M-x org-journal-tags-link-get-region-at-point</code> to select the referenced region of the buffer.</p>
+<p>To add a tag to the entire section, run <code>M-x org-journal-tags-prop-set</code>, which will create or update the <code>Tags</code> property in the property drawer of the current time section. This command features a notmuch-like UI, i.e. completing read for multiple entries, where <code>+<tag></code> adds a tag and <code>-<tag></code> deletes a tag.</p>
+<p>If you decide to rename a tag, there’s <code>M-x org-journal-tags-refactor</code>.</p>
+<h3 id="tag-kinds">Tag kinds</h3>
+<p>Tag kind is a predefined class of tag with some extra functionality. The link format fo such tags is as follows:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[[org-journal:<kind>:<tag-name>][<tag-description>]]
+</span></span><span style="display:flex;"><span>[[org-journal:<kind>:<tag-name>::<number-of-paragraphs>][<tag-description>]]
+</span></span></code></pre></div><p>If <code><kind></code> is omitted, a tag is considered “normal”.</p>
+<p>Running <code>C-u M-x org-journal-tags-insert-tag</code> will first prompt for the tag kind and then for the tag itself from the set of already used tags of that kind.</p>
+<p>Running <code>C-u C-u M-x org-journal-tags-insert-tag</code> will also first prompt for the tag kind, but then will try to invoke the kind-specific tag selection logic, if such is available. For instance, the <code>contact</code> kind will prompt the <code>org-contacts</code> database.</p>
+<p>For now, the only available tag kind is <a href="https://repo.or.cz/org-contacts.git">org-contacts</a>.</p>
+<h3 id="adding-timestamps">Adding timestamps</h3>
+<p>In addition to tags, the package also aggregates inline timestamps, i.e. timestamps that are left in the text like this:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>This is a text. This is a text with <2022-04-07 Thu> a timestamp. This is a text again.
+</span></span></code></pre></div><p>A timestamp will reference just the current paragraph.</p>
+<p>Other forms of timestamps (<code>SCHEDULED</code>, <code>DEADLINE</code>, etc.) are not supported at the moment, because this functionality is implemented well enough by <a href="https://orgmode.org/manual/Agenda-Views.html">org-agenda</a>.</p>
+<p>The envisioned use case for this functionality to leave references for the future to be seen at a particular date.</p>
+<h3 id="database">Database</h3>
+<p>The package stores tags and references to these tags in a database.</p>
+<p><code>org-journal-tags-autosync-mode</code> enables synchronizing the database at the moment of saving of the org-journal buffer. You can also run the synchronization manually:</p>
+<ul>
+<li><code>M-x org-journal-tags-process-buffer</code> to process the current buffer.</li>
+<li><code>M-x org-journal-tags-db-sync</code> to sync changed org-journal files in the filesystem.</li>
+</ul>
+<p>The same mode enables saving the database on killing Emacs, but you can always run <code>M-x org-journal-tags-db-save</code> manually.</p>
+<p><code>M-x org-journal-tags-db-unload</code> saves and unloads the database from the memory, <code>M-x org-journal-tags-db-reset</code> creates a new database.</p>
+<h3 id="status-buffer">Status buffer</h3>
+<figure><img src="https://sqrtminusone.xyz/org-journal-tags-img/status.png"/>
+</figure>
+
+<p><em>(I replaced tag names with “X” just for the screenshot)</em></p>
+<p><code>M-x org-journal-tags-status</code> opens the status buffer with some statistics about the journal and tags. Press <code>?</code> to see the available keybindings.</p>
+<p>Pressing <code>RET</code> on a tag name in the “All tags” section should open a query buffer set to return all references for this tag.</p>
+<h3 id="query-constructor">Query constructor</h3>
+<figure><img src="https://sqrtminusone.xyz/org-journal-tags-img/query.png"/>
+</figure>
+
+<p>Pressing <code>s</code> in the status buffer or running <code>M-x org-journal-tags-transient-query</code> opens a <a href="https://magit.vc/manual/transient/">transient.el</a> buffer with query settings.</p>
+<p>The options are as follows:</p>
+<ul>
+<li><strong>Include tags</strong> filters the references so that each reference had at least one of these tags.</li>
+<li><strong>Exclude tags</strong> filters the references so that each reference didn’t have any of these tags.</li>
+<li><strong>Include children</strong> includes child tags to the previous two lists.</li>
+<li><strong>Tag location</strong> can filter only section tags on inline tags.</li>
+<li><strong>Start date</strong> and <strong>End date</strong> filter the references by date.</li>
+<li><strong>Filter timestamps</strong> filters the references so that they include a timestamp.</li>
+<li><strong>Timestamp start date</strong> and <strong>Timestamp end date</strong> filter
+timestamps by their date.</li>
+<li><strong>Regex</strong> filter the references by a regular expression. It can be a string or <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Rx-Notation.html">rx</a> expression (it just has to start with <code>(rx</code> in this case).</li>
+<li><strong>Narrow to regex</strong> makes it so that each reference had only paragraphs that have a regex match.</li>
+<li><strong>Sort</strong> sorts the result in ascending order. It’s descending by default.</li>
+</ul>
+<p>Pressing <code>RET</code> or <code>e</code> executes the query. Journal files are cached, so subsequent queries within one session are much faster.</p>
+<h3 id="query-results">Query results</h3>
+<figure><img src="https://sqrtminusone.xyz/org-journal-tags-img/query-results.png"/>
+</figure>
+
+<p>After the query completes, the package opens the results buffer. Press <code>?</code> to see the available keybindings there.</p>
+<p>Pressing <code>RET</code> opens the corresponding org-journal entry.</p>
+<p>Pressing <code>s</code> opens the query constructor buffer. If opened from inside the query results, the query constructor has 4 additional options:</p>
+<table>
+<thead>
+<tr>
+<th>Command</th>
+<th>Set operation</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><strong>Union</strong></td>
+<td>old ∪ new</td>
+<td>Add records of the new query to the displayed records</td>
+</tr>
+<tr>
+<td><strong>Intersection</strong></td>
+<td>old ∩ new</td>
+<td>Leave only those records that are both displayed and in the new query</td>
+</tr>
+<tr>
+<td><strong>Difference from current</strong></td>
+<td>old \ new</td>
+<td>Exclude records of the new query from the displayed records</td>
+</tr>
+<tr>
+<td><strong>Difference to current</strong></td>
+<td>new \ old</td>
+<td>Exclude displayed records from ones of the new query</td>
+</tr>
+</tbody>
+</table>
+<p>Thus it is possible to make any query that can be described as a sequence of such set operations.</p>
+<h2 id="advanced-usage">Advanced usage</h2>
+<h3 id="automatic-tagging">Automatic tagging</h3>
+<p>org-journal provides a hook to automatically add information to the journal entries.</p>
+<p>It can be used to automatically assign tags, for instance, based on hostname. Here’s an excerpt from my configuration:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/set-journal-header</span> ()
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">org-journal-tags-prop-apply-delta</span> <span style="color:#008000">:add</span> (<span style="color:#00f">list</span> (<span style="color:#00f">format</span> <span style="color:#ba2121">"host.%s"</span> (<span style="color:#00f">system-name</span>))))
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">when</span> (<span style="color:#00f">boundp</span> <span style="color:#19177c">'my/loc-tag</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">org-journal-tags-prop-apply-delta</span> <span style="color:#008000">:add</span> (<span style="color:#00f">list</span> <span style="color:#19177c">my/loc-tag</span>))))
+</span></span><span style="display:flex;"><span>
+</span></span><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'org-journal-after-entry-create-hook</span>
+</span></span><span style="display:flex;"><span> <span style="color:#00f">#'</span><span style="color:#19177c">my/set-journal-header</span>)
+</span></span></code></pre></div><h3 id="encryption">Encryption</h3>
+<p>There are two ways how org-journal can be encrypted:</p>
+<ul>
+<li>With <a href="https://orgmode.org/manual/Org-Crypt.html">org-crypt</a>, by setting <code>org-journal-enable-encryption</code>.</li>
+<li>With <a href="https://www.gnu.org/software/emacs/manual/html_node/epa/Encrypting_002fdecrypting-gpg-files.html">epa</a>, by setting <code>org-journal-encrypt-journal</code>.</li>
+</ul>
+<p>Both ways are supported by this package (I use the first). The decryption of entries takes some time, but this is alleviated by caching.</p>
+<p>The cache is stored in the <code>org-journal-tags--files-cache</code> variable, so in principle, someone could come to your computer and inspect the value of this variable (who would ever do that?). If that’s an issue, you can do something like:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">run-with-idle-timer</span> (<span style="color:#00f">*</span> <span style="color:#666">60</span> <span style="color:#666">15</span>) <span style="color:#800">t</span> <span style="color:#00f">#'</span><span style="color:#19177c">org-journal-tags-cache-reset</span>)
+</span></span></code></pre></div><p>To clear the cache on Emacs being idle after 15 minutes.</p>
+<p>Also, as said above, <code>org-journal-tags</code> uses its own database, which is more like persistent cache for tags and references. You can encrypt it as well with <a href="https://www.gnu.org/software/emacs/manual/html_node/epa/Encrypting_002fdecrypting-gpg-files.html">epa</a> by adding <code>.gpg</code> to the <code>org-journal-tags-db-file</code> variable:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">org-journal-tags-db-file</span> (<span style="color:#00f">concat</span> <span style="color:#19177c">user-emacs-directory</span> <span style="color:#ba2121">"var/org-journal-tags/index.gpg"</span>))
+</span></span></code></pre></div><p>The database is also stored in memory in <code>org-journal-tags-db</code> variable, so once again, someone could inspect the value of the variable or just run <code>M-x org-journal-tags-status</code>.</p>
+<p>To avoid that, you can manually run <code>M-x org-journal-tags-db-unload</code> or add it to <code>run-with-idle-timer</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">run-with-idle-timer</span> (<span style="color:#00f">*</span> <span style="color:#666">60</span> <span style="color:#666">15</span>) <span style="color:#800">t</span> <span style="color:#00f">#'</span><span style="color:#19177c">org-journal-tags-db-unload</span>)
+</span></span></code></pre></div><p>If you have everything set up correctly, encrypting a file shouldn’t ask for a passphrase, so this function can be run automatically.</p>
+<h3 id="advanced-querying">Advanced querying</h3>
+<p>This package provides an API for doing queries from the Lisp code.</p>
+<p>The central function there <code>org-journal-tags-query</code>, which has an interface corresponding to the flags in the query constructor. Take a look at its docstring for more info.</p>
+<p>Also, you can use some of the following operations on the set of journal references:</p>
+<ul>
+<li><code>org-journal-tags--query-union-refs</code> - union</li>
+<li><code>org-journal-tags--query-diff-refs</code> - difference</li>
+<li><code>org-journal-tags--query-intersect-refs</code> - intersection</li>
+<li><code>org-journal-tags--query-merge-refs</code> - merge intersecting references within one set</li>
+<li><code>org-journal-tags--query-sort-refs</code> - order references by date</li>
+<li><code>org-journal-tags--string-extract-refs</code> - collect strings corresponding to references</li>
+</ul>
+<h2 id="final-notes">Final notes</h2>
+<p>This package turned out to be almost as long and complex as <a href="https://github.com/bastibe/org-journal">org-journal</a> itself, and it also introduces some new dependencies. Hence I decided it would be better off as a separate package.</p>
+<p>Also, I want to list some sources of inspiration. The database logic is heavily inspired by <a href="https://github.com/skeeto/elfeed">elfeed</a>. The UI with <a href="https://www.gnu.org/software/emacs/manual/html_mono/widget.html">Emacs widgets</a> for tags & <code>completing-read-multiple</code> and the tagging system in general is inspired by <a href="https://notmuchmail.org/">notmuch</a>. Finally, <a href="https://github.com/magit/transient">transient.el</a> and <a href="https://magit.vc/manual/magit-section.html">magit-section</a> are the UI packages that made this one possible, or at least much easier to implement.</p>
+
+
+
+
+
+ exwm-modeline
+ https://sqrtminusone.xyz/packages/exwm-modeline/
+ Wed, 22 Dec 2021 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/exwm-modeline/
+
+ <figure><a href="https://melpa.org/#/exwm-modeline"><img src="https://melpa.org/packages/exwm-modeline-badge.svg"/></a>
+</figure>
+
+<p>A modeline segment to display exwm workspaces.</p>
+<p>Here’s how it looks near the list of <a href="https://github.com/nex3/perspective-el">perspectives</a> (the segment of the current package is to the left):
+<img src="https://sqrtminusone.xyz/exwm-modeline-img/screenshot.png" alt=""></p>
+<ul>
+<li>workspaces 0 and 5 do not have any X windows</li>
+<li>workspace 1 is the current workspace</li>
+<li>workspace 2 has at least one X window.</li>
+</ul>
+<p>Features:</p>
+<ul>
+<li>Supports <code>exwm-randr</code> to display only workspaces related to the current monitor.</li>
+<li>Numbers are clickable.</li>
+</ul>
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you usually install packages, I use <a href="https://github.com/jwiegley/use-package">use-package</a> and <a href="https://github.com/raxod502/straight.el">straight.el</a>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">exwm-modeline</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:after</span> (<span style="color:#19177c">exwm</span>))
+</span></span></code></pre></div><p>Then put a call to <code>exwm-modeline-mode</code> somewhere after the moment when EXWM has been initialized, for instance:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'exwm-init-hook</span> <span style="color:#00f">#'</span><span style="color:#19177c">exwm-modeline-mode</span>)
+</span></span></code></pre></div><h2 id="customization">Customization</h2>
+<p>Set <code>exwm-modeline-randr</code> to nil to turn off filtering of workspaces by monitor.</p>
+<p>Set <code>exwm-modeline-short</code> to <code>t</code> display only the current workspace in the modeline.</p>
+<p>Set <code>exwm-modeline-display-urgent</code> to nil to turn off displaying whether a workspace has an urgent window. This will significantly decrease the number of modeline updates, which may help with performance issues.</p>
+<h2 id="credits">Credits</h2>
+<p><a href="https://github.com/nex3/perspective-el">perspective.el</a> by <a href="https://github.com/nex3">@nex3</a> was extremely instructive on how to make a modeline segment individual to a particular frame and avoid recalculating it too often.</p>
+<p><a href="https://github.com/elken/doom-modeline-exwm">doom-modeline-exwm</a> by <a href="https://github.com/elken">@elken</a> also was a source of inspiration.</p>
+
+
+
+
+
+ perspective-exwm
+ https://sqrtminusone.xyz/packages/perspective-exwm/
+ Wed, 01 Dec 2021 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/perspective-exwm/
+
+ <figure><a href="https://melpa.org/#/perspective-exwm"><img src="https://melpa.org/packages/perspective-exwm-badge.svg"/></a>
+</figure>
+
+<p>A couple of tricks and fixes to make using <a href="https://github.com/ch11ng/exwm">EXWM</a> and <a href="https://github.com/nex3/perspective-el">perspective.el</a> a better experience.</p>
+<h2 id="installation">Installation</h2>
+<p>This package is available on MELPA. Install it however you usually install packages, I use <a href="https://github.com/jwiegley/use-package">use-package</a> and <a href="https://github.com/raxod502/straight.el">straight.el</a>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">perspective-exwm</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>)
+</span></span></code></pre></div><p>Or clone the repository, add the package to the <code>load-path</code> and load it with <code>require</code>.</p>
+<p>The package provides a minor mode, <code>perspective-exwm-mode</code>, which is meant to be loaded before <code>exwm-init</code>. For instance, if you use <code>use-package</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">exwm</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:config</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">...</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">perspective-exwm-mode</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">exwm-init</span>))
+</span></span></code></pre></div><h2 id="usage-and-details">Usage and details</h2>
+<ul>
+<li>
+<p><code>perspective-exwm-mode</code><br />
+The mode does a couple of things:</p>
+<ul>
+<li>advises away a bug with half-killing the current perspective when closing a floating window. <del>I haven’t tested this as thoroughly</del> I haven’t run into this issue for nearly a month, so it seems to be fixed. But there’s <code>M-x perspective-exwm-revive-perspectives</code> if the problem arises anyway.</li>
+<li>fixes a bug with running <code>persp-set-buffer</code> on an EXWM buffer that was moved between workspaces by advising <code>persp-buffer-in-other-p</code>.</li>
+<li>fixes a bug with <code>persp-set-buffer</code> copying all the perspectives from other workspaces to the current one.</li>
+<li>adjusts the name of the initial perspective in the new workspace. It tries to get the name from the <code>perspective-exwm-override-initial-name</code> variable and fallbacks to <code>main-<index></code>.</li>
+</ul>
+<p>For the last point, I have the following in my configuration:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">perspective-exwm-override-initial-name</span>
+</span></span><span style="display:flex;"><span> <span style="color:#666">'</span>((<span style="color:#666">0</span> <span style="color:#666">.</span> <span style="color:#ba2121">"misc"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#666">1</span> <span style="color:#666">.</span> <span style="color:#ba2121">"core"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#666">2</span> <span style="color:#666">.</span> <span style="color:#ba2121">"browser"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#666">3</span> <span style="color:#666">.</span> <span style="color:#ba2121">"comms"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#666">4</span> <span style="color:#666">.</span> <span style="color:#ba2121">"dev"</span>)))
+</span></span></code></pre></div><p>Having distinct perspective names between frames also serves a purpose, because otherwise there are issues with multiple perspectives sharing the same scratch buffer.</p>
+</li>
+<li>
+<p><code>M-x perspective-exwm-cycle-exwm-buffers-forward</code>, <code>perspective-exwm-cycle-exwm-buffers-backward</code><br />
+Cycle EXWM buffers in the current perspective.</p>
+<figure><img src="https://sqrtminusone.xyz/perspective-exwm-img/cycle-buffers.png"/>
+ </figure>
+
+<p>The buffer highlighted in yellow is the current one, the buffer highlighted in blue is shown in another window of the perspective so it will be omitted from the cycle.</p>
+<p>Set <code>perspective-exwm-get-exwm-buffer-name</code> to customize the displayed name, by default it’s <code>exwm-class-name</code>.</p>
+</li>
+<li>
+<p><code>M-x perspective-exwm-cycle-all-buffers-forward</code>, <code>perspective-exwm-cycle-exwm-all-backward</code><br />
+The same as above, but not restricted to EXWM buffers.</p>
+</li>
+<li>
+<p><code>M-x perspective-exwm-switch-perspective</code><br />
+Select a perspective from the list of all perspectives on all workspaces.</p>
+<figure><img src="https://sqrtminusone.xyz/perspective-exwm-img/switch-perspective.png"/>
+ </figure>
+
+</li>
+<li>
+<p><code>M-x perspective-exwm-copy-to-workspace</code><br />
+Copy the current perspective to another EXWM workspace.</p>
+</li>
+<li>
+<p><code>M-x perspective-exwm-move-to-workspace</code><br />
+Move the current perspective to another EXWM workspace.</p>
+</li>
+<li>
+<p><code>perspective-exwm-assign-windows</code><br />
+A handy function to move the current window to a given workspace and/or perspective. Example usage:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">defun</span> <span style="color:#19177c">my/exwm-configure-window</span> ()
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">interactive</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#008000">pcase</span> <span style="color:#19177c">exwm-class-name</span>
+</span></span><span style="display:flex;"><span> ((<span style="color:#008000">or</span> <span style="color:#ba2121">"Firefox"</span> <span style="color:#ba2121">"Nightly"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">perspective-exwm-assign-window</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:workspace-index</span> <span style="color:#666">2</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:persp-name</span> <span style="color:#ba2121">"browser"</span>))
+</span></span><span style="display:flex;"><span> (<span style="color:#ba2121">"Alacritty"</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">perspective-exwm-assign-window</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:persp-name</span> <span style="color:#ba2121">"term"</span>))
+</span></span><span style="display:flex;"><span> ((<span style="color:#008000">or</span> <span style="color:#ba2121">"VK"</span> <span style="color:#ba2121">"Slack"</span> <span style="color:#ba2121">"Discord"</span> <span style="color:#ba2121">"TelegramDesktop"</span>)
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">perspective-exwm-assign-window</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:workspace-index</span> <span style="color:#666">3</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:persp-name</span> <span style="color:#ba2121">"comms"</span>))))
+</span></span><span style="display:flex;"><span>
+</span></span><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'exwm-manage-finish-hook</span> <span style="color:#00f">#'</span><span style="color:#19177c">my/exwm-configure-window</span>)
+</span></span></code></pre></div></li>
+</ul>
+<h2 id="known-issues">Known issues</h2>
+<ul>
+<li><code>perspective-exwm-move-to-workspace</code> kills X windows in the perspective it tries to move. Have no idea how to fix this at the moment.</li>
+</ul>
+
+
+
+
+
+ pomm.el
+ https://sqrtminusone.xyz/packages/pomm/
+ Fri, 05 Nov 2021 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/pomm/
+
+ <figure><a href="https://melpa.org/#/pomm"><img src="https://melpa.org/packages/pomm-badge.svg"/></a>
+</figure>
+
+<p>Implementation of <a href="https://en.wikipedia.org/wiki/Pomodoro_Technique">Pomodoro</a> and <a href="https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work">Third Time</a> techniques for Emacs.</p>
+<figure><img src="https://sqrtminusone.xyz/pomm-img/screenshot.png"/>
+</figure>
+
+<p>Features:</p>
+<ul>
+<li>Managing the timer with the excellent <a href="https://github.com/magit/transient/blob/master/lisp/transient.el">transient.el</a>.</li>
+<li>Persistent state between Emacs sessions.
+The timer state isn’t reset if you close Emacs. If necessary, the state file can be synchronized between machines.</li>
+<li>History.
+History of the timer can be stored in a CSV file. Eventually, I want to join this with <a href="https://activitywatch.net/">other activity data</a> to see if the state of the timer changes how I use the computer.</li>
+</ul>
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you usually install Emacs packages, e.g.</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>M-x package-install pomm
+</span></span></code></pre></div><p>My preferred way is <code>use-package</code> with <code>straight.el</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">pomm</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:commands</span> (<span style="color:#19177c">pomm</span> <span style="color:#19177c">pomm-third-time</span>))
+</span></span></code></pre></div><p>Or you can clone the repository, add the package to the <code>load-path</code> and load it with <code>require</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">require</span> <span style="color:#19177c">'pomm</span>)
+</span></span></code></pre></div><p>The package requires Emacs 27.1 because the time API of the previous versions is kinda crazy and 27.1 has <code>time-convert</code>.</p>
+<h2 id="usage">Usage</h2>
+<h3 id="pomodoro">Pomodoro</h3>
+<p>Run <code>M-x pomm</code> to open the transient buffer.</p>
+<p>The listed commands are rather self-descriptive and match the Pomodoro ideology.</p>
+<p>The timer can have 3 states:</p>
+<ul>
+<li><strong>Stopped</strong>. Can be started with “s” or <code>M-x pomm-start</code>. A new iteration of the timer will be started.</li>
+<li><strong>Paused</strong>. Can be continuted with “s” / <code>M-x pomm-start</code> or stopped competely with “S” / <code>M-x pomm-stop</code>.</li>
+<li><strong>Running</strong>. Can be paused with “p” / <code>M-x pomm-pause</code> or stopped with “S” / <code>M-x pomm-stop</code>.</li>
+</ul>
+<p>The state of the timer can be reset with “R” or <code>M-x pomm-reset</code>.</p>
+<p>“u” updates the transient buffer. The update is manual because I didn’t figure out how to automate this, and I think this is not <em>really</em> necessary.</p>
+<p>With “r” or <code>M-x pomm-set-context</code> you can set the current “context”, that is some description of the task you are currently working on. This description will show up in history and in the csv file. Also, <code>M-x pomm-start-with-context</code> will prompt for the context and then start the timer.</p>
+<h3 id="third-time">Third Time</h3>
+<p>Run <code>M-x pomm-third-time</code> to open the transient buffer for the Third Time technique.</p>
+<figure><img src="https://sqrtminusone.xyz/pomm-img/screenshot-tt.png"/>
+</figure>
+
+<p>Essentially, the techique is designed aroud the formula:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Time of break = 1/3 x Time of work.
+</span></span></code></pre></div><p>I.e. you work as long as you want or need, and then take a break with the maximum duration <code>1/3</code> of the time worked. If you take a shorter break, the remaining break time is saved and added to the next break within the same session. <a href="https://www.lesswrong.com/posts/RWu8eZqbwgB9zaerh/third-time-a-better-way-to-work">Here is a more detailed explanation</a>.</p>
+<p>The Third Time timer can have 2 states:</p>
+<ul>
+<li><strong>Stopped</strong>. Can be started with “s” or <code>M-x pomm-third-time-start</code>.</li>
+<li><strong>Running</strong>. Can be stopped with “S” or <code>M-x pomm-third-time-stop</code>. This resets the accumulated break time.</li>
+</ul>
+<p>Use “b” or <code>M-x pomm-third-time-switch</code> to switch the current period type (work or break). If the break time runs out, the timer automatically switches to work.</p>
+<h2 id="customization">Customization</h2>
+<p>Some settings are available in the transient buffer, but you can customize the relevant variables to make them permanent. Check <code>M-x customize-group</code> <code>pomm</code> and <code>M-x customize-group pomm-third-time</code> for more information.</p>
+<h3 id="alerts">Alerts</h3>
+<p>The package sends alerts via <code>alert.el</code>. The default style of alert is a plain <code>message</code>, but if you want an actual notification, set <code>alert-default-style</code> accordingly:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">alert-default-style</span> <span style="color:#19177c">'libnotify</span>)
+</span></span></code></pre></div><h3 id="sounds">Sounds</h3>
+<p>By default sounds are disabled. Set <code>pomm-audio-enabled</code> to <code>t</code> to toggle them. Set <code>pomm-audio-tick-enabled</code> to <code>t</code> if you want the ticking sound.</p>
+<p>This functionality needs <code>pomm-audio-player-executable</code> to be set so that the program could be invoked like: <code><executable> /path/to/sound.wav</code>.</p>
+<p>The package ships with some built-it sounds, which you can replace by customizing the <code>pomm-audio-files</code> variable.</p>
+<h3 id="modeline">Modeline</h3>
+<p>If you want the timer to display in the modeline, activate the <code>pomm-mode-line-mode</code> minor mode.</p>
+<h3 id="polybar-module">Polybar module</h3>
+<p>If you want to display the Pomodoro status in something like polybar, you can add the following lines to your config:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'pomm-on-tick-hook</span> <span style="color:#19177c">'pomm-update-mode-line-string</span>)
+</span></span><span style="display:flex;"><span>(<span style="color:#19177c">add-hook</span> <span style="color:#19177c">'pomm-on-status-changed-hook</span> <span style="color:#19177c">'pomm-update-mode-line-string</span>)
+</span></span></code></pre></div><p>Create a script like this:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#008000;font-weight:bold">if</span> ps -e | grep emacs >> /dev/null; <span style="color:#008000;font-weight:bold">then</span>
+</span></span><span style="display:flex;"><span> emacsclient --eval <span style="color:#ba2121">"(if (boundp 'pomm-current-mode-line-string) pomm-current-mode-line-string \"\") "</span> | xargs <span style="color:#008000">echo</span> -e
+</span></span><span style="display:flex;"><span><span style="color:#008000;font-weight:bold">fi</span>
+</span></span></code></pre></div><p>And add a polybar module definition to your polybar config:</p>
+<pre tabindex="0"><code class="language-conf-windows" data-lang="conf-windows">[module/pomm]
+type = custom/script
+exec = /home/pavel/bin/polybar/pomm.sh
+interval = 1
+</code></pre><h3 id="state-file-location">State file location</h3>
+<p>To implement pesistence between Emacs sessions, the package stores its state in the following files:</p>
+<ul>
+<li><code>pomm-state-file-location</code>, <code>.emacs.d/pomm</code> by default</li>
+<li><code>pomm-third-time-state-file-location</code>, <code>/.emacs.d/pomm-third-time</code> by default</li>
+</ul>
+<p>Set these paths however like.</p>
+<h3 id="history">History</h3>
+<p>If you set the <code>pomm-csv-history-file</code> (and/or <code>pomm-third-time-csv-history-file</code>) variable, the package will log its history in CSV format. Just keep in mind that the parent directory has to exist.</p>
+<p>The file for the Pomodoro technique has the following columns:</p>
+<ul>
+<li><code>timestamp</code></li>
+<li><code>status</code> (<code>stopped</code>, <code>paused</code> or <code>running</code>, according to the <a href="#usage-1">usage</a> section)</li>
+<li><code>kind</code> (<code>work</code>, <code>short-break</code>, <code>long-break</code> or <code>nil</code>)</li>
+<li><code>iteration</code></li>
+<li><code>context</code></li>
+</ul>
+<p>One for the Third Time technique has an extra column called <code>break-time-remaining</code>.</p>
+<p>A new entry is written after a particular state of the timer comes into being.</p>
+<p>To customize timestamp, set the <code>pomm-csv-history-file-timestamp-format</code> variable. For example, for traditional <code>YYYY-MM-DD HH:mm:ss</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">pomm-csv-history-file-timestamp-format</span> <span style="color:#ba2121">"%F %T"</span>)
+</span></span></code></pre></div><p>The format is the same as in <code>format-time-string</code>.</p>
+<h2 id="alternatives">Alternatives</h2>
+<p>There is a number of packages with a similar purpose, here is a rough comparison of features:</p>
+<table>
+<thead>
+<tr>
+<th>Package</th>
+<th>3rd party integrations</th>
+<th>Control method (1)</th>
+<th>Persistent history</th>
+<th>Persistent state</th>
+<th>Notifications</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><a href="https://github.com/SqrtMinusOne/pomm.el">pomm.el</a></td>
+<td>-</td>
+<td>transient.el</td>
+<td>CSV</td>
+<td>+</td>
+<td>alert.el + sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/marcinkoziej/org-pomodoro/tree/master">org-pomodoro</a></td>
+<td>Org Mode!</td>
+<td>via Org commands</td>
+<td>via Org mode</td>
+<td>-</td>
+<td>alert.el + sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/TatriX/pomidor/">pomidor</a></td>
+<td>-</td>
+<td>self-cooked interactive buffer</td>
+<td>custom delimited format?</td>
+<td>+, but saving on-demand</td>
+<td>alert.el + sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/baudtack/pomodoro.el/">pomodoro.el</a></td>
+<td>-</td>
+<td>-</td>
+<td>-</td>
+<td>-</td>
+<td>notifications.el + sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/konr/tomatinho/">tomatinho</a></td>
+<td>-</td>
+<td>self-cooked interactive buffer</td>
+<td>-</td>
+<td>-</td>
+<td>message + sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/ferfebles/redtick">redtick</a></td>
+<td>-</td>
+<td>mode-line icon</td>
+<td>+</td>
+<td>-</td>
+<td>sounds</td>
+</tr>
+<tr>
+<td><a href="https://github.com/abo-abo/gtk-pomodoro-indicator">gtk-pomodoro-indicator</a></td>
+<td>GTK panel</td>
+<td>CLI</td>
+<td>-</td>
+<td>-, but the program is independent from Emacs</td>
+<td>GTK notifications</td>
+</tr>
+</tbody>
+</table>
+<p>Be sure to check those out if this one doesn’t quite fit your workflow!</p>
+<p>(1) Means of timer control with exception of Emacs interactive commands</p>
+<p>Also take a look at <a href="https://github.com/telotortium/org-pomodoro-third-time">org-pomodoro-third-time</a>, which adapts <code>org-pomodoro</code> for the Third Time technique.</p>
+<h2 id="p-dot-s-dot">P.S.</h2>
+<p>The package name is not an abbreviation. I just hope it doesn’t mean something horrible in some language I don’t know.</p>
+<p>The sounds are made by Mike Koening under <a href="https://creativecommons.org/licenses/by/3.0/legalcode">CC BY 3.0</a>.</p>
+
+
+
+
+
+ lyrics-fetcher.el
+ https://sqrtminusone.xyz/packages/lyrics-fetcher/
+ Sat, 14 Aug 2021 00:00:00 +0000
+
+ https://sqrtminusone.xyz/packages/lyrics-fetcher/
+
+ <figure><a href="https://melpa.org/#/lyrics-fetcher"><img src="https://melpa.org/packages/lyrics-fetcher-badge.svg"/></a>
+</figure>
+
+<p>A package to fetch song lyrics and album covers. Integrates with EMMS.</p>
+<figure><img src="https://sqrtminusone.xyz/lyrics-fetcher-img/screenshot.png"/>
+</figure>
+
+<p>The available backends are <a href="https://genius.com">genius.com</a> and <a href="https://music.163.com/">music.163.com</a>.</p>
+<h2 id="installation">Installation</h2>
+<p>The package is available on MELPA. Install it however you normally install packages, I prefer <code>use-package</code> with <code>straight</code>:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">use-package</span> <span style="color:#19177c">lyrics-fetcher</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:straight</span> <span style="color:#800">t</span>
+</span></span><span style="display:flex;"><span> <span style="color:#008000">:after</span> (<span style="color:#19177c">emms</span>))
+</span></span></code></pre></div><p>Install <a href="https://imagemagick.org/index.php">imagemagick</a> if you want to download covers.</p>
+<p>If you want to use the genius backend, you have to set <a href="https://docs.genius.com/">genius.com</a> client access token. To do that, <a href="https://genius.com/api-clients/new">create a new client,</a> click “Generate Access Token” and put the result to the <code>lyrics-fetcher-genius-access-token</code> variable. I do this with password-store:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#008000">setq</span> <span style="color:#19177c">lyrics-fetcher-genius-access-token</span>
+</span></span><span style="display:flex;"><span> (<span style="color:#19177c">password-store-get</span> <span style="color:#ba2121">"My_Online/APIs/genius.com"</span>))
+</span></span></code></pre></div><p>But of course, you can just hardcode the string.</p>
+<h2 id="usage">Usage</h2>
+<p>Available commands:</p>
+<ul>
+<li>
+<p><code>M-x lyrics-fetcher-show-lyrics</code> - show lyrics for the current playing track.</p>
+<p>The resulting lyric files are saved to the <code>lyrics-fetcher-lyrics-folder</code> and have the <code>lyrics-fetcher-lyrics-file-extension</code> extension. The folder will be created if it doesn’t exist.</p>
+<p>By default, the function opens an already saved lyrics file if one exists, otherwise tries to fetch the lyrics.</p>
+<p>If called with <code>C-u</code>, then tries to fetch the text regardless of the latter.</p>
+<p>If called with <code>C-u C-u</code>, prompts the user to select a matching song. That is helpful when there are multiple songs with similar names, and the top one isn’t the right one.</p>
+<p>If called with <code>C-u C-u C-u</code>, edit the search query in minibuffer before sending. This is helpful when there is extra information in the song title which prevents the API from finding the song.</p>
+</li>
+<li>
+<p><code>M-x lyrics-fetcher-show-lyrics-query</code> - fetch lyrics by a text query.</p>
+<p>Modified by <code>C-u</code> the same way as <code>lyrics-fetcher-show-lyrics</code>.</p>
+</li>
+<li>
+<p><code>M-x lyrics-fetcher-use-backend</code> - select a backend to use.</p>
+</li>
+</ul>
+<p>EMMS integration:</p>
+<ul>
+<li>
+<p><code>M-x lyrics-fetcher-emms-browser-show-at-point</code> - fetch data for the current point in EMMS browser.</p>
+<p>If the point contains just one song, it will be fetched the usual way and lyrics will be shown upon successful completion.</p>
+<p>If the point contains many songs (e.g. it’s an album), the lyrics will be fetched consequentially for every song. The process then will stop at the first failure.</p>
+<p>Modified by <code>C-u</code> the same way as <code>lyrics-fetcher-show-lyrics</code>.</p>
+</li>
+<li>
+<p><code>M-x lyrics-fetcher-emms-browser-fetch-covers-at-point</code> - fetch album covers for the current point in the EMMS browser.</p>
+<p>This functionality requires songs’ directories to be grouped by albums, i.e. one album per one folder.</p>
+<p>The files will be saved to the folder with names like “cover_small.jpg”, “cover_med.jpg”, “cover_large.jpg”.</p>
+<p>You can customize the sizes via the <code>lyrics-fetcher-small-cover-size</code> and <code>lyrics-fetcher-medium-cover-size</code> variables.</p>
+<p>Modified by <code>C-u</code> the same way as <code>lyrics-fetcher-show-lyrics</code>.</p>
+</li>
+<li>
+<p><code>M-x lyrics-fetcher-emms-browser-open-large-cover-at-point</code> - open large_cover for the current point in EMMS browser.</p>
+</li>
+<li>
+<p><code>M-x lyrics-fetcher-lyrics-catchup</code> - feed the LRC file for the current track to EMMS.</p>
+</li>
+</ul>
+<p>Lyric view mode keybindings:</p>
+<ul>
+<li><code>q</code> - close the lyrics buffer</li>
+<li><code>r</code> - refetch the lyrics in the buffer</li>
+</ul>
+<h2 id="available-backends">Available backends</h2>
+<p>As of now, the available backends are <code>genius</code> and <code>neteasecloud</code> (thanks <a href="https://github.com/Elilif">@Elilif</a>). Backends can be switched with <code>M-x lyrics-fetcher-use-backend</code>, or from the Lisp code:</p>
+<div class="highlight"><pre tabindex="0" style=";-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-emacs-lisp" data-lang="emacs-lisp"><span style="display:flex;"><span>(<span style="color:#19177c">lyrics-fetcher-use-backend</span> <span style="color:#19177c">'neteasecloud</span>)
+</span></span></code></pre></div><p>The <code>genius</code> backend fetches lyrics in a simple text format.</p>
+<p><code>neteasecloud</code> fetches in the <a href="https://en.wikipedia.org/wiki/LRC_(file_format)">LRC</a> format, which contains timestamps for each line of the lyrics text.</p>
+<p>LRC files can also be read by <code>emms-lyrics</code>. <code>lyrics-fetcher-use-backend</code> sets up <code>lyrics-fetcher</code> and EMMS variables so that EMMS could see the lyrics, downloaded by <code>lyrics-fetcher</code>. Running <code>M-x emms-lyrics</code> then should enable lyric display for newly played tracks, or you can run <code>M-x lyrics-fetcher-lyrics-catchup</code> to manually feed the current LRC file to EMMS.</p>
+<h2 id="customization-and-extension">Customization and extension</h2>
+<h3 id="lyrics-file-naming-and-location">Lyrics file naming and location</h3>
+<p>As was outlined above, lyrics files are saved to <code>lyrics-fetcher-lyrics-folder</code> and have an extension set in <code>lyrics-fetcher-lyrics-file-extension</code>.</p>
+<p>Take a look at the <code>lyrics-fetcher-format-song-name-method</code> and <code>lyrics-fetcher-format-file-name-method</code> variables if you want to customize the lyrics buffer and file naming.</p>
+<p>Also note that integration with <code>emms-lyrics</code> requires these variables to be set with <code>lyrics-fetcher-use-backend</code></p>
+<h3 id="using-other-player-than-emms">Using other player than EMMS</h3>
+<p>To use another player, customize <code>lyrics-fetcher-current-track-method</code>.</p>
+<p>This variable contains a function that returns the current playing track. The return format has to be either a string or (recommended) an EMMS-like alist, which has to have the following fields:</p>
+<ul>
+<li><code>info-artist</code> or <code>info-albumartist</code></li>
+<li><code>info-title</code></li>
+</ul>
+<h3 id="adding-another-backend">Adding another backend</h3>
+<p>A function to perform the lyric fetching is set in <code>lyrics-fetcher-fetch-method</code>.</p>
+<p>The function has to receive 3 arguments:</p>
+<ul>
+<li><code>track</code> - a string or alist, as outlined <a href="#using-other-player-than-emms-1">above</a>.</li>
+<li><code>callback</code> - the function which has to be called with the resulting lyrics string</li>
+<li><code>sync</code> - if non-nil, inquire the user about the possible choices. This is called <code>sync</code> because then it is reasonable to perform the request synchronously, as otherwise, it won’t be nice to suddenly throw a prompt at the user.</li>
+</ul>
+<p>The album cover fetching is similar. The corresponding function is set in <code>lyrics-fetcher-download-cover-method</code> and has to receive the following parameters:</p>
+<ul>
+<li><code>track</code> - as above</li>
+<li><code>callback</code> - has to be called with the path to the resulting file. This file should be named <code>cover_large.<extension></code>.</li>
+<li><code>folder</code> - where the file has to be put</li>
+<li><code>sync</code> - as above.</li>
+</ul>
+<p>The first argument is <code>track</code> because in EMMS all the required information is stored in tracks, and album data is deduced from tracks. So this package just takes a sample track in the album.</p>
+<h2 id="troubleshooting">Troubleshooting</h2>
+<p>I’ve noticed that Genius can give pages with different DOMs to different people. If you have an empty buffer instead of lyrics, please attach the <code>curl-cookie-jar</code> file to the issue. It usually resides in <code>.emacs.d/request</code>.</p>
+
+
+
+
+
+
diff --git a/packages/lyrics-fetcher/index.html b/packages/lyrics-fetcher/index.html
new file mode 100644
index 0000000..6e11a8a
--- /dev/null
+++ b/packages/lyrics-fetcher/index.html
@@ -0,0 +1,236 @@
+
+
+
+
+
+ lyrics-fetcher.el
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ lyrics-fetcher.el
+
+
+
+
+
+
+ lyrics-fetcher.el
+
+
+
+
+
+
+
+
A package to fetch song lyrics and album covers. Integrates with EMMS.
Install imagemagick if you want to download covers.
+
If you want to use the genius backend, you have to set genius.com client access token. To do that, create a new client, click “Generate Access Token” and put the result to the lyrics-fetcher-genius-access-token variable. I do this with password-store:
M-x lyrics-fetcher-show-lyrics - show lyrics for the current playing track.
+
The resulting lyric files are saved to the lyrics-fetcher-lyrics-folder and have the lyrics-fetcher-lyrics-file-extension extension. The folder will be created if it doesn’t exist.
+
By default, the function opens an already saved lyrics file if one exists, otherwise tries to fetch the lyrics.
+
If called with C-u, then tries to fetch the text regardless of the latter.
+
If called with C-u C-u, prompts the user to select a matching song. That is helpful when there are multiple songs with similar names, and the top one isn’t the right one.
+
If called with C-u C-u C-u, edit the search query in minibuffer before sending. This is helpful when there is extra information in the song title which prevents the API from finding the song.
+
+
+
M-x lyrics-fetcher-show-lyrics-query - fetch lyrics by a text query.
+
Modified by C-u the same way as lyrics-fetcher-show-lyrics.
+
+
+
M-x lyrics-fetcher-use-backend - select a backend to use.
+
+
+
EMMS integration:
+
+
+
M-x lyrics-fetcher-emms-browser-show-at-point - fetch data for the current point in EMMS browser.
+
If the point contains just one song, it will be fetched the usual way and lyrics will be shown upon successful completion.
+
If the point contains many songs (e.g. it’s an album), the lyrics will be fetched consequentially for every song. The process then will stop at the first failure.
+
Modified by C-u the same way as lyrics-fetcher-show-lyrics.
+
+
+
M-x lyrics-fetcher-emms-browser-fetch-covers-at-point - fetch album covers for the current point in the EMMS browser.
+
This functionality requires songs’ directories to be grouped by albums, i.e. one album per one folder.
+
The files will be saved to the folder with names like “cover_small.jpg”, “cover_med.jpg”, “cover_large.jpg”.
+
You can customize the sizes via the lyrics-fetcher-small-cover-size and lyrics-fetcher-medium-cover-size variables.
+
Modified by C-u the same way as lyrics-fetcher-show-lyrics.
+
+
+
M-x lyrics-fetcher-emms-browser-open-large-cover-at-point - open large_cover for the current point in EMMS browser.
+
+
+
M-x lyrics-fetcher-lyrics-catchup - feed the LRC file for the current track to EMMS.
+
+
+
Lyric view mode keybindings:
+
+
q - close the lyrics buffer
+
r - refetch the lyrics in the buffer
+
+
Available backends
+
As of now, the available backends are genius and neteasecloud (thanks @Elilif). Backends can be switched with M-x lyrics-fetcher-use-backend, or from the Lisp code:
+
(lyrics-fetcher-use-backend'neteasecloud)
+
The genius backend fetches lyrics in a simple text format.
+
neteasecloud fetches in the LRC format, which contains timestamps for each line of the lyrics text.
+
LRC files can also be read by emms-lyrics. lyrics-fetcher-use-backend sets up lyrics-fetcher and EMMS variables so that EMMS could see the lyrics, downloaded by lyrics-fetcher. Running M-x emms-lyrics then should enable lyric display for newly played tracks, or you can run M-x lyrics-fetcher-lyrics-catchup to manually feed the current LRC file to EMMS.
+
Customization and extension
+
Lyrics file naming and location
+
As was outlined above, lyrics files are saved to lyrics-fetcher-lyrics-folder and have an extension set in lyrics-fetcher-lyrics-file-extension.
+
Take a look at the lyrics-fetcher-format-song-name-method and lyrics-fetcher-format-file-name-method variables if you want to customize the lyrics buffer and file naming.
+
Also note that integration with emms-lyrics requires these variables to be set with lyrics-fetcher-use-backend
+
Using other player than EMMS
+
To use another player, customize lyrics-fetcher-current-track-method.
+
This variable contains a function that returns the current playing track. The return format has to be either a string or (recommended) an EMMS-like alist, which has to have the following fields:
+
+
info-artist or info-albumartist
+
info-title
+
+
Adding another backend
+
A function to perform the lyric fetching is set in lyrics-fetcher-fetch-method.
callback - the function which has to be called with the resulting lyrics string
+
sync - if non-nil, inquire the user about the possible choices. This is called sync because then it is reasonable to perform the request synchronously, as otherwise, it won’t be nice to suddenly throw a prompt at the user.
+
+
The album cover fetching is similar. The corresponding function is set in lyrics-fetcher-download-cover-method and has to receive the following parameters:
+
+
track - as above
+
callback - has to be called with the path to the resulting file. This file should be named cover_large.<extension>.
+
folder - where the file has to be put
+
sync - as above.
+
+
The first argument is track because in EMMS all the required information is stored in tracks, and album data is deduced from tracks. So this package just takes a sample track in the album.
+
Troubleshooting
+
I’ve noticed that Genius can give pages with different DOMs to different people. If you have an empty buffer instead of lyrics, please attach the curl-cookie-jar file to the issue. It usually resides in .emacs.d/request.
Emacs package for working with micromamba environments.
+
mamba is a reimplementation of the conda package manager in C++. mamba is notably much faster and essentially compatible with conda, so it also works with conda.el. micromamba, however, implements only a subset of mamba commands, and as such requires a separate integration.
+
Installation
+
The package is available on MELPA. Install it however you normally install packages, I prefer use-package and straight:
+
(use-packagemicromamba
+:straightt)
+
Or clone the repository, add it to the load-path and require the package.
+
If your micromamba binary is located in some place unknown to executable-find, set the micromamba-executable variable.
+
If you are running shells (e.g. vterm) from Emacs, you probably want to set auto_activate_base in your .condarc or .mambarc, because the shells are launched in the correct environment anyway.
+
Usage
+
The package has two entrypoints:
+
+
M-x micromamba-activate - activate the environment
+
M-x micromamba-deactivate - deactivate the environment
+
+
micromamba-activate prompts for the environment (by parsing micromamba env list). If some environments have duplicate names, these names are replaced by full paths.
+
I’ve noticed that micromamba also sees conda environments, so migrating from conda was rather painless for me.
+
Implementation notes
+
I initially wanted to extend conda.el, but decided it would be counterproductive for a few reasons.
+
First, conda is rather slow, so conda.el does various tricks to avoid calling the conda executable. For instance, it gets the environment list from scanning the anaconda home directory instead of running conda env list. This is really not necessary with micromamba, which is written in C++.
+
Second, and more importantly, conda.el relies heavily on passing shell.posix+json to conda. micromamba doesn’t support that. It supports the --json flag in some places, but not in the activate command, so I have to parse the output of micromamba shell -s bash activate and micromamba shell -s bash deactivate to get the environment configuration.
+
This also means the package most likely won’t work out-of-the-box on Windows.
Aggregate org-clock records and display the results in an interactive buffer. The records are grouped by predicates such as file name, their outline path in the file, etc. Each record is placed in a tree structure; each node of the tree shows the total time spent in that node and its children. The top-level node shows the total time spent in all records found by the query.
+
+
+
+
Installation
+
The package isn’t yet available anywhere but in this repository. My preferred way for such cases is use-package and straight.el:
Alternatively, clone the repository, add it to the load-path, and require the package.
+
Usage
+
Run M-x org-clock-agg to open the interactive buffer (as depicted in the screenshot above).
+
The interactive buffer provides the following controls:
+
+
Files: Specifies the org files from which to select (defaults to org-agenda).
+
Date from and To: Define the date range.
+
Group by: Determines how org-clock records are grouped.
+
Show elements: Whether to display raw org-clock records in each node.
+
Add “Ungrouped”: Option to include the “Ungrouped” node. This is particularly useful with custom grouping predicates.
+
+
Press [Refresh] to update the buffer. The initial search might take some time, but subsequent searches are generally faster due to the caching mechanism employed by org-ql.
+
The buffer uses outline-mode to display the tree, so each node becomes an outline-mode header. Refer to the linked manual for available commands/keybindings, or, if you use evil-mode, check the relevant evil-collection file.
+
Files
+
By default, the package selects org-clock records from (org-agenda-files). Additional options can be included by customizing the org-clock-agg-files-preset variable. For instance:
Note that after updating any of these variables, you’ll need to reopen the *org-clock-agg* buffer to view the changes.
+
Alternatively, you can directly specify the list of files within the buffer by selecting “Custom list” in the “Files” control.
+
Date Range
+
Dates can take the following values:
+
+
A number: Represents a relative number of days from the current date. E.g. the default value of -7 to 0 menas the previous week up to today.
+
A date string in the format YYYY-MM-DD HH:mm:ss, with or without the time part.
+
+
By default, the interval is inclusive. For instance, specifying an interval like 2023-12-12 .. 2023-12-13 includes all records from 2023-12-12 00:00:00 to 2023-12-13 23:59:59.
+
Group By
+
Records are grouped based on the sequence of grouping predicates.
+
For example, with the following content in tasks.org:
The org-clock-agg-node-format variable determines the formatting of individual tree nodes. This uses a format string that with the following format specifiers avaiable:
+
+
%t: Node title with the level prefix, truncated to title-width characters (refer to below)
+
%c: Name of the grouping function that generated the node
+
%z: Time spent in the node, formatted according to org-clock-agg-duration-format.
+
%s: Time share of the node against the parent node
+
%S: Time share of the node against the top-level node
+
+
The default value is:
+
%-%(+ title-width)t %20c %8z
+
Where %(+ title-width) is (- (window-width) org-clock-agg-node-title-width-delta), with the default value of the latter set to 40.
+
Thefore, in the default configuration, the node title is truncated to title-width characters, while 40 symbols are allocated for the rest of the header, i.e. " %20c %8z" (30 symbols), along with additional space for folding symbols of outline-minor-mode, line numbers, etc.
+
Record Formatting
+
When the “Show records” flag is enabled, associated records for each node are displayed. The formatting of these is defined by org-clock-agg-elem-format, which is also a format string with the following specifiers:
+Customize the formatting of these records through org-clock-agg-elem-format, which also utilizes a format string comprising the following specifiers:
+
+
%s: Start of the time range
+
%e: End of the time range
+
%d: Duration of the time range
+
%t: Title of the record.
+
+
The default value is:
+
- [%s]--[%e] => %d : %t
+
Custom grouping predicates
+
It’s possible to define custom grouping predicates in addition to the default ones. In fact, it’s probably the only way to get grouping that is tailored to your particular org workflow; I haven’t included my predicates in the package because they aren’t general enough.
+
To create new predicates, use org-clock-agg-defgroupby:
The extra-params variable is an alist of global parameters controlling the function’s behavior. Additional parameters can be added by customizing org-clock-agg-extra-params. This alist has keys as parameter names and values as widget.el expressions (applied to widget-create) controlling the UI. Each widget must contain an :extras-key key.
When checked, extra-params takes the value ((:extras-keys . t)).
+
Here’s an example predicate. I store meetings the following way:
+
* Some project
+** Meetings
+*** Some meeting 1
+*** Some meeting 2
+* Another project
+** Meetings
+*** Another meeting 1
+*** Another meeting 2 (offline)
+
I want to group these meetings by title, i.e. group all instances of “Some meeting”, “Another meeting”, etc. Optionally I want to group online and offline meetings.
A package to make sense of my lifeorg-journal records.
+
The package adds the org-journal: link type to Org Mode. When placed in an org-journal file, the link serves as a “tag” that references one or many paragraphs of the journal or the entire section. These tags are aggregated in the database that can be queried in various ways.
+
Rationale
+
Journal files, by their very nature, are weakly structured. A single journal note can reference multiple entities (or none) and can itself be composed of multiple parts that have in common only the date and time when they were written. Needless to say, it’s hard to find anything in such records.
+
This package attempts to improve the accessibility of the journal by:
+
+
Taking advantage of temporal data, e.g. allowing to query entries in some date range.
+
Allowing to extract (and reference) only certain parts of a particular journal entry.
+
Compensating weak structure by with more advanced query engine.
+
+
For instance, when I’m writing down the progress on a job project, I can leave a tag like job.<project-name> in the paragraph(s) related to that project. Later, I can query only those paragraphs that are referenced by this particular tag. The query results can then be narrowed, for instance, to include the word “backend”, or extended with some other tag.
+
If no tag matches the subject matter, the journal can be queried with a regular expression, e.g. by searching some regex within a specific time frame. Subsequent searches are also significantly faster than the built-in org-journal search functionality due to the to caching mechanism.
+
Installation
+
The package is available on MELPA. Install it however you normally install packages, my preferred way is use-package with straight:
To add an inline tag, you can manually create a link of the following format:
+
[[org-journal:<tag-name>][<tag-description>]]
+
Or run M-x org-journal-tags-insert-tag to insert a tag with a completion interface. The description is not aggregated and thus optional. Also, <tag-name> cannot contain :.
+
The link will reference the current Org Mode paragraph. If you want to reference more paragraphs, you can set the number of paragraphs like this:
Run M-x org-journal-tags-link-get-region-at-point to select the referenced region of the buffer.
+
To add a tag to the entire section, run M-x org-journal-tags-prop-set, which will create or update the Tags property in the property drawer of the current time section. This command features a notmuch-like UI, i.e. completing read for multiple entries, where +<tag> adds a tag and -<tag> deletes a tag.
+
If you decide to rename a tag, there’s M-x org-journal-tags-refactor.
+
Tag kinds
+
Tag kind is a predefined class of tag with some extra functionality. The link format fo such tags is as follows:
If <kind> is omitted, a tag is considered “normal”.
+
Running C-u M-x org-journal-tags-insert-tag will first prompt for the tag kind and then for the tag itself from the set of already used tags of that kind.
+
Running C-u C-u M-x org-journal-tags-insert-tag will also first prompt for the tag kind, but then will try to invoke the kind-specific tag selection logic, if such is available. For instance, the contact kind will prompt the org-contacts database.
+
For now, the only available tag kind is org-contacts.
+
Adding timestamps
+
In addition to tags, the package also aggregates inline timestamps, i.e. timestamps that are left in the text like this:
+
This is a text. This is a text with <2022-04-07 Thu> a timestamp. This is a text again.
+
A timestamp will reference just the current paragraph.
+
Other forms of timestamps (SCHEDULED, DEADLINE, etc.) are not supported at the moment, because this functionality is implemented well enough by org-agenda.
+
The envisioned use case for this functionality to leave references for the future to be seen at a particular date.
+
Database
+
The package stores tags and references to these tags in a database.
+
org-journal-tags-autosync-mode enables synchronizing the database at the moment of saving of the org-journal buffer. You can also run the synchronization manually:
+
+
M-x org-journal-tags-process-buffer to process the current buffer.
+
M-x org-journal-tags-db-sync to sync changed org-journal files in the filesystem.
+
+
The same mode enables saving the database on killing Emacs, but you can always run M-x org-journal-tags-db-save manually.
+
M-x org-journal-tags-db-unload saves and unloads the database from the memory, M-x org-journal-tags-db-reset creates a new database.
+
Status buffer
+
+
+
+
(I replaced tag names with “X” just for the screenshot)
+
M-x org-journal-tags-status opens the status buffer with some statistics about the journal and tags. Press ? to see the available keybindings.
+
Pressing RET on a tag name in the “All tags” section should open a query buffer set to return all references for this tag.
+
Query constructor
+
+
+
+
Pressing s in the status buffer or running M-x org-journal-tags-transient-query opens a transient.el buffer with query settings.
+
The options are as follows:
+
+
Include tags filters the references so that each reference had at least one of these tags.
+
Exclude tags filters the references so that each reference didn’t have any of these tags.
+
Include children includes child tags to the previous two lists.
+
Tag location can filter only section tags on inline tags.
+
Start date and End date filter the references by date.
+
Filter timestamps filters the references so that they include a timestamp.
+
Timestamp start date and Timestamp end date filter
+timestamps by their date.
+
Regex filter the references by a regular expression. It can be a string or rx expression (it just has to start with (rx in this case).
+
Narrow to regex makes it so that each reference had only paragraphs that have a regex match.
+
Sort sorts the result in ascending order. It’s descending by default.
+
+
Pressing RET or e executes the query. Journal files are cached, so subsequent queries within one session are much faster.
+
Query results
+
+
+
+
After the query completes, the package opens the results buffer. Press ? to see the available keybindings there.
+
Pressing RET opens the corresponding org-journal entry.
+
Pressing s opens the query constructor buffer. If opened from inside the query results, the query constructor has 4 additional options:
+
+
+
+
Command
+
Set operation
+
Description
+
+
+
+
+
Union
+
old ∪ new
+
Add records of the new query to the displayed records
+
+
+
Intersection
+
old ∩ new
+
Leave only those records that are both displayed and in the new query
+
+
+
Difference from current
+
old \ new
+
Exclude records of the new query from the displayed records
+
+
+
Difference to current
+
new \ old
+
Exclude displayed records from ones of the new query
+
+
+
+
Thus it is possible to make any query that can be described as a sequence of such set operations.
+
Advanced usage
+
Automatic tagging
+
org-journal provides a hook to automatically add information to the journal entries.
+
It can be used to automatically assign tags, for instance, based on hostname. Here’s an excerpt from my configuration:
Both ways are supported by this package (I use the first). The decryption of entries takes some time, but this is alleviated by caching.
+
The cache is stored in the org-journal-tags--files-cache variable, so in principle, someone could come to your computer and inspect the value of this variable (who would ever do that?). If that’s an issue, you can do something like:
To clear the cache on Emacs being idle after 15 minutes.
+
Also, as said above, org-journal-tags uses its own database, which is more like persistent cache for tags and references. You can encrypt it as well with epa by adding .gpg to the org-journal-tags-db-file variable:
The database is also stored in memory in org-journal-tags-db variable, so once again, someone could inspect the value of the variable or just run M-x org-journal-tags-status.
+
To avoid that, you can manually run M-x org-journal-tags-db-unload or add it to run-with-idle-timer:
If you have everything set up correctly, encrypting a file shouldn’t ask for a passphrase, so this function can be run automatically.
+
Advanced querying
+
This package provides an API for doing queries from the Lisp code.
+
The central function there org-journal-tags-query, which has an interface corresponding to the flags in the query constructor. Take a look at its docstring for more info.
+
Also, you can use some of the following operations on the set of journal references:
org-journal-tags--query-merge-refs - merge intersecting references within one set
+
org-journal-tags--query-sort-refs - order references by date
+
org-journal-tags--string-extract-refs - collect strings corresponding to references
+
+
Final notes
+
This package turned out to be almost as long and complex as org-journal itself, and it also introduces some new dependencies. Hence I decided it would be better off as a separate package.
+
Also, I want to list some sources of inspiration. The database logic is heavily inspired by elfeed. The UI with Emacs widgets for tags & completing-read-multiple and the tagging system in general is inspired by notmuch. Finally, transient.el and magit-section are the UI packages that made this one possible, or at least much easier to implement.
A pass frontend based on Ivy, made primarily to use with EXWM and ivy-posframe. Types fields from entries.
+
Also take a look at Nicolas Petton’s pass, password-store-ivy is designed as complementary to the Nicolas’ package.
+
This package is made with Ivy because I need some fine-tuning like actions and turning off sorting in some completions, and Ivy happens to be the completion system I’m using now.
+
Installation
+
As the package isn’t yet available anywhere but in this repository, you can clone the repository, add it to the load-path and require the package. My preferred way is use-package with straight:
In addition to the global override, sequences can be overriden per-entry with a field called sequence-<name>, where <name> is a key of password-store-ivy-sequences.
+
For example, here is an override to press Tab twice:
perspective-exwm-mode
+The mode does a couple of things:
+
+
advises away a bug with half-killing the current perspective when closing a floating window. I haven’t tested this as thoroughly I haven’t run into this issue for nearly a month, so it seems to be fixed. But there’s M-x perspective-exwm-revive-perspectives if the problem arises anyway.
+
fixes a bug with running persp-set-buffer on an EXWM buffer that was moved between workspaces by advising persp-buffer-in-other-p.
+
fixes a bug with persp-set-buffer copying all the perspectives from other workspaces to the current one.
+
adjusts the name of the initial perspective in the new workspace. It tries to get the name from the perspective-exwm-override-initial-name variable and fallbacks to main-<index>.
+
+
For the last point, I have the following in my configuration:
Having distinct perspective names between frames also serves a purpose, because otherwise there are issues with multiple perspectives sharing the same scratch buffer.
+
+
+
M-x perspective-exwm-cycle-exwm-buffers-forward, perspective-exwm-cycle-exwm-buffers-backward
+Cycle EXWM buffers in the current perspective.
+
+
+
+
The buffer highlighted in yellow is the current one, the buffer highlighted in blue is shown in another window of the perspective so it will be omitted from the cycle.
+
Set perspective-exwm-get-exwm-buffer-name to customize the displayed name, by default it’s exwm-class-name.
+
+
+
M-x perspective-exwm-cycle-all-buffers-forward, perspective-exwm-cycle-exwm-all-backward
+The same as above, but not restricted to EXWM buffers.
+
+
+
M-x perspective-exwm-switch-perspective
+Select a perspective from the list of all perspectives on all workspaces.
+
+
+
+
+
+
M-x perspective-exwm-copy-to-workspace
+Copy the current perspective to another EXWM workspace.
+
+
+
M-x perspective-exwm-move-to-workspace
+Move the current perspective to another EXWM workspace.
+
+
+
perspective-exwm-assign-windows
+A handy function to move the current window to a given workspace and/or perspective. Example usage:
Managing the timer with the excellent transient.el.
+
Persistent state between Emacs sessions.
+The timer state isn’t reset if you close Emacs. If necessary, the state file can be synchronized between machines.
+
History.
+History of the timer can be stored in a CSV file. Eventually, I want to join this with other activity data to see if the state of the timer changes how I use the computer.
+
+
Installation
+
The package is available on MELPA. Install it however you usually install Emacs packages, e.g.
Or you can clone the repository, add the package to the load-path and load it with require:
+
(require'pomm)
+
The package requires Emacs 27.1 because the time API of the previous versions is kinda crazy and 27.1 has time-convert.
+
Usage
+
Pomodoro
+
Run M-x pomm to open the transient buffer.
+
The listed commands are rather self-descriptive and match the Pomodoro ideology.
+
The timer can have 3 states:
+
+
Stopped. Can be started with “s” or M-x pomm-start. A new iteration of the timer will be started.
+
Paused. Can be continuted with “s” / M-x pomm-start or stopped competely with “S” / M-x pomm-stop.
+
Running. Can be paused with “p” / M-x pomm-pause or stopped with “S” / M-x pomm-stop.
+
+
The state of the timer can be reset with “R” or M-x pomm-reset.
+
“u” updates the transient buffer. The update is manual because I didn’t figure out how to automate this, and I think this is not really necessary.
+
With “r” or M-x pomm-set-context you can set the current “context”, that is some description of the task you are currently working on. This description will show up in history and in the csv file. Also, M-x pomm-start-with-context will prompt for the context and then start the timer.
+
Third Time
+
Run M-x pomm-third-time to open the transient buffer for the Third Time technique.
+
+
+
+
Essentially, the techique is designed aroud the formula:
+
Time of break = 1/3 x Time of work.
+
I.e. you work as long as you want or need, and then take a break with the maximum duration 1/3 of the time worked. If you take a shorter break, the remaining break time is saved and added to the next break within the same session. Here is a more detailed explanation.
+
The Third Time timer can have 2 states:
+
+
Stopped. Can be started with “s” or M-x pomm-third-time-start.
+
Running. Can be stopped with “S” or M-x pomm-third-time-stop. This resets the accumulated break time.
+
+
Use “b” or M-x pomm-third-time-switch to switch the current period type (work or break). If the break time runs out, the timer automatically switches to work.
+
Customization
+
Some settings are available in the transient buffer, but you can customize the relevant variables to make them permanent. Check M-x customize-grouppomm and M-x customize-group pomm-third-time for more information.
+
Alerts
+
The package sends alerts via alert.el. The default style of alert is a plain message, but if you want an actual notification, set alert-default-style accordingly:
+
(setqalert-default-style'libnotify)
+
Sounds
+
By default sounds are disabled. Set pomm-audio-enabled to t to toggle them. Set pomm-audio-tick-enabled to t if you want the ticking sound.
+
This functionality needs pomm-audio-player-executable to be set so that the program could be invoked like: <executable> /path/to/sound.wav.
+
The package ships with some built-it sounds, which you can replace by customizing the pomm-audio-files variable.
+
Modeline
+
If you want the timer to display in the modeline, activate the pomm-mode-line-mode minor mode.
+
Polybar module
+
If you want to display the Pomodoro status in something like polybar, you can add the following lines to your config:
To implement pesistence between Emacs sessions, the package stores its state in the following files:
+
+
pomm-state-file-location, .emacs.d/pomm by default
+
pomm-third-time-state-file-location, /.emacs.d/pomm-third-time by default
+
+
Set these paths however like.
+
History
+
If you set the pomm-csv-history-file (and/or pomm-third-time-csv-history-file) variable, the package will log its history in CSV format. Just keep in mind that the parent directory has to exist.
+
The file for the Pomodoro technique has the following columns:
+
+
timestamp
+
status (stopped, paused or running, according to the usage section)
+
kind (work, short-break, long-break or nil)
+
iteration
+
context
+
+
One for the Third Time technique has an extra column called break-time-remaining.
+
A new entry is written after a particular state of the timer comes into being.
+
To customize timestamp, set the pomm-csv-history-file-timestamp-format variable. For example, for traditional YYYY-MM-DD HH:mm:ss:
Or clone the repository, add it to the load-path and require the package.
+
Usage
+
There’s a single entrypoint for all implemented functions: M-x reverso. The UI is implemented using the excellent transient.el.
+
Input Handling
+
All commands handle input as follows:
+
By default, the input string is empty. If a command is launched with a region selected, use the string of that region. If launched with the prefix argument (C-u), use the entire buffer.
+
Results are displayed in reverso-result-mode buffers. When launched within that buffer, the command uses the input string specific to the buffer. If launched with C-u, it uses the output string from that buffer (if available).
+
Translation
+
Use M-x reverso t or M-x reverso-translate to invoke the translation transient.
+
+
+
+
The “Source language” and “Target language” parameters are self-explanatory. Note that not every language is compatible with every other language in the general case. “Swap languages” attempts to swap them.
+
Enabling “Brief translation output” will display only the translated version of the string in the output buffer.
+
+
+
+
Otherwise, the result buffer may contain the following sections:
+
+
Source text and Translation
+
Corrected text, if available
+
Context results, if available
+
+
Context results typically appear for short strings, as seen in the example from the screenshot.
+
Context
+
Use M-x reverso c or M-x reverso-context to invoke context search (or bilingual concordances, essentially a Rosetta stone generator).
+
The input/output UI resembles that of the translation command.
+
Interestingly, direct context search often yields different results than the “Context results” section of the translation command. Hence, checking both might provide more comprehensive data.
+
Synonyms
+
Use M-x reverso s or M-x reverso-synonyms to invoke the synonyms search.
+
+
+
+
+
+
+
If necessary, results are segmented by parts of speech.
+
Each part of speech section contains up to three subsections:
+
+
Synonyms
+
Examples
+
Antonyms
+
+
Grammar check
+
Use M-x reverso g or M-x reverso-grammar to invoke the grammar check.
+
+
+
+
Currently, only English, French, Spanish, and Italian languages are available.
+
+
+
+
The results may contain the following sections:
+
+
Source text, highlighting errors with reverso-error-face
+
Corrected text
+
Corrections
+
+
Grammar check in buffer
+
It can be convenient to apply the grammar check directly to the current buffer without displaying results in another buffer. Use M-x reverso b or M-x reverso-grammar-buffer for this.
+
+
+
+
Running e there (or M-x reverso-check-buffer) utilizes the current buffer as input and highlights any found errors using overlays. If a region is selected, the check is confined to that region.
+
There are a couple of caveats there. First, the service considers each linebreak as a new line, which is incompatible with filling text, i.e. breaking it into lines of a specified width. The “Remove linebreaks” option (l) is a workaround for this.
+
Secondly, the service usually freaks out with special syntax, for instance, Org Mode links.
+
The third issue partly follows from the second one, as the service often finds “errors” within hidden parts of Org links. Either skip these errors or execute M-x org-toggle-link-display in Org files beforehand.
+
Lastly (and this applies to all other methods as well), the API usually restricts input size. If the service returns an error, try running the command on a smaller region of the buffer.
+
+
+
+
When the cursor is placed on an error, the “Information” section provides details.
+
“Fix error” (f or M-x reverso-check-fix-at-point) opens a completion interface with potential fixes. “Ignore error” (i or M-x reverso-check-ignore-error) simply removes the overlay and moves to the next error.
+
“Previous error” (p or M-x reverso-check-prev-error), “Next error” (n or M-x reverso-check-next-error), “First error” (P or M-x reverso-check-first-error) and “Last error” (L or M-x reverso-check-last-error) serve to navigate the error list.
+
“Clear” (c or M-x reverso-clear) removes error overlays. If a region is selected, it removes overlays only in that region; otherwise, it removes them from the entire buffer.
+
History
+
Enable reverso-history-mode to keep history:
+
(reverso-history-mode)
+
I haven’t implemented persistence yet, but I might in the future.
+
After enabling the minor mode, M-x reverso-history or M-x reverso h will display recent commans. RET on shows the results of each command.
+
Caveats
+
Before proceeding further, here are some general caveats to be aware of.
+
Firstly, the package uses a reverse-engineered API, so all the typical consequences apply, such as sudden irreparable breakages. Although I’ve been using it for over a year, so… maybe not.
+
Secondly, the limit on input size has been mentioned. The obvious is executing commands on a smaller region.
+
Thirdly, there have been reports that Reverso dispatches IP bans to particularly enthusiastic users, so be cautious if you’re sending lots of automated queries. This is also why I didn’t implement running one command for multiple consecutive regions.
+
Lastly, exercise caution with the content sent to the service. Avoid inadvertently sharing confidential information (like passwords) or anything that could be used against you in other ways. While the service claims to be GDPR-compliant, we can’t actually check that.
+
Customization
+
Run M-x customize-group reverso to view the available parameters. Here are a few.
+
If you don’t need all 17 languages, customize the reverso-languages variable to narrow down the list:
+
(setqreverso-languages'(englishgermanrussian))
+
If the length of reverso-languages exceeds reverso-language-completing-read-threshold, switching a language in transient buffers will invoke completing-read (minibuffer completion). Otherwise, it will simply switch to the next language available.
+
reverso-max-display-lines-in-input controls the maximum number of lines displayed in the input section of a transient buffer.
+
The available faces:
+
+
reverso-highlight-face
+
reverso-error-face
+
reverso-heading-face
+
reverso-keyword-face
+
reverso-definition-face
+
+
are inherited from the faces of transient.el and basic-faces to look nice.
+
Elisp API
+
In Emacs Lisp, there are four primary functions that interact with the Reverso API:
+
+
reverso--translate
+
reverso--get-context
+
reverso--get-grammar
+
reverso--get-context
+
+
Refer to the docstrings for more detailed information.
+
Each function is asynchronous, and the results are retrieved via a callback.
+
As Reverso sometimes modifies its available languages and compatibility matrix, so if you change that, execute reverso-verify-settings to check for potential errors.
The emacs-grammarly package series provides the Elisp API for Grammarly (a grammar checking service) along with multiple frontends. Unlike Reverso, Grammarly has an official API (so you don’t risk getting an IP ban), and it allows a much larger input size.
+
Additionally, Grammarly is less bothered by Org and Markdown syntax, although it struggles with inline code blocks. It seems to do work generally better than Reverso, but it also generates a lot of false positives. For instance, it finds a lot of issues in The Economist articles, which, I think, have beautiful English.
+
Another notable grammar-checking solution is LanguageTool, which can be run offline and used with its Emacs package. This tool offers the advantage of unlimited usage and doesn’t transmit your data to a third-party server you can’t control. But it still doesn’t like markup syntaxes.
+
Also, I’ve been pretty happy with LTeX LS, which is a LanguageTool-based language server explicitly designed to support markup formats like Org, Markdown, LaTeX, among others.
+
The reverso-api npm package implements the same commands in JavaScript. It also provided invaluable information for creating this package.